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.FileInputStream;
22 import java.io.IOException;
23 import java.io.InputStreamReader;
24 import java.io.InterruptedIOException;
25 import java.io.OutputStreamWriter;
26 import java.math.BigInteger;
27 import java.net.ConnectException;
28 import java.net.InetSocketAddress;
29 import java.net.NoRouteToHostException;
30 import java.net.Socket;
31 import java.net.SocketAddress;
32 import java.net.SocketTimeoutException;
33 import java.net.UnknownHostException;
34 import java.nio.charset.StandardCharsets;
35 import java.security.GeneralSecurityException;
36 import java.security.KeyStore;
37 import java.security.NoSuchAlgorithmException;
38 import java.security.UnrecoverableKeyException;
39 import java.security.cert.Certificate;
40 import java.security.cert.CertificateEncodingException;
41 import java.security.cert.CertificateException;
42 import java.security.cert.X509Certificate;
43 import java.util.concurrent.BlockingQueue;
44 import java.util.concurrent.Future;
45 import java.util.concurrent.LinkedBlockingQueue;
46 import java.util.concurrent.ScheduledExecutorService;
47 import java.util.concurrent.ScheduledFuture;
48 import java.util.concurrent.TimeUnit;
50 import javax.net.ssl.KeyManagerFactory;
51 import javax.net.ssl.SSLContext;
52 import javax.net.ssl.SSLServerSocket;
53 import javax.net.ssl.SSLServerSocketFactory;
54 import javax.net.ssl.SSLSession;
55 import javax.net.ssl.SSLSocket;
56 import javax.net.ssl.SSLSocketFactory;
57 import javax.net.ssl.TrustManager;
58 import javax.net.ssl.X509TrustManager;
60 import org.eclipse.jdt.annotation.NonNullByDefault;
61 import org.eclipse.jdt.annotation.Nullable;
62 import org.openhab.binding.androidtv.internal.AndroidTVHandler;
63 import org.openhab.binding.androidtv.internal.utils.AndroidTVPKI;
64 import org.openhab.core.OpenHAB;
65 import org.openhab.core.library.types.NextPreviousType;
66 import org.openhab.core.library.types.OnOffType;
67 import org.openhab.core.library.types.PercentType;
68 import org.openhab.core.library.types.PlayPauseType;
69 import org.openhab.core.library.types.RewindFastforwardType;
70 import org.openhab.core.library.types.StringType;
71 import org.openhab.core.thing.ChannelUID;
72 import org.openhab.core.types.Command;
73 import org.slf4j.Logger;
74 import org.slf4j.LoggerFactory;
77 * The {@link GoogleTVConnectionManager} is responsible for handling connections via the googletv protocol
79 * Significant portions reused from Lutron binding with permission from Bob A.
81 * @author Ben Rosenblum - Initial contribution
84 public class GoogleTVConnectionManager {
85 private static final int DEFAULT_RECONNECT_SECONDS = 60;
86 private static final int DEFAULT_HEARTBEAT_SECONDS = 5;
87 private static final long KEEPALIVE_TIMEOUT_SECONDS = 30;
88 private static final String DEFAULT_KEYSTORE_PASSWORD = "secret";
89 private static final String DEFAULT_MODE = "NORMAL";
90 private static final String PIN_MODE = "PIN";
91 private static final int DEFAULT_PORT = 6466;
92 private static final int PIN_DELAY = 1000;
94 private final Logger logger = LoggerFactory.getLogger(GoogleTVConnectionManager.class);
96 private ScheduledExecutorService scheduler;
98 private final AndroidTVHandler handler;
99 private GoogleTVConfiguration config;
101 private @NonNullByDefault({}) SSLSocketFactory sslSocketFactory;
102 private @Nullable SSLSocket sslSocket;
103 private @Nullable BufferedWriter writer;
104 private @Nullable BufferedReader reader;
106 private @NonNullByDefault({}) SSLServerSocketFactory sslServerSocketFactory;
107 private @Nullable Socket shimServerSocket;
108 private @Nullable BufferedWriter shimWriter;
109 private @Nullable BufferedReader shimReader;
111 private @Nullable GoogleTVConnectionManager connectionManager;
112 private @Nullable GoogleTVConnectionManager childConnectionManager;
113 private @NonNullByDefault({}) GoogleTVMessageParser messageParser;
115 private final BlockingQueue<GoogleTVCommand> sendQueue = new LinkedBlockingQueue<>();
116 private final BlockingQueue<GoogleTVCommand> shimQueue = new LinkedBlockingQueue<>();
118 private @Nullable Future<?> asyncInitializeTask;
119 private @Nullable Future<?> shimAsyncInitializeTask;
121 private @Nullable Thread senderThread;
122 private @Nullable Thread readerThread;
123 private @Nullable Thread shimSenderThread;
124 private @Nullable Thread shimReaderThread;
126 private @Nullable ScheduledFuture<?> keepAliveJob;
127 private @Nullable ScheduledFuture<?> keepAliveReconnectJob;
128 private @Nullable ScheduledFuture<?> connectRetryJob;
129 private final Object keepAliveReconnectLock = new Object();
130 private final Object connectionLock = new Object();
132 private @Nullable ScheduledFuture<?> deviceHealthJob;
133 private boolean isOnline = true;
135 private StringBuffer sbReader = new StringBuffer();
136 private StringBuffer sbShimReader = new StringBuffer();
137 private String thisMsg = "";
139 private X509Certificate @Nullable [] shimX509ClientChain;
140 private Certificate @Nullable [] shimClientChain;
141 private Certificate @Nullable [] shimServerChain;
142 private Certificate @Nullable [] shimClientLocalChain;
144 private boolean disposing = false;
145 private boolean isLoggedIn = false;
146 private String statusMessage = "";
147 private String pinHash = "";
148 private String shimPinHash = "";
150 private boolean power = false;
151 private String volCurr = "00";
152 private String volMax = "ff";
153 private boolean volMute = false;
154 private String audioMode = "";
155 private String currentApp = "";
156 private String manufacturer = "";
157 private String model = "";
158 private String androidVersion = "";
159 private String remoteServer = "";
160 private String remoteServerVersion = "";
162 private AndroidTVPKI androidtvPKI = new AndroidTVPKI();
163 private byte[] encryptionKey;
165 public GoogleTVConnectionManager(AndroidTVHandler handler, GoogleTVConfiguration config) {
166 messageParser = new GoogleTVMessageParser(this);
167 this.config = config;
168 this.handler = handler;
169 this.connectionManager = this;
170 this.scheduler = handler.getScheduler();
171 this.encryptionKey = androidtvPKI.generateEncryptionKey();
175 public GoogleTVConnectionManager(AndroidTVHandler handler, GoogleTVConfiguration config,
176 GoogleTVConnectionManager connectionManager) {
177 messageParser = new GoogleTVMessageParser(this);
178 this.config = config;
179 this.handler = handler;
180 this.connectionManager = connectionManager;
181 this.scheduler = handler.getScheduler();
182 this.encryptionKey = androidtvPKI.generateEncryptionKey();
186 public String getThingID() {
187 return handler.getThingID();
190 public void setManufacturer(String manufacturer) {
191 this.manufacturer = manufacturer;
192 handler.setThingProperty("manufacturer", manufacturer);
195 public String getManufacturer() {
199 public void setModel(String model) {
201 handler.setThingProperty("model", model);
204 public String getModel() {
208 public void setAndroidVersion(String androidVersion) {
209 this.androidVersion = androidVersion;
210 handler.setThingProperty("androidVersion", androidVersion);
213 public String getAndroidVersion() {
214 return androidVersion;
217 public void setRemoteServer(String remoteServer) {
218 this.remoteServer = remoteServer;
219 handler.setThingProperty("remoteServer", remoteServer);
222 public String getRemoteServer() {
226 public void setRemoteServerVersion(String remoteServerVersion) {
227 this.remoteServerVersion = remoteServerVersion;
228 handler.setThingProperty("remoteServerVersion", remoteServerVersion);
231 public String getRemoteServerVersion() {
232 return remoteServerVersion;
235 public void setPower(boolean power) {
237 logger.debug("{} - Setting power to {}", handler.getThingID(), power);
239 handler.updateChannelState(CHANNEL_POWER, OnOffType.ON);
241 handler.updateChannelState(CHANNEL_POWER, OnOffType.OFF);
245 public boolean getPower() {
249 public void setVolCurr(String volCurr) {
250 this.volCurr = volCurr;
251 int max = Integer.parseInt(this.volMax, 16);
252 int volume = ((Integer.parseInt(volCurr, 16) * 100) / max);
253 handler.updateChannelState(CHANNEL_VOLUME, new PercentType(volume));
256 public String getVolCurr() {
260 public void setVolMax(String volMax) {
261 this.volMax = volMax;
264 public String getVolMax() {
268 public void setVolMute(String volMute) {
269 if (DELIMITER_00.equals(volMute)) {
270 this.volMute = false;
271 handler.updateChannelState(CHANNEL_MUTE, OnOffType.OFF);
272 } else if (DELIMITER_01.equals(volMute)) {
274 handler.updateChannelState(CHANNEL_MUTE, OnOffType.ON);
278 public boolean getVolMute() {
282 public void setAudioMode(String audioMode) {
283 this.audioMode = audioMode;
286 public String getAudioMode() {
290 public void setCurrentApp(String currentApp) {
291 this.currentApp = currentApp;
292 handler.updateChannelState(CHANNEL_APP, new StringType(currentApp));
295 public String getStatusMessage() {
296 return statusMessage;
299 private void setStatus(boolean isLoggedIn) {
301 setStatus(isLoggedIn, "ONLINE");
303 setStatus(isLoggedIn, "UNKNOWN");
307 private void setStatus(boolean isLoggedIn, String statusMessage) {
308 if ((this.isLoggedIn != isLoggedIn) || (!this.statusMessage.equals(statusMessage))) {
309 this.isLoggedIn = isLoggedIn;
310 this.statusMessage = statusMessage;
311 handler.checkThingStatus();
315 public String getCurrentApp() {
319 public void setLoggedIn(boolean isLoggedIn) {
320 if (this.isLoggedIn != isLoggedIn) {
321 setStatus(isLoggedIn);
325 public boolean getLoggedIn() {
329 private boolean servicePing() {
332 SocketAddress socketAddress = new InetSocketAddress(config.ipAddress, config.port);
333 try (Socket socket = new Socket()) {
334 socket.connect(socketAddress, timeout);
336 } catch (ConnectException | SocketTimeoutException | NoRouteToHostException ignored) {
338 } catch (IOException ignored) {
339 // IOException is thrown by automatic close() of the socket.
340 // This should actually never return a value as we should return true above already
345 private void checkHealth() {
348 isOnline = servicePing();
352 logger.debug("{} - Device Health - Online: {} - Logged In: {} - Mode: {}", handler.getThingID(), isOnline,
353 isLoggedIn, config.mode);
354 if (isOnline != this.isOnline) {
355 this.isOnline = isOnline;
357 logger.debug("{} - Device is back online. Attempting reconnection.", handler.getThingID());
363 private void setShimX509ClientChain(X509Certificate @Nullable [] shimX509ClientChain) {
365 this.shimX509ClientChain = shimX509ClientChain;
366 logger.trace("Setting shimX509ClientChain {}", config.port);
367 if (shimX509ClientChain != null && logger.isTraceEnabled()) {
368 for (int cert = 0; cert < shimX509ClientChain.length; cert++) {
369 logger.trace("Subject DN: {}", shimX509ClientChain[cert].getSubjectX500Principal());
370 logger.trace("Issuer DN: {}", shimX509ClientChain[cert].getIssuerX500Principal());
371 logger.trace("Serial number: {}", shimX509ClientChain[cert].getSerialNumber());
372 logger.trace("Cert: {}", GoogleTVRequest
373 .decodeMessage(GoogleTVUtils.byteArrayToString(shimX509ClientChain[cert].getEncoded())));
376 } catch (CertificateEncodingException e) {
377 logger.trace("setShimX509ClientChain CertificateEncodingException", e);
381 private void startChildConnectionManager(int port, String mode) {
382 GoogleTVConfiguration childConfig = new GoogleTVConfiguration();
383 childConfig.ipAddress = config.ipAddress;
384 childConfig.port = port;
385 childConfig.reconnect = config.reconnect;
386 childConfig.heartbeat = config.heartbeat;
387 childConfig.keystoreFileName = config.keystoreFileName;
388 childConfig.keystorePassword = config.keystorePassword;
389 childConfig.delay = config.delay;
390 childConfig.shim = config.shim;
391 childConfig.mode = mode;
392 logger.debug("{} - startChildConnectionManager parent config: {} {} {}", handler.getThingID(), config.port,
393 config.mode, config.shim);
394 logger.debug("{} - startChildConnectionManager child config: {} {} {}", handler.getThingID(), childConfig.port,
395 childConfig.mode, childConfig.shim);
396 childConnectionManager = new GoogleTVConnectionManager(this.handler, childConfig, this);
399 private TrustManager[] defineNoOpTrustManager() {
400 return new TrustManager[] { new X509TrustManager() {
402 public void checkClientTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType) {
403 logger.debug("Assuming client certificate is valid");
404 if (chain != null && logger.isTraceEnabled()) {
405 for (int cert = 0; cert < chain.length; cert++) {
406 logger.trace("Subject DN: {}", chain[cert].getSubjectX500Principal());
407 logger.trace("Issuer DN: {}", chain[cert].getIssuerX500Principal());
408 logger.trace("Serial number: {}", chain[cert].getSerialNumber());
414 public void checkServerTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType) {
415 logger.debug("Assuming server certificate is valid");
416 if (chain != null && logger.isTraceEnabled()) {
417 for (int cert = 0; cert < chain.length; cert++) {
418 logger.trace("Subject DN: {}", chain[cert].getSubjectX500Principal());
419 logger.trace("Issuer DN: {}", chain[cert].getIssuerX500Principal());
420 logger.trace("Serial number: {}", chain[cert].getSerialNumber());
426 public X509Certificate @Nullable [] getAcceptedIssuers() {
427 X509Certificate[] x509ClientChain = shimX509ClientChain;
428 if (x509ClientChain != null && logger.isTraceEnabled()) {
429 logger.debug("Returning shimX509ClientChain for getAcceptedIssuers");
430 for (int cert = 0; cert < x509ClientChain.length; cert++) {
431 logger.trace("Subject DN: {}", x509ClientChain[cert].getSubjectX500Principal());
432 logger.trace("Issuer DN: {}", x509ClientChain[cert].getIssuerX500Principal());
433 logger.trace("Serial number: {}", x509ClientChain[cert].getSerialNumber());
435 return x509ClientChain;
437 logger.debug("Returning empty certificate for getAcceptedIssuers");
438 return new X509Certificate[0];
444 private void initialize() {
445 SSLContext sslContext;
447 String folderName = OpenHAB.getUserDataFolder() + "/androidtv";
448 File folder = new File(folderName);
450 if (!folder.exists()) {
451 logger.debug("Creating directory {}", folderName);
455 config.port = (config.port > 0) ? config.port : DEFAULT_PORT;
456 config.reconnect = (config.reconnect > 0) ? config.reconnect : DEFAULT_RECONNECT_SECONDS;
457 config.heartbeat = (config.heartbeat > 0) ? config.heartbeat : DEFAULT_HEARTBEAT_SECONDS;
458 config.delay = (config.delay < 0) ? 0 : config.delay;
459 config.shim = (config.shim) ? true : false;
460 config.shimNewKeys = (config.shimNewKeys) ? true : false;
461 config.mode = (!config.mode.equals("")) ? config.mode : DEFAULT_MODE;
463 config.keystoreFileName = (!config.keystoreFileName.equals("")) ? config.keystoreFileName
464 : folderName + "/googletv." + ((config.shim) ? "shim." : "") + handler.getThing().getUID().getId()
466 config.keystorePassword = (!config.keystorePassword.equals("")) ? config.keystorePassword
467 : DEFAULT_KEYSTORE_PASSWORD;
469 androidtvPKI.setKeystoreFileName(config.keystoreFileName);
470 androidtvPKI.setAlias("nvidia");
472 if (config.mode.equals(DEFAULT_MODE)) {
473 deviceHealthJob = scheduler.scheduleWithFixedDelay(this::checkHealth, config.heartbeat, config.heartbeat,
478 File keystoreFile = new File(config.keystoreFileName);
480 if (!keystoreFile.exists() || config.shimNewKeys) {
481 androidtvPKI.generateNewKeyPair(encryptionKey);
482 androidtvPKI.saveKeyStore(config.keystorePassword, this.encryptionKey);
484 androidtvPKI.loadFromKeyStore(config.keystorePassword, this.encryptionKey);
487 logger.trace("{} - Initializing SSL Context", handler.getThingID());
488 KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
489 kmf.init(androidtvPKI.getKeyStore(config.keystorePassword, this.encryptionKey),
490 config.keystorePassword.toCharArray());
492 TrustManager[] trustManagers = defineNoOpTrustManager();
494 sslContext = SSLContext.getInstance("TLS");
495 sslContext.init(kmf.getKeyManagers(), trustManagers, null);
497 sslSocketFactory = sslContext.getSocketFactory();
499 asyncInitializeTask = scheduler.submit(this::connect);
501 shimAsyncInitializeTask = scheduler.submit(this::shimInitialize);
503 } catch (NoSuchAlgorithmException | IOException e) {
504 setStatus(false, "Error initializing keystore");
505 logger.debug("Error initializing keystore", e);
506 } catch (UnrecoverableKeyException e) {
507 setStatus(false, "Key unrecoverable with supplied password");
508 } catch (GeneralSecurityException e) {
509 logger.debug("General security exception", e);
510 } catch (Exception e) {
511 logger.debug("General exception", e);
515 public void connect() {
516 synchronized (connectionLock) {
517 if (isOnline || config.mode.equals(PIN_MODE)) {
519 logger.debug("{} - Opening GoogleTV SSL connection to {}:{} {}", handler.getThingID(),
520 config.ipAddress, config.port, config.mode);
521 SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(config.ipAddress, config.port);
522 sslSocket.startHandshake();
523 this.shimServerChain = ((SSLSocket) sslSocket).getSession().getPeerCertificates();
524 writer = new BufferedWriter(
525 new OutputStreamWriter(sslSocket.getOutputStream(), StandardCharsets.ISO_8859_1));
526 reader = new BufferedReader(
527 new InputStreamReader(sslSocket.getInputStream(), StandardCharsets.ISO_8859_1));
528 this.sslSocket = sslSocket;
529 this.sendQueue.clear();
530 logger.debug("{} - Connection to {}:{} {} successful", handler.getThingID(), config.ipAddress,
531 config.port, config.mode);
532 } catch (UnknownHostException e) {
533 setStatus(false, "Unknown host");
534 logger.debug("{} - Unknown host {}", handler.getThingID(), config.ipAddress);
536 } catch (IllegalArgumentException e) {
537 // port out of valid range
538 setStatus(false, "Invalid port number");
539 logger.debug("{} - Invalid port number {}:{}", handler.getThingID(), config.ipAddress, config.port);
541 } catch (InterruptedIOException e) {
542 logger.debug("{} - Interrupted while establishing GoogleTV connection", handler.getThingID());
543 Thread.currentThread().interrupt();
545 } catch (IOException e) {
546 String message = e.getMessage();
547 if ((message != null) && (message.contains("certificate_unknown"))
548 && (!config.mode.equals(PIN_MODE)) && (!config.shim)) {
549 setStatus(false, "PIN Process Incomplete");
550 logger.debug("{} - GoogleTV PIN Process Incomplete", handler.getThingID());
551 reconnectTaskCancel(true);
552 startChildConnectionManager(this.config.port + 1, PIN_MODE);
553 } else if ((message != null) && (message.contains("certificate_unknown")) && (config.shim)) {
554 logger.debug("Shim cert_unknown I/O error while connecting: {}", e.getMessage());
555 Socket shimServerSocket = this.shimServerSocket;
556 if (shimServerSocket != null) {
558 shimServerSocket.close();
559 } catch (IOException ex) {
560 logger.debug("Error closing GoogleTV SSL socket: {}", ex.getMessage());
562 this.shimServerSocket = null;
565 setStatus(false, "Error opening GoogleTV SSL connection. Check log.");
566 logger.info("{} - Error opening GoogleTV SSL connection to {}:{} {}", handler.getThingID(),
567 config.ipAddress, config.port, e.getMessage());
569 scheduleConnectRetry(config.reconnect); // Possibly a temporary problem. Try again later.
574 setStatus(false, "Initializing");
576 logger.trace("{} - Starting Reader Thread for {}:{}", handler.getThingID(), config.ipAddress,
579 Thread readerThread = new Thread(this::readerThreadJob, "GoogleTV reader " + handler.getThingID());
580 readerThread.setDaemon(true);
581 readerThread.start();
582 this.readerThread = readerThread;
584 logger.trace("{} - Starting Sender Thread for {}:{}", handler.getThingID(), config.ipAddress,
587 Thread senderThread = new Thread(this::senderThreadJob, "GoogleTV sender " + handler.getThingID());
588 senderThread.setDaemon(true);
589 senderThread.start();
590 this.senderThread = senderThread;
592 logger.trace("{} - Checking for PIN MODE for {}:{} {}", handler.getThingID(), config.ipAddress,
593 config.port, config.mode);
595 if (config.mode.equals(PIN_MODE)) {
596 logger.trace("{} - Sending PIN Login to {}:{}", handler.getThingID(), config.ipAddress,
598 // Send app name and device name
599 sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage(GoogleTVRequest.loginRequest(1))));
600 // Unknown but required
601 sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage(GoogleTVRequest.loginRequest(2))));
602 // Don't send pin request yet, let user send REQUEST via PINCODE channel
604 logger.trace("{} - Not PIN Mode {}:{} {}", handler.getThingID(), config.ipAddress, config.port,
608 scheduleConnectRetry(config.reconnect); // Possibly a temporary problem. Try again later.
613 public void shimInitialize() {
614 synchronized (connectionLock) {
615 AndroidTVPKI shimPKI = new AndroidTVPKI();
616 byte[] shimEncryptionKey = shimPKI.generateEncryptionKey();
617 SSLContext sslContext;
620 shimPKI.generateNewKeyPair(shimEncryptionKey);
621 // Move this to PKI. Shim requires a trusted cert chain in the keystore.
622 KeyStore keystore = KeyStore.getInstance("JKS");
623 FileInputStream keystoreInputStream = new FileInputStream(config.keystoreFileName);
624 keystore.load(keystoreInputStream, config.keystorePassword.toCharArray());
626 KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
627 kmf.init(keystore, config.keystorePassword.toCharArray());
628 TrustManager[] trustManagers = defineNoOpTrustManager();
630 sslContext = SSLContext.getInstance("TLS");
631 sslContext.init(kmf.getKeyManagers(), trustManagers, null);
632 this.sslServerSocketFactory = sslContext.getServerSocketFactory();
634 logger.trace("Opening GoogleTV shim on port {}", config.port);
635 SSLServerSocket sslServerSocket = (SSLServerSocket) this.sslServerSocketFactory
636 .createServerSocket(config.port);
637 if (this.config.mode.equals(DEFAULT_MODE)) {
638 sslServerSocket.setNeedClientAuth(true);
640 sslServerSocket.setWantClientAuth(true);
644 logger.trace("Waiting for shim connection... {}", config.port);
645 if (this.config.mode.equals(DEFAULT_MODE) && (childConnectionManager == null)) {
646 logger.trace("Starting childConnectionManager {}", config.port);
647 startChildConnectionManager(this.config.port + 1, PIN_MODE);
649 SSLSocket serverSocket = (SSLSocket) sslServerSocket.accept();
650 logger.trace("shimInitialize accepted {}", config.port);
652 serverSocket.startHandshake();
653 logger.trace("shimInitialize startHandshake {}", config.port);
655 logger.trace("shimInitialize connected {}", config.port);
657 SSLSession session = serverSocket.getSession();
658 Certificate[] cchain2 = session.getPeerCertificates();
659 this.shimClientChain = cchain2;
660 Certificate[] cchain3 = session.getLocalCertificates();
661 this.shimClientLocalChain = cchain3;
663 X509Certificate[] shimX509ClientChain = new X509Certificate[cchain2.length];
665 for (int i = 0; i < cchain2.length; i++) {
666 logger.trace("Connection from: {}",
667 ((X509Certificate) cchain2[i]).getSubjectX500Principal());
668 shimX509ClientChain[i] = ((X509Certificate) cchain2[i]);
671 if (this.config.mode.equals(PIN_MODE)) {
672 this.shimX509ClientChain = shimX509ClientChain;
673 GoogleTVConnectionManager connectionManager = this.connectionManager;
674 if (connectionManager != null) {
675 connectionManager.setShimX509ClientChain(shimX509ClientChain);
679 if (cchain3 != null) {
680 for (int i = 0; i < cchain3.length; i++) {
681 logger.trace("Connection from: {}",
682 ((X509Certificate) cchain3[i]).getSubjectX500Principal());
686 logger.trace("Peer host is {}", session.getPeerHost());
687 logger.trace("Cipher is {}", session.getCipherSuite());
688 logger.trace("Protocol is {}", session.getProtocol());
689 logger.trace("ID is {}", new BigInteger(session.getId()));
690 logger.trace("Session created in {}", session.getCreationTime());
691 logger.trace("Session accessed in {}", session.getLastAccessedTime());
693 shimWriter = new BufferedWriter(
694 new OutputStreamWriter(serverSocket.getOutputStream(), StandardCharsets.ISO_8859_1));
695 shimReader = new BufferedReader(
696 new InputStreamReader(serverSocket.getInputStream(), StandardCharsets.ISO_8859_1));
697 this.shimServerSocket = serverSocket;
698 this.shimQueue.clear();
700 Thread readerThread = new Thread(this::shimReaderThreadJob, "GoogleTV shim reader");
701 readerThread.setDaemon(true);
702 readerThread.start();
703 this.shimReaderThread = readerThread;
705 Thread senderThread = new Thread(this::shimSenderThreadJob, "GoogleTV shim sender");
706 senderThread.setDaemon(true);
707 senderThread.start();
708 this.shimSenderThread = senderThread;
709 } catch (Exception e) {
710 logger.trace("Shim initalization exception {}", config.port);
711 logger.trace("Shim initalization exception", e);
714 } catch (Exception e) {
715 logger.trace("Shim initalization exception {}", config.port);
716 logger.trace("Shim initalization exception", e);
723 private void scheduleConnectRetry(long waitSeconds) {
724 logger.trace("{} - Scheduling GoogleTV connection retry in {} seconds", handler.getThingID(), waitSeconds);
725 connectRetryJob = scheduler.schedule(this::connect, waitSeconds, TimeUnit.SECONDS);
729 * Disconnect from bridge, cancel retry and keepalive jobs, stop reader and writer threads, and
732 * @param interruptAll Set if reconnect task should be interrupted if running. Should be false when calling from
733 * connect or reconnect, and true when calling from dispose.
735 private void disconnect(boolean interruptAll) {
736 synchronized (connectionLock) {
737 logger.debug("{} - Disconnecting GoogleTV", handler.getThingID());
739 this.isLoggedIn = false;
741 ScheduledFuture<?> connectRetryJob = this.connectRetryJob;
742 if (connectRetryJob != null) {
743 connectRetryJob.cancel(true);
745 ScheduledFuture<?> keepAliveJob = this.keepAliveJob;
746 if (keepAliveJob != null) {
747 keepAliveJob.cancel(true);
749 reconnectTaskCancel(interruptAll); // May be called from keepAliveReconnectJob thread
751 Thread senderThread = this.senderThread;
752 if (senderThread != null && senderThread.isAlive()) {
753 senderThread.interrupt();
756 Thread readerThread = this.readerThread;
757 if (readerThread != null && readerThread.isAlive()) {
758 readerThread.interrupt();
761 Thread shimSenderThread = this.shimSenderThread;
762 if (shimSenderThread != null && shimSenderThread.isAlive()) {
763 shimSenderThread.interrupt();
766 Thread shimReaderThread = this.shimReaderThread;
767 if (shimReaderThread != null && shimReaderThread.isAlive()) {
768 shimReaderThread.interrupt();
771 SSLSocket sslSocket = this.sslSocket;
772 if (sslSocket != null) {
775 } catch (IOException e) {
776 logger.debug("Error closing GoogleTV SSL socket: {}", e.getMessage());
778 this.sslSocket = null;
780 BufferedReader reader = this.reader;
781 if (reader != null) {
784 } catch (IOException e) {
785 logger.debug("Error closing reader: {}", e.getMessage());
788 BufferedWriter writer = this.writer;
789 if (writer != null) {
792 } catch (IOException e) {
793 logger.debug("Error closing writer: {}", e.getMessage());
797 Socket shimServerSocket = this.shimServerSocket;
798 if (shimServerSocket != null) {
800 shimServerSocket.close();
801 } catch (IOException e) {
802 logger.debug("Error closing GoogleTV SSL socket: {}", e.getMessage());
804 this.shimServerSocket = null;
806 BufferedReader shimReader = this.shimReader;
807 if (shimReader != null) {
810 } catch (IOException e) {
811 logger.debug("Error closing shimReader: {}", e.getMessage());
814 BufferedWriter shimWriter = this.shimWriter;
815 if (shimWriter != null) {
818 } catch (IOException e) {
819 logger.debug("Error closing shimWriter: {}", e.getMessage());
825 private void reconnect() {
826 synchronized (connectionLock) {
827 if (!this.disposing) {
828 logger.debug("{} - Attempting to reconnect to the GoogleTV", handler.getThingID());
829 setStatus(false, "reconnecting");
837 * Method executed by the message sender thread (senderThread)
839 private void senderThreadJob() {
840 logger.debug("{} - Command sender thread started {}", handler.getThingID(), config.port);
842 while (!Thread.currentThread().isInterrupted() && writer != null) {
843 GoogleTVCommand command = sendQueue.take();
846 BufferedWriter writer = this.writer;
847 if (writer != null) {
848 logger.trace("{} - Raw GoogleTV command decodes as: {}", handler.getThingID(),
849 GoogleTVRequest.decodeMessage(command.toString()));
850 writer.write(command.toString());
853 } catch (InterruptedIOException e) {
854 logger.debug("Interrupted while sending to GoogleTV");
855 setStatus(false, "Interrupted");
856 break; // exit loop and terminate thread
857 } catch (IOException e) {
858 logger.warn("{} - Communication error, will try to reconnect GoogleTV. Error: {}",
859 handler.getThingID(), e.getMessage());
860 setStatus(false, "Communication error, will try to reconnect");
861 sendQueue.add(command); // Requeue command
862 this.isLoggedIn = false;
864 break; // reconnect() will start a new thread; terminate this one
866 if (config.delay > 0) {
867 Thread.sleep(config.delay); // introduce delay to throttle send rate
870 } catch (InterruptedException e) {
871 Thread.currentThread().interrupt();
873 logger.debug("{} - Command sender thread exiting {}", handler.getThingID(), config.port);
877 private void shimSenderThreadJob() {
878 logger.debug("Shim sender thread started");
880 while (!Thread.currentThread().isInterrupted() && shimWriter != null) {
881 GoogleTVCommand command = shimQueue.take();
884 BufferedWriter writer = this.shimWriter;
885 if (writer != null) {
886 logger.trace("Shim received from google: {}",
887 GoogleTVRequest.decodeMessage(command.toString()));
888 writer.write(command.toString());
891 } catch (InterruptedIOException e) {
892 logger.debug("Shim interrupted while sending.");
893 break; // exit loop and terminate thread
894 } catch (IOException e) {
895 logger.warn("Shim communication error. Error: {}", e.getMessage());
896 break; // reconnect() will start a new thread; terminate this one
899 } catch (InterruptedException e) {
900 Thread.currentThread().interrupt();
902 logger.debug("Command sender thread exiting");
907 * Method executed by the message reader thread (readerThread)
909 private void readerThreadJob() {
910 logger.debug("{} - Message reader thread started {}", handler.getThingID(), config.port);
912 BufferedReader reader = this.reader;
915 while (!Thread.interrupted() && reader != null) {
916 thisMsg = GoogleTVRequest.fixMessage(Integer.toHexString(reader.read()));
917 if (HARD_DROP.equals(thisMsg)) {
918 // Google has crashed the connection. Disconnect hard.
919 logger.debug("{} - readerThreadJob received ffffffff. Disconnecting hard.", handler.getThingID());
920 this.isLoggedIn = false;
925 length = Integer.parseInt(thisMsg.toString(), 16);
926 logger.trace("{} - readerThreadJob message length {}", handler.getThingID(), length);
928 sbReader = new StringBuffer();
929 sbReader.append(thisMsg.toString());
931 sbReader.append(thisMsg.toString());
935 if ((length > 0) && (current == length)) {
936 logger.trace("{} - GoogleTV Message: {} {}", handler.getThingID(), length, sbReader.toString());
937 messageParser.handleMessage(sbReader.toString());
939 String thisCommand = interceptMessages(sbReader.toString());
940 shimQueue.add(new GoogleTVCommand(GoogleTVRequest.encodeMessage(thisCommand)));
945 } catch (InterruptedIOException e) {
946 logger.debug("Interrupted while reading");
947 setStatus(false, "Interrupted");
948 } catch (IOException e) {
949 String message = e.getMessage();
950 if ((message != null) && (message.contains("certificate_unknown")) && (!config.mode.equals(PIN_MODE))
952 setStatus(false, "PIN Process Incomplete");
953 logger.debug("{} - GoogleTV PIN Process Incomplete", handler.getThingID());
954 reconnectTaskCancel(true);
955 startChildConnectionManager(this.config.port + 1, PIN_MODE);
956 } else if ((message != null) && (message.contains("certificate_unknown")) && (config.shim)) {
957 logger.debug("Shim cert_unknown I/O error while reading from stream: {}", e.getMessage());
958 Socket shimServerSocket = this.shimServerSocket;
959 if (shimServerSocket != null) {
961 shimServerSocket.close();
962 } catch (IOException ex) {
963 logger.debug("Error closing GoogleTV SSL socket: {}", ex.getMessage());
965 this.shimServerSocket = null;
968 logger.debug("I/O error while reading from stream: {}", e.getMessage());
969 setStatus(false, "I/O Error");
971 } catch (RuntimeException e) {
972 logger.warn("Runtime exception in reader thread", e);
973 setStatus(false, "Runtime exception");
975 logger.debug("{} - Message reader thread exiting {}", handler.getThingID(), config.port);
979 private String interceptMessages(String message) {
980 if (message.startsWith("080210c801c202", 2)) {
981 // intercept PIN hash and replace with valid shim hash
982 int length = this.pinHash.length() / 2;
983 String len1 = GoogleTVRequest.fixMessage(Integer.toHexString(length + 2));
984 String len2 = GoogleTVRequest.fixMessage(Integer.toHexString(length));
985 String reply = "080210c801c202" + len1 + "0a" + len2 + this.pinHash;
986 String replyLength = GoogleTVRequest.fixMessage(Integer.toHexString(reply.length() / 2));
987 String finalReply = replyLength + reply;
988 logger.trace("Message Intercepted: {}", message);
989 logger.trace("Message chagnged to: {}", finalReply);
991 } else if (message.startsWith("080210c801ca02", 2)) {
992 // intercept PIN hash and replace with valid shim hash
993 int length = this.shimPinHash.length() / 2;
994 String len1 = GoogleTVRequest.fixMessage(Integer.toHexString(length + 2));
995 String len2 = GoogleTVRequest.fixMessage(Integer.toHexString(length));
996 String reply = "080210c801ca02" + len1 + "0a" + len2 + this.shimPinHash;
997 String replyLength = GoogleTVRequest.fixMessage(Integer.toHexString(reply.length() / 2));
998 String finalReply = replyLength + reply;
999 logger.trace("Message Intercepted: {}", message);
1000 logger.trace("Message chagnged to: {}", finalReply);
1003 // don't intercept message
1008 private void shimReaderThreadJob() {
1009 logger.debug("Shim reader thread started {}", config.port);
1011 BufferedReader reader = this.shimReader;
1012 String thisShimMsg = "";
1015 while (!Thread.interrupted() && reader != null) {
1016 thisShimMsg = GoogleTVRequest.fixMessage(Integer.toHexString(reader.read()));
1017 if (HARD_DROP.equals(thisShimMsg)) {
1018 // Google has crashed the connection. Disconnect hard.
1023 length = Integer.parseInt(thisShimMsg.toString(), 16);
1024 logger.trace("shimReaderThreadJob message length {}", length);
1026 sbShimReader = new StringBuffer();
1027 sbShimReader.append(thisShimMsg.toString());
1029 sbShimReader.append(thisShimMsg.toString());
1032 if ((length > 0) && (current == length)) {
1033 logger.trace("Shim GoogleTV Message: {} {}", length, sbShimReader.toString());
1034 String thisCommand = interceptMessages(sbShimReader.toString());
1035 sendQueue.add(new GoogleTVCommand(GoogleTVRequest.encodeMessage(thisCommand)));
1039 } catch (InterruptedIOException e) {
1040 logger.debug("Interrupted while reading");
1041 setStatus(false, "Interrupted");
1042 } catch (IOException e) {
1043 logger.debug("I/O error while reading from stream: {}", e.getMessage());
1044 setStatus(false, "I/O Error");
1045 } catch (RuntimeException e) {
1046 logger.warn("Runtime exception in reader thread", e);
1047 setStatus(false, "Runtime exception");
1049 logger.debug("Shim message reader thread exiting {}", config.port);
1053 public void sendKeepAlive(String request) {
1054 String keepalive = GoogleTVRequest.encodeMessage(GoogleTVRequest.keepAlive(request));
1055 logger.debug("{} - Sending GoogleTV keepalive - request {} - response {}", handler.getThingID(), request,
1056 GoogleTVRequest.decodeMessage(keepalive));
1057 sendCommand(new GoogleTVCommand(keepalive));
1058 reconnectTaskSchedule();
1062 * Schedules the reconnect task keepAliveReconnectJob to execute in KEEPALIVE_TIMEOUT_SECONDS. This should
1064 * cancelled by calling reconnectTaskCancel() if a valid response is received from the bridge.
1066 private void reconnectTaskSchedule() {
1067 synchronized (keepAliveReconnectLock) {
1068 logger.trace("{} - Scheduling Reconnect Job for {}", handler.getThingID(), KEEPALIVE_TIMEOUT_SECONDS);
1069 keepAliveReconnectJob = scheduler.schedule(this::keepAliveTimeoutExpired, KEEPALIVE_TIMEOUT_SECONDS,
1075 * Cancels the reconnect task keepAliveReconnectJob.
1077 private void reconnectTaskCancel(boolean interrupt) {
1078 synchronized (keepAliveReconnectLock) {
1079 ScheduledFuture<?> keepAliveReconnectJob = this.keepAliveReconnectJob;
1080 if (keepAliveReconnectJob != null) {
1081 logger.trace("{} - Canceling GoogleTV scheduled reconnect job.", handler.getThingID());
1082 keepAliveReconnectJob.cancel(interrupt);
1083 this.keepAliveReconnectJob = null;
1089 * Executed by keepAliveReconnectJob if it is not cancelled by the LEAP message parser calling
1090 * validMessageReceived() which in turn calls reconnectTaskCancel().
1092 private void keepAliveTimeoutExpired() {
1093 logger.debug("{} - GoogleTV keepalive response timeout expired. Initiating reconnect.", handler.getThingID());
1097 public void validMessageReceived() {
1098 reconnectTaskCancel(true); // Got a good message, so cancel reconnect task.
1101 public void finishPinProcess() {
1102 GoogleTVConnectionManager connectionManager = this.connectionManager;
1103 GoogleTVConnectionManager childConnectionManager = this.childConnectionManager;
1104 if ((connectionManager != null) && (config.mode.equals(PIN_MODE)) && (!config.shim)) {
1106 connectionManager.finishPinProcess();
1107 } else if ((childConnectionManager != null) && (config.mode.equals(DEFAULT_MODE)) && (!config.shim)) {
1108 childConnectionManager.dispose();
1113 public void sendCommand(GoogleTVCommand command) {
1114 if ((!config.shim) && (!command.isEmpty())) {
1115 int length = command.toString().length();
1116 String hexLength = GoogleTVRequest.encodeMessage(GoogleTVRequest.fixMessage(Integer.toHexString(length)));
1117 String message = hexLength + command.toString();
1118 GoogleTVCommand lenCommand = new GoogleTVCommand(message);
1119 sendQueue.add(lenCommand);
1123 public void sendShim(GoogleTVCommand command) {
1124 if (!command.isEmpty()) {
1125 shimQueue.add(command);
1129 public void handleCommand(ChannelUID channelUID, Command command) {
1130 logger.debug("{} - Command received: {}", handler.getThingID(), channelUID.getId());
1132 if (CHANNEL_KEYPRESS.equals(channelUID.getId())) {
1133 if (command instanceof StringType) {
1134 if (command.toString().length() == 5) {
1135 // Account for KEY_(ASCII Character)
1136 String keyPress = "aa01071a0512031a01"
1137 + GoogleTVRequest.decodeMessage(new String("" + command.toString().charAt(4)));
1138 sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage(keyPress)));
1142 String message = "";
1144 String shortCommand = command.toString();
1145 if (command.toString().endsWith("_PRESS")) {
1147 shortCommand = "KEY_" + command.toString().split("_")[1];
1148 } else if (command.toString().endsWith("_RELEASE")) {
1150 shortCommand = "KEY_" + command.toString().split("_")[1];
1155 switch (shortCommand) {
1157 message = "52040813" + suffix;
1160 message = "52040814" + suffix;
1163 message = "52040816" + suffix;
1166 message = "52040815" + suffix;
1169 message = "52040817" + suffix;
1172 message = "52040803" + suffix;
1175 message = "52040804" + suffix;
1178 message = "52040852" + suffix;
1181 message = "5204087E" + suffix;
1184 message = "5204087F" + suffix;
1186 case "KEY_PLAYPAUSE":
1187 message = "52040855" + suffix;
1190 message = "52040856" + suffix;
1193 message = "52040857" + suffix;
1195 case "KEY_PREVIOUS":
1196 message = "52040858" + suffix;
1199 message = "52040859" + suffix;
1202 message = "5204085A" + suffix;
1205 message = "5204081a" + suffix;
1208 message = "52040818" + suffix;
1211 message = "52040819" + suffix;
1214 message = "5204085b" + suffix;
1217 logger.debug("Unknown Key {}", command);
1220 sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage(message)));
1222 } else if (CHANNEL_KEYCODE.equals(channelUID.getId())) {
1223 if (command instanceof StringType) {
1224 String shortCommand = command.toString().split("_")[0];
1225 int commandInt = Integer.parseInt(shortCommand, 10);
1227 if (commandInt > 255) {
1230 } else if (commandInt > 127) {
1234 String key = Integer.toHexString(commandInt) + suffix;
1236 if ((key.length() % 2) == 1) {
1242 if (command.toString().endsWith("_PRESS")) {
1244 } else if (command.toString().endsWith("_RELEASE")) {
1250 String length = "0" + (key.length() / 2);
1251 String message = "52" + length + key;
1253 logger.trace("Sending KEYCODE {} as {}", key, message);
1254 sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage(message)));
1257 } else if (CHANNEL_PINCODE.equals(channelUID.getId())) {
1258 if (command instanceof StringType) {
1260 Certificate[] shimClientChain = this.shimClientChain;
1261 Certificate[] shimServerChain = this.shimServerChain;
1262 Certificate[] shimClientLocalChain = this.shimClientLocalChain;
1263 if (config.mode.equals(DEFAULT_MODE)) {
1264 if ((!isLoggedIn) && (command.toString().equals("REQUEST"))
1265 && (childConnectionManager == null)) {
1266 setStatus(false, "User Forced PIN Process");
1267 logger.debug("{} - User Forced PIN Process", handler.getThingID());
1269 startChildConnectionManager(config.port + 1, PIN_MODE);
1271 Thread.sleep(PIN_DELAY);
1272 } catch (InterruptedException e) {
1273 logger.trace("InterruptedException", e);
1276 GoogleTVConnectionManager childConnectionManager = this.childConnectionManager;
1277 if (childConnectionManager != null) {
1278 childConnectionManager.handleCommand(channelUID, command);
1280 logger.debug("{} - Child Connection Manager unavailable.", handler.getThingID());
1282 } else if ((config.mode.equals(PIN_MODE)) && (!config.shim)) {
1284 if (command.toString().equals("REQUEST")) {
1285 sendCommand(new GoogleTVCommand(
1286 GoogleTVRequest.encodeMessage(GoogleTVRequest.pinRequest(command.toString()))));
1287 } else if (shimServerChain != null) {
1288 this.pinHash = GoogleTVUtils.validatePIN(command.toString(), androidtvPKI.getCert(),
1289 shimServerChain[0]);
1290 sendCommand(new GoogleTVCommand(
1291 GoogleTVRequest.encodeMessage(GoogleTVRequest.pinRequest(this.pinHash))));
1294 } else if ((config.mode.equals(PIN_MODE)) && (config.shim)) {
1295 if ((shimClientChain != null) && (shimServerChain != null) && (shimClientLocalChain != null)) {
1296 this.pinHash = GoogleTVUtils.validatePIN(command.toString(), androidtvPKI.getCert(),
1297 shimServerChain[0]);
1298 this.shimPinHash = GoogleTVUtils.validatePIN(command.toString(), shimClientChain[0],
1299 shimClientLocalChain[0]);
1302 } catch (CertificateException e) {
1303 logger.trace("PIN CertificateException", e);
1306 } else if (CHANNEL_POWER.equals(channelUID.getId())) {
1307 if (command instanceof OnOffType) {
1308 if ((power && command.equals(OnOffType.OFF)) || (!power && command.equals(OnOffType.ON))) {
1309 sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage("5204081a1003")));
1311 } else if (command instanceof StringType) {
1312 if ((power && command.toString().equals("OFF")) || (!power && command.toString().equals("ON"))) {
1313 sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage("5204081a1003")));
1316 } else if (CHANNEL_MUTE.equals(channelUID.getId())) {
1317 if (command instanceof OnOffType) {
1318 if ((volMute && command.equals(OnOffType.OFF)) || (!volMute && command.equals(OnOffType.ON))) {
1319 sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage("5204085b1003")));
1322 } else if (CHANNEL_DEBUG.equals(channelUID.getId())) {
1323 if (command instanceof StringType) {
1324 if (command.toString().startsWith("RAW", 9)) {
1325 String newCommand = command.toString().substring(13);
1326 String message = GoogleTVRequest.encodeMessage(newCommand);
1327 if (logger.isTraceEnabled()) {
1328 logger.trace("Raw Message Decodes as: {}", GoogleTVRequest.decodeMessage(message));
1330 sendCommand(new GoogleTVCommand(message));
1331 } else if (command.toString().startsWith("MSG", 9)) {
1332 String newCommand = command.toString().substring(13);
1333 messageParser.handleMessage(newCommand);
1336 } else if (CHANNEL_KEYBOARD.equals(channelUID.getId())) {
1337 if (command instanceof StringType) {
1338 String keyPress = "";
1339 for (int i = 0; i < command.toString().length(); i++) {
1340 keyPress = "aa01071a0512031a01"
1341 + GoogleTVRequest.decodeMessage(String.valueOf(command.toString().charAt(i)));
1342 sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage(keyPress)));
1345 } else if (CHANNEL_PLAYER.equals(channelUID.getId())) {
1346 String message = "";
1347 if (command == PlayPauseType.PAUSE || command == OnOffType.OFF) {
1348 message = "5204087F1003";
1349 } else if (command == PlayPauseType.PLAY || command == OnOffType.ON) {
1350 message = "5204087E1003";
1351 } else if (command == NextPreviousType.NEXT) {
1352 message = "520408571003";
1353 } else if (command == NextPreviousType.PREVIOUS) {
1354 message = "520408581003";
1355 } else if (command == RewindFastforwardType.FASTFORWARD) {
1356 message = "5204085A1003";
1357 } else if (command == RewindFastforwardType.REWIND) {
1358 message = "520408591003";
1360 sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage(message)));
1364 public void dispose() {
1365 this.disposing = true;
1367 Future<?> asyncInitializeTask = this.asyncInitializeTask;
1368 if (asyncInitializeTask != null) {
1369 asyncInitializeTask.cancel(true); // Interrupt async init task if it isn't done yet
1371 Future<?> shimAsyncInitializeTask = this.shimAsyncInitializeTask;
1372 if (shimAsyncInitializeTask != null) {
1373 shimAsyncInitializeTask.cancel(true); // Interrupt async init task if it isn't done yet
1375 ScheduledFuture<?> deviceHealthJob = this.deviceHealthJob;
1376 if (deviceHealthJob != null) {
1377 deviceHealthJob.cancel(true);
1379 GoogleTVConnectionManager childConnectionManager = this.childConnectionManager;
1380 if (childConnectionManager != null) {
1381 childConnectionManager.dispose();