2 * Copyright (c) 2010-2024 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
14 * This file is based on:
19 * Copyright (c) 2014 LG Electronics.
20 * Created by Hyun Kook Khang on 19 Jan 2014
22 * Licensed under the Apache License, Version 2.0 (the "License");
23 * you may not use this file except in compliance with the License.
24 * You may obtain a copy of the License at
26 * http://www.apache.org/licenses/LICENSE-2.0
28 * Unless required by applicable law or agreed to in writing, software
29 * distributed under the License is distributed on an "AS IS" BASIS,
30 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
31 * See the License for the specific language governing permissions and
32 * limitations under the License.
34 package org.openhab.binding.lgwebos.internal.handler;
36 import static org.openhab.binding.lgwebos.internal.LGWebOSBindingConstants.*;
38 import java.io.IOException;
39 import java.net.ConnectException;
40 import java.net.SocketTimeoutException;
42 import java.net.URISyntaxException;
43 import java.time.Instant;
44 import java.util.ArrayList;
45 import java.util.HashMap;
46 import java.util.List;
47 import java.util.Locale;
49 import java.util.Map.Entry;
50 import java.util.Optional;
51 import java.util.concurrent.Future;
52 import java.util.concurrent.ScheduledExecutorService;
53 import java.util.concurrent.ScheduledFuture;
54 import java.util.concurrent.TimeUnit;
55 import java.util.function.Consumer;
57 import org.eclipse.jdt.annotation.NonNullByDefault;
58 import org.eclipse.jdt.annotation.Nullable;
59 import org.eclipse.jetty.websocket.api.Session;
60 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
61 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
62 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
63 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
64 import org.eclipse.jetty.websocket.api.annotations.WebSocket;
65 import org.eclipse.jetty.websocket.client.WebSocketClient;
66 import org.openhab.binding.lgwebos.internal.handler.LGWebOSTVMouseSocket.WebOSTVMouseSocketListener;
67 import org.openhab.binding.lgwebos.internal.handler.command.ServiceCommand;
68 import org.openhab.binding.lgwebos.internal.handler.command.ServiceSubscription;
69 import org.openhab.binding.lgwebos.internal.handler.core.AppInfo;
70 import org.openhab.binding.lgwebos.internal.handler.core.ChannelInfo;
71 import org.openhab.binding.lgwebos.internal.handler.core.CommandConfirmation;
72 import org.openhab.binding.lgwebos.internal.handler.core.LaunchSession;
73 import org.openhab.binding.lgwebos.internal.handler.core.LaunchSession.LaunchSessionType;
74 import org.openhab.binding.lgwebos.internal.handler.core.Response;
75 import org.openhab.binding.lgwebos.internal.handler.core.ResponseListener;
76 import org.openhab.binding.lgwebos.internal.handler.core.TextInputStatusInfo;
77 import org.slf4j.Logger;
78 import org.slf4j.LoggerFactory;
80 import com.google.gson.Gson;
81 import com.google.gson.GsonBuilder;
82 import com.google.gson.JsonElement;
83 import com.google.gson.JsonObject;
84 import com.google.gson.reflect.TypeToken;
87 * WebSocket to handle the communication with WebOS device.
89 * @author Hyun Kook Khang - Initial contribution
90 * @author Sebastian Prehn - Web Socket implementation and adoption for openHAB
94 public class LGWebOSTVSocket {
96 private static final String FOREGROUND_APP = "ssap://com.webos.applicationManager/getForegroundAppInfo";
97 // private static final String APP_STATUS = "ssap://com.webos.service.appstatus/getAppStatus";
98 // private static final String APP_STATE = "ssap://system.launcher/getAppState";
99 private static final String VOLUME = "ssap://audio/getVolume";
100 private static final String MUTE = "ssap://audio/getMute";
101 // private static final String VOLUME_STATUS = "ssap://audio/getStatus";
102 private static final String CHANNEL_LIST = "ssap://tv/getChannelList";
103 private static final String CHANNEL = "ssap://tv/getCurrentChannel";
104 // private static final String PROGRAM = "ssap://tv/getChannelProgramInfo";
105 // private static final String CURRENT_PROGRAM = "ssap://tv/getChannelCurrentProgramInfo";
106 // private static final String THREED_STATUS = "ssap://com.webos.service.tv.display/get3DStatus";
108 private static final int DISCONNECTING_DELAY_SECONDS = 2;
110 private static final Gson GSON = new GsonBuilder().create();
112 private final Logger logger = LoggerFactory.getLogger(LGWebOSTVSocket.class);
114 private final ConfigProvider config;
115 private final WebSocketClient client;
116 private final URI destUri;
117 private final LGWebOSTVKeyboardInput keyboardInput;
118 private final ScheduledExecutorService scheduler;
119 private final Protocol protocol;
129 private enum Protocol {
130 WEB_SOCKET("ws", DEFAULT_WS_PORT),
131 WEB_SOCKET_SECURE("wss", DEFAULT_WSS_PORT);
133 private Protocol(String name, int port) {
142 private State state = State.DISCONNECTED;
144 private @Nullable Session session;
145 private @Nullable Future<?> sessionFuture;
146 private @Nullable WebOSTVSocketListener listener;
149 * Requests to which we are awaiting response.
151 private HashMap<Integer, ServiceCommand<?>> requests = new HashMap<>();
153 private int nextRequestId = 0;
155 private @Nullable ScheduledFuture<?> disconnectingJob;
157 public LGWebOSTVSocket(WebSocketClient client, ConfigProvider config, String host, boolean useTLS,
158 ScheduledExecutorService scheduler) {
159 this.config = config;
160 this.client = client;
161 this.keyboardInput = new LGWebOSTVKeyboardInput(this);
162 this.protocol = useTLS ? Protocol.WEB_SOCKET_SECURE : Protocol.WEB_SOCKET;
165 this.destUri = new URI(protocol.name + "://" + host + ":" + protocol.port);
166 } catch (URISyntaxException e) {
167 throw new IllegalArgumentException("IP address or hostname provided is invalid: " + host);
170 this.scheduler = scheduler;
173 public State getState() {
177 private void setState(State state) {
178 logger.debug("setState new {} - current {}", state, this.state);
179 State oldState = this.state;
180 if (oldState != state) {
182 Optional.ofNullable(this.listener).ifPresent(l -> l.onStateChanged(this.state));
186 public void setListener(@Nullable WebOSTVSocketListener listener) {
187 this.listener = listener;
190 public void clearRequests() {
194 public void connect() {
196 sessionFuture = this.client.connect(this, this.destUri);
197 logger.debug("Connecting to: {}", this.destUri);
198 } catch (IOException e) {
199 logger.debug("Unable to connect.", e);
203 public void disconnect() {
204 Optional.ofNullable(this.session).ifPresent(s -> s.close());
205 Future<?> future = sessionFuture;
206 if (future != null && !future.isDone()) {
209 stopDisconnectingJob();
210 setState(State.DISCONNECTED);
213 private void disconnecting() {
214 logger.debug("disconnecting");
215 if (state == State.REGISTERED) {
216 setState(State.DISCONNECTING);
220 private void scheduleDisconectingJob() {
221 ScheduledFuture<?> job = disconnectingJob;
222 if (job == null || job.isCancelled()) {
223 logger.debug("Schedule disconecting job");
224 disconnectingJob = scheduler.schedule(this::disconnecting, DISCONNECTING_DELAY_SECONDS, TimeUnit.SECONDS);
228 private void stopDisconnectingJob() {
229 ScheduledFuture<?> job = disconnectingJob;
230 if (job != null && !job.isCancelled()) {
231 logger.debug("Stop disconnecting job");
234 disconnectingJob = null;
238 * WebSocket Callbacks
242 public void onConnect(Session session) {
243 logger.debug("WebSocket Connected to: {}", session.getRemoteAddress().getAddress());
244 this.session = session;
249 public void onError(Throwable cause) {
250 logger.trace("Connection Error", cause);
251 if (cause instanceof SocketTimeoutException && "Connect Timeout".equals(cause.getMessage())) {
252 // this is expected during connection attempts while TV is off
253 setState(State.DISCONNECTED);
256 if (cause instanceof ConnectException && "Connection refused".equals(cause.getMessage())) {
257 // this is expected during TV startup or shutdown
261 String message = cause.getMessage();
262 Optional.ofNullable(this.listener).ifPresent(l -> l.onError(message != null ? message : ""));
266 public void onClose(int statusCode, String reason) {
267 logger.debug("WebSocket Closed - Code: {}, Reason: {}", statusCode, reason);
268 this.requests.clear();
270 setState(State.DISCONNECTED);
274 * WebOS WebSocket API specific Communication
277 setState(State.CONNECTING);
279 JsonObject packet = new JsonObject();
280 packet.addProperty("id", nextRequestId());
281 packet.addProperty("type", "hello");
283 JsonObject payload = new JsonObject();
284 payload.addProperty("appId", "org.openhab");
285 payload.addProperty("appName", "openHAB");
286 payload.addProperty("appRegion", Locale.getDefault().getDisplayCountry());
287 packet.add("payload", payload);
288 // the hello response will not contain id, therefore not registering in requests
292 void sendRegister() {
293 setState(State.REGISTERING);
295 JsonObject packet = new JsonObject();
296 int id = nextRequestId();
297 packet.addProperty("id", id);
298 packet.addProperty("type", "register");
300 JsonObject manifest = new JsonObject();
301 manifest.addProperty("manifestVersion", 1);
303 String[] permissions = { "LAUNCH", "LAUNCH_WEBAPP", "APP_TO_APP", "CONTROL_AUDIO",
304 "CONTROL_INPUT_MEDIA_PLAYBACK", "CONTROL_POWER", "READ_INSTALLED_APPS", "CONTROL_DISPLAY",
305 "CONTROL_INPUT_JOYSTICK", "CONTROL_INPUT_MEDIA_RECORDING", "CONTROL_INPUT_TV", "READ_INPUT_DEVICE_LIST",
306 "READ_NETWORK_STATE", "READ_TV_CHANNEL_LIST", "WRITE_NOTIFICATION_TOAST", "CONTROL_INPUT_TEXT",
307 "CONTROL_MOUSE_AND_KEYBOARD", "READ_CURRENT_CHANNEL", "READ_RUNNING_APPS" };
309 manifest.add("permissions", GSON.toJsonTree(permissions));
311 JsonObject payload = new JsonObject();
312 String key = config.getKey();
313 if (!key.isEmpty()) {
314 payload.addProperty("client-key", key);
316 payload.addProperty("pairingType", "PROMPT"); // PIN, COMBINED
317 payload.add("manifest", manifest);
318 packet.add("payload", payload);
319 ResponseListener<JsonObject> dummyListener = new ResponseListener<>() {
322 public void onSuccess(@Nullable JsonObject payload) {
323 // Noting to do here. TV shows PROMPT dialog.
324 // Waiting for message of type error or registered
328 public void onError(String message) {
329 logger.debug("Registration failed with message: {}", message);
334 this.requests.put(id, new ServiceSubscription<>("dummy", payload, x -> x, dummyListener));
335 sendMessage(packet, !key.isEmpty());
338 private int nextRequestId() {
341 requestId = nextRequestId++;
342 } while (requests.containsKey(requestId));
346 public void sendCommand(ServiceCommand<?> command) {
349 int requestId = nextRequestId();
350 requests.put(requestId, command);
351 JsonObject packet = new JsonObject();
352 packet.addProperty("type", command.getType());
353 packet.addProperty("id", requestId);
354 packet.addProperty("uri", command.getTarget());
355 JsonElement payload = command.getPayload();
356 if (payload != null) {
357 packet.add("payload", payload);
359 this.sendMessage(packet);
366 logger.debug("Skipping {} command {} for {} in state {}", command.getType(), command,
367 command.getTarget(), state);
372 public void unsubscribe(ServiceSubscription<?> subscription) {
373 Optional<Entry<Integer, ServiceCommand<?>>> entry = this.requests.entrySet().stream()
374 .filter(e -> e.getValue().equals(subscription)).findFirst();
375 if (entry.isPresent()) {
376 int requestId = entry.get().getKey();
377 this.requests.remove(requestId);
378 JsonObject packet = new JsonObject();
379 packet.addProperty("type", "unsubscribe");
380 packet.addProperty("id", requestId);
385 private void sendMessage(JsonObject json) {
386 sendMessage(json, false);
389 private void sendMessage(JsonObject json, boolean checkKey) {
390 String msg = GSON.toJson(json);
391 Session s = this.session;
394 if (logger.isTraceEnabled()) {
395 logger.trace("Message [out]: {}", checkKey ? GSON.toJson(maskKeyInJson(json)) : msg);
397 s.getRemote().sendString(msg);
399 logger.warn("No Connection to TV, skipping [out]: {}",
400 checkKey ? GSON.toJson(maskKeyInJson(json)) : msg);
402 } catch (IOException e) {
403 logger.warn("Unable to send message.", e);
407 private JsonObject maskKeyInJson(JsonObject json) {
408 if (json.has("payload") && json.getAsJsonObject("payload").has("client-key")) {
409 JsonObject jsonCopy = json.deepCopy();
410 JsonObject payload = jsonCopy.getAsJsonObject("payload");
411 payload.remove("client-key");
412 payload.addProperty("client-key", "***");
419 public void onMessage(String message) {
420 Response response = GSON.fromJson(message, Response.class);
421 JsonElement payload = response.getPayload();
422 JsonObject jsonPayload = payload == null ? null : payload.getAsJsonObject();
423 String messageToLog = (jsonPayload != null && jsonPayload.has("client-key")) ? "***" : message;
424 logger.trace("Message [in]: {}", messageToLog);
425 ServiceCommand<?> request = null;
427 if (response.getId() != null) {
428 request = requests.get(response.getId());
429 if (request == null) {
430 logger.warn("Received a response with id {}, for which no request was found. This should not happen.",
433 // for subscriptions we want to keep the original
434 // message, so that we have a reference to the response listener
435 if (!(request instanceof ServiceSubscription<?>)) {
436 requests.remove(response.getId());
441 switch (response.getType()) {
443 if (request == null) {
444 logger.debug("No matching request found for response message: {}", messageToLog);
447 if (payload == null) {
448 logger.debug("No payload in response message: {}", messageToLog);
452 request.processResponse(jsonPayload);
453 } catch (RuntimeException ex) {
454 // An uncaught runtime exception in @OnWebSocketMessage annotated method will cause the web socket
455 // implementation to call @OnWebSocketError callback in which we would reset the connection.
456 // Users have the ability to create miss-configurations in which IllegalArgumentException could be
458 logger.warn("Error while processing message: {} - in response to request: {} - Error Message: {}",
459 messageToLog, request, ex.getMessage());
463 logger.debug("Error: {}", messageToLog);
465 if (request == null) {
466 logger.warn("No matching request found for error message: {}", messageToLog);
469 if (payload == null) {
470 logger.warn("No payload in error message: {}", messageToLog);
474 request.processError(response.getError());
475 } catch (RuntimeException ex) {
476 // An uncaught runtime exception in @OnWebSocketMessage annotated method will cause the web socket
477 // implementation to call @OnWebSocketError callback in which we would reset the connection.
478 // Users have the ability to create miss-configurations in which IllegalArgumentException could be
480 logger.warn("Error while processing error: {} - in response to request: {} - Error Message: {}",
481 messageToLog, request, ex.getMessage());
485 if (state != State.CONNECTING) {
486 logger.debug("Skipping response {}, not in CONNECTING state, state was {}", messageToLog, state);
489 if (jsonPayload == null) {
490 logger.warn("No payload in error message: {}", messageToLog);
493 Map<String, String> map = new HashMap<>();
494 map.put(PROPERTY_DEVICE_OS, jsonPayload.get("deviceOS").getAsString());
495 map.put(PROPERTY_DEVICE_OS_VERSION, jsonPayload.get("deviceOSVersion").getAsString());
496 map.put(PROPERTY_DEVICE_OS_RELEASE_VERSION, jsonPayload.get("deviceOSReleaseVersion").getAsString());
497 map.put(PROPERTY_LAST_CONNECTED, Instant.now().toString());
498 config.storeProperties(map);
502 if (state != State.REGISTERING) {
503 logger.debug("Skipping response {}, not in REGISTERING state, state was {}", messageToLog, state);
506 if (jsonPayload == null) {
507 logger.warn("No payload in registered message: {}", messageToLog);
510 this.requests.remove(response.getId());
511 config.storeKey(jsonPayload.get("client-key").getAsString());
512 setState(State.REGISTERED);
517 public interface WebOSTVSocketListener {
519 public void onStateChanged(State state);
521 public void onError(String errorMessage);
524 public ServiceSubscription<Boolean> subscribeMute(ResponseListener<Boolean> listener) {
525 ServiceSubscription<Boolean> request = new ServiceSubscription<>(MUTE, null,
526 (jsonObj) -> jsonObj.get("mute").getAsBoolean(), listener);
527 sendCommand(request);
531 public ServiceCommand<Boolean> getMute(ResponseListener<Boolean> listener) {
532 ServiceCommand<Boolean> request = new ServiceCommand<>(MUTE, null,
533 (jsonObj) -> jsonObj.get("mute").getAsBoolean(), listener);
534 sendCommand(request);
538 private Float volumeFromResponse(JsonObject jsonObj) {
539 JsonObject parent = jsonObj.has("volumeStatus") ? jsonObj.getAsJsonObject("volumeStatus") : jsonObj;
540 return parent.get("volume").getAsInt() >= 0 ? (float) (parent.get("volume").getAsInt() / 100.0) : Float.NaN;
543 public ServiceSubscription<Float> subscribeVolume(ResponseListener<Float> listener) {
544 ServiceSubscription<Float> request = new ServiceSubscription<>(VOLUME, null, this::volumeFromResponse,
546 sendCommand(request);
550 public ServiceCommand<Float> getVolume(ResponseListener<Float> listener) {
551 ServiceCommand<Float> request = new ServiceCommand<>(VOLUME, null, this::volumeFromResponse, listener);
552 sendCommand(request);
556 public void setMute(boolean isMute, ResponseListener<CommandConfirmation> listener) {
557 String uri = "ssap://audio/setMute";
558 JsonObject payload = new JsonObject();
559 payload.addProperty("mute", isMute);
561 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, payload,
562 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
563 sendCommand(request);
566 public void setVolume(float volume, ResponseListener<CommandConfirmation> listener) {
567 String uri = "ssap://audio/setVolume";
568 JsonObject payload = new JsonObject();
569 int intVolume = Math.round(volume * 100.0f);
570 payload.addProperty("volume", intVolume);
571 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, payload,
572 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
573 sendCommand(request);
576 public void volumeUp(ResponseListener<CommandConfirmation> listener) {
577 String uri = "ssap://audio/volumeUp";
578 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
579 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
580 sendCommand(request);
583 public void volumeDown(ResponseListener<CommandConfirmation> listener) {
584 String uri = "ssap://audio/volumeDown";
585 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
586 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
587 sendCommand(request);
590 public ServiceSubscription<ChannelInfo> subscribeCurrentChannel(ResponseListener<ChannelInfo> listener) {
591 ServiceSubscription<ChannelInfo> request = new ServiceSubscription<>(CHANNEL, null,
592 jsonObj -> GSON.fromJson(jsonObj, ChannelInfo.class), listener);
593 sendCommand(request);
598 public ServiceCommand<ChannelInfo> getCurrentChannel(ResponseListener<ChannelInfo> listener) {
599 ServiceCommand<ChannelInfo> request = new ServiceCommand<>(CHANNEL, null,
600 jsonObj -> GSON.fromJson(jsonObj, ChannelInfo.class), listener);
601 sendCommand(request);
606 public void setChannel(ChannelInfo channelInfo, ResponseListener<CommandConfirmation> listener) {
607 JsonObject payload = new JsonObject();
608 if (channelInfo.getId() != null) {
609 payload.addProperty("channelId", channelInfo.getId());
611 if (channelInfo.getChannelNumber() != null) {
612 payload.addProperty("channelNumber", channelInfo.getChannelNumber());
614 setChannel(payload, listener);
617 private void setChannel(JsonObject payload, ResponseListener<CommandConfirmation> listener) {
618 String uri = "ssap://tv/openChannel";
619 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, payload,
620 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
621 sendCommand(request);
624 public void channelUp(ResponseListener<CommandConfirmation> listener) {
625 String uri = "ssap://tv/channelUp";
626 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
627 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
628 sendCommand(request);
631 public void channelDown(ResponseListener<CommandConfirmation> listener) {
632 String uri = "ssap://tv/channelDown";
633 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
634 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
635 sendCommand(request);
638 public void getChannelList(ResponseListener<List<ChannelInfo>> listener) {
639 ServiceCommand<List<ChannelInfo>> request = new ServiceCommand<>(CHANNEL_LIST, null,
640 jsonObj -> GSON.fromJson(jsonObj.get("channelList"), new TypeToken<ArrayList<ChannelInfo>>() {
641 }.getType()), listener);
642 sendCommand(request);
647 public void showToast(String message, ResponseListener<CommandConfirmation> listener) {
648 showToast(message, null, null, listener);
651 public void showToast(String message, @Nullable String iconData, @Nullable String iconExtension,
652 ResponseListener<CommandConfirmation> listener) {
653 JsonObject payload = new JsonObject();
654 payload.addProperty("message", message);
656 if (iconData != null && iconExtension != null) {
657 payload.addProperty("iconData", iconData);
658 payload.addProperty("iconExtension", iconExtension);
661 sendToast(payload, listener);
664 private void sendToast(JsonObject payload, ResponseListener<CommandConfirmation> listener) {
665 String uri = "ssap://system.notifications/createToast";
666 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, payload,
667 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
668 sendCommand(request);
672 public void powerOff(ResponseListener<CommandConfirmation> listener) {
673 String uri = "ssap://system/turnOff";
675 ResponseListener<CommandConfirmation> interceptor = new ResponseListener<>() {
678 public void onSuccess(CommandConfirmation confirmation) {
679 if (confirmation.getReturnValue()) {
682 listener.onSuccess(confirmation);
686 public void onError(String message) {
687 listener.onError(message);
690 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
691 x -> GSON.fromJson(x, CommandConfirmation.class), interceptor);
692 sendCommand(request);
696 public void play(ResponseListener<CommandConfirmation> listener) {
697 String uri = "ssap://media.controls/play";
698 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
699 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
700 sendCommand(request);
703 public void pause(ResponseListener<CommandConfirmation> listener) {
704 String uri = "ssap://media.controls/pause";
705 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
706 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
707 sendCommand(request);
710 public void stop(ResponseListener<CommandConfirmation> listener) {
711 String uri = "ssap://media.controls/stop";
712 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
713 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
714 sendCommand(request);
717 public void rewind(ResponseListener<CommandConfirmation> listener) {
718 String uri = "ssap://media.controls/rewind";
719 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
720 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
721 sendCommand(request);
724 public void fastForward(ResponseListener<CommandConfirmation> listener) {
725 String uri = "ssap://media.controls/fastForward";
726 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
727 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
728 sendCommand(request);
733 public void getAppList(final ResponseListener<List<AppInfo>> listener) {
734 String uri = "ssap://com.webos.applicationManager/listApps";
736 ServiceCommand<List<AppInfo>> request = new ServiceCommand<>(uri, null,
737 jsonObj -> GSON.fromJson(jsonObj.get("apps"), new TypeToken<ArrayList<AppInfo>>() {
738 }.getType()), listener);
740 sendCommand(request);
743 public void launchAppWithInfo(AppInfo appInfo, ResponseListener<LaunchSession> listener) {
744 launchAppWithInfo(appInfo, null, listener);
747 public void launchAppWithInfo(final AppInfo appInfo, @Nullable JsonObject params,
748 final ResponseListener<LaunchSession> listener) {
749 String uri = "ssap://system.launcher/launch";
750 JsonObject payload = new JsonObject();
752 final String appId = appInfo.getId();
754 String contentId = null;
756 if (params != null) {
757 contentId = params.get("contentId").getAsString();
760 payload.addProperty("id", appId);
762 if (contentId != null) {
763 payload.addProperty("contentId", contentId);
766 if (params != null) {
767 payload.add("params", params);
770 ServiceCommand<LaunchSession> request = new ServiceCommand<>(uri, payload, obj -> {
771 LaunchSession launchSession = new LaunchSession();
772 launchSession.setService(this);
773 launchSession.setAppId(appId); // note that response uses id to mean appId
774 if (obj.has("sessionId")) {
775 launchSession.setSessionId(obj.get("sessionId").getAsString());
776 launchSession.setSessionType(LaunchSessionType.App);
778 launchSession.setSessionType(LaunchSessionType.Unknown);
780 return launchSession;
782 sendCommand(request);
785 public void launchBrowser(String url, final ResponseListener<LaunchSession> listener) {
786 String uri = "ssap://system.launcher/open";
787 JsonObject payload = new JsonObject();
788 payload.addProperty("target", url);
790 ServiceCommand<LaunchSession> request = new ServiceCommand<>(uri, payload, obj -> {
791 LaunchSession launchSession = new LaunchSession();
792 launchSession.setService(this);
793 launchSession.setAppId(obj.get("id").getAsString()); // note that response uses id to mean appId
794 if (obj.has("sessionId")) {
795 launchSession.setSessionId(obj.get("sessionId").getAsString());
796 launchSession.setSessionType(LaunchSessionType.App);
798 launchSession.setSessionType(LaunchSessionType.Unknown);
800 return launchSession;
802 sendCommand(request);
805 public void closeLaunchSession(LaunchSession launchSession, ResponseListener<CommandConfirmation> listener) {
806 LGWebOSTVSocket service = launchSession.getService();
808 switch (launchSession.getSessionType()) {
810 case ExternalInputPicker:
811 service.closeApp(launchSession, listener);
815 * If we want to extend support for MediaPlayer or WebAppLauncher at some point, this is how it was handeled
819 * if (service instanceof MediaPlayer) {
820 * ((MediaPlayer) service).closeMedia(launchSession, listener);
826 * if (service instanceof WebAppLauncher) {
827 * ((WebAppLauncher) service).closeWebApp(launchSession, listener);
833 listener.onError("This DeviceService does not know ho to close this LaunchSession");
838 public void closeApp(LaunchSession launchSession, ResponseListener<CommandConfirmation> listener) {
839 String uri = "ssap://system.launcher/close";
841 JsonObject payload = new JsonObject();
842 payload.addProperty("id", launchSession.getAppId());
843 payload.addProperty("sessionId", launchSession.getSessionId());
845 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, payload,
846 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
847 launchSession.getService().sendCommand(request);
850 public ServiceSubscription<AppInfo> subscribeRunningApp(ResponseListener<AppInfo> listener) {
851 ResponseListener<AppInfo> interceptor = new ResponseListener<>() {
854 public void onSuccess(AppInfo appInfo) {
855 if (appInfo.getId().isEmpty()) {
856 scheduleDisconectingJob();
858 stopDisconnectingJob();
859 if (state == State.DISCONNECTING) {
860 setState(State.REGISTERED);
863 listener.onSuccess(appInfo);
867 public void onError(String message) {
868 listener.onError(message);
871 ServiceSubscription<AppInfo> request = new ServiceSubscription<>(FOREGROUND_APP, null,
872 jsonObj -> GSON.fromJson(jsonObj, AppInfo.class), interceptor);
873 sendCommand(request);
877 public ServiceCommand<AppInfo> getRunningApp(ResponseListener<AppInfo> listener) {
878 ServiceCommand<AppInfo> request = new ServiceCommand<>(FOREGROUND_APP, null,
879 jsonObj -> GSON.fromJson(jsonObj, AppInfo.class), listener);
880 sendCommand(request);
886 public ServiceSubscription<TextInputStatusInfo> subscribeTextInputStatus(
887 ResponseListener<TextInputStatusInfo> listener) {
888 return keyboardInput.connect(listener);
891 public void sendText(String input) {
892 keyboardInput.sendText(input);
895 public void sendEnter() {
896 keyboardInput.sendEnter();
899 public void sendDelete() {
900 keyboardInput.sendDel();
905 public void executeMouse(Consumer<LGWebOSTVMouseSocket> onConnected) {
906 LGWebOSTVMouseSocket mouseSocket = new LGWebOSTVMouseSocket(this.client);
907 mouseSocket.setListener(new WebOSTVMouseSocketListener() {
910 public void onStateChanged(LGWebOSTVMouseSocket.State oldState, LGWebOSTVMouseSocket.State newState) {
913 onConnected.accept(mouseSocket);
914 mouseSocket.disconnect();
922 public void onError(String errorMessage) {
923 logger.debug("Error in communication with Mouse Socket: {}", errorMessage);
927 String uri = "ssap://com.webos.service.networkinput/getPointerInputSocket";
929 ResponseListener<JsonObject> listener = new ResponseListener<>() {
932 public void onSuccess(@Nullable JsonObject jsonObj) {
933 if (jsonObj != null) {
934 String socketPath = jsonObj.get("socketPath").getAsString();
935 if (protocol == Protocol.WEB_SOCKET) {
936 socketPath = socketPath
937 .replace(Protocol.WEB_SOCKET_SECURE.name + ":", Protocol.WEB_SOCKET.name + ":")
938 .replace(":" + Protocol.WEB_SOCKET_SECURE.port + "/",
939 ":" + Protocol.WEB_SOCKET.port + "/");
942 mouseSocket.connect(new URI(socketPath));
943 } catch (URISyntaxException e) {
944 logger.warn("Connect mouse error: {}", e.getMessage());
950 public void onError(String error) {
951 logger.warn("Connect mouse error: {}", error);
955 ServiceCommand<JsonObject> request = new ServiceCommand<>(uri, null, x -> x, listener);
956 sendCommand(request);
959 // Simulate Remote Control Button press
961 public void sendRCButton(String rcButton, ResponseListener<CommandConfirmation> listener) {
962 executeMouse(s -> s.button(rcButton));
965 public interface ConfigProvider {
966 void storeKey(String key);
968 void storeProperties(Map<String, String> properties);