2 * Copyright (c) 2010-2022 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;
128 private State state = State.DISCONNECTED;
130 private @Nullable Session session;
131 private @Nullable Future<?> sessionFuture;
132 private @Nullable WebOSTVSocketListener listener;
135 * Requests to which we are awaiting response.
137 private HashMap<Integer, ServiceCommand<?>> requests = new HashMap<>();
139 private int nextRequestId = 0;
141 private @Nullable ScheduledFuture<?> disconnectingJob;
143 public LGWebOSTVSocket(WebSocketClient client, ConfigProvider config, String host, int port,
144 ScheduledExecutorService scheduler) {
145 this.config = config;
146 this.client = client;
147 this.keyboardInput = new LGWebOSTVKeyboardInput(this);
150 this.destUri = new URI("ws://" + host + ":" + port);
151 } catch (URISyntaxException e) {
152 throw new IllegalArgumentException("IP address or hostname provided is invalid: " + host);
155 this.scheduler = scheduler;
158 public State getState() {
162 private void setState(State state) {
163 logger.debug("setState new {} - current {}", state, this.state);
164 State oldState = this.state;
165 if (oldState != state) {
167 Optional.ofNullable(this.listener).ifPresent(l -> l.onStateChanged(this.state));
171 public void setListener(@Nullable WebOSTVSocketListener listener) {
172 this.listener = listener;
175 public void clearRequests() {
179 public void connect() {
181 sessionFuture = this.client.connect(this, this.destUri);
182 logger.debug("Connecting to: {}", this.destUri);
183 } catch (IOException e) {
184 logger.debug("Unable to connect.", e);
188 public void disconnect() {
189 Optional.ofNullable(this.session).ifPresent(s -> s.close());
190 Future<?> future = sessionFuture;
191 if (future != null && !future.isDone()) {
194 stopDisconnectingJob();
195 setState(State.DISCONNECTED);
198 private void disconnecting() {
199 logger.debug("disconnecting");
200 if (state == State.REGISTERED) {
201 setState(State.DISCONNECTING);
205 private void scheduleDisconectingJob() {
206 ScheduledFuture<?> job = disconnectingJob;
207 if (job == null || job.isCancelled()) {
208 logger.debug("Schedule disconecting job");
209 disconnectingJob = scheduler.schedule(this::disconnecting, DISCONNECTING_DELAY_SECONDS, TimeUnit.SECONDS);
213 private void stopDisconnectingJob() {
214 ScheduledFuture<?> job = disconnectingJob;
215 if (job != null && !job.isCancelled()) {
216 logger.debug("Stop disconnecting job");
219 disconnectingJob = null;
223 * WebSocket Callbacks
227 public void onConnect(Session session) {
228 logger.debug("WebSocket Connected to: {}", session.getRemoteAddress().getAddress());
229 this.session = session;
234 public void onError(Throwable cause) {
235 logger.trace("Connection Error", cause);
236 if (cause instanceof SocketTimeoutException && "Connect Timeout".equals(cause.getMessage())) {
237 // this is expected during connection attempts while TV is off
238 setState(State.DISCONNECTED);
241 if (cause instanceof ConnectException && "Connection refused".equals(cause.getMessage())) {
242 // this is expected during TV startup or shutdown
246 String message = cause.getMessage();
247 Optional.ofNullable(this.listener).ifPresent(l -> l.onError(message != null ? message : ""));
251 public void onClose(int statusCode, String reason) {
252 logger.debug("WebSocket Closed - Code: {}, Reason: {}", statusCode, reason);
253 this.requests.clear();
255 setState(State.DISCONNECTED);
259 * WebOS WebSocket API specific Communication
262 setState(State.CONNECTING);
264 JsonObject packet = new JsonObject();
265 packet.addProperty("id", nextRequestId());
266 packet.addProperty("type", "hello");
268 JsonObject payload = new JsonObject();
269 payload.addProperty("appId", "org.openhab");
270 payload.addProperty("appName", "openHAB");
271 payload.addProperty("appRegion", Locale.getDefault().getDisplayCountry());
272 packet.add("payload", payload);
273 // the hello response will not contain id, therefore not registering in requests
277 void sendRegister() {
278 setState(State.REGISTERING);
280 JsonObject packet = new JsonObject();
281 int id = nextRequestId();
282 packet.addProperty("id", id);
283 packet.addProperty("type", "register");
285 JsonObject manifest = new JsonObject();
286 manifest.addProperty("manifestVersion", 1);
288 String[] permissions = { "LAUNCH", "LAUNCH_WEBAPP", "APP_TO_APP", "CONTROL_AUDIO",
289 "CONTROL_INPUT_MEDIA_PLAYBACK", "CONTROL_POWER", "READ_INSTALLED_APPS", "CONTROL_DISPLAY",
290 "CONTROL_INPUT_JOYSTICK", "CONTROL_INPUT_MEDIA_RECORDING", "CONTROL_INPUT_TV", "READ_INPUT_DEVICE_LIST",
291 "READ_NETWORK_STATE", "READ_TV_CHANNEL_LIST", "WRITE_NOTIFICATION_TOAST", "CONTROL_INPUT_TEXT",
292 "CONTROL_MOUSE_AND_KEYBOARD", "READ_CURRENT_CHANNEL", "READ_RUNNING_APPS" };
294 manifest.add("permissions", GSON.toJsonTree(permissions));
296 JsonObject payload = new JsonObject();
297 String key = config.getKey();
298 if (!key.isEmpty()) {
299 payload.addProperty("client-key", key);
301 payload.addProperty("pairingType", "PROMPT"); // PIN, COMBINED
302 payload.add("manifest", manifest);
303 packet.add("payload", payload);
304 ResponseListener<JsonObject> dummyListener = new ResponseListener<JsonObject>() {
307 public void onSuccess(@Nullable JsonObject payload) {
308 // Noting to do here. TV shows PROMPT dialog.
309 // Waiting for message of type error or registered
313 public void onError(String message) {
314 logger.debug("Registration failed with message: {}", message);
319 this.requests.put(id, new ServiceSubscription<>("dummy", payload, x -> x, dummyListener));
320 sendMessage(packet, !key.isEmpty());
323 private int nextRequestId() {
326 requestId = nextRequestId++;
327 } while (requests.containsKey(requestId));
331 public void sendCommand(ServiceCommand<?> command) {
334 int requestId = nextRequestId();
335 requests.put(requestId, command);
336 JsonObject packet = new JsonObject();
337 packet.addProperty("type", command.getType());
338 packet.addProperty("id", requestId);
339 packet.addProperty("uri", command.getTarget());
340 JsonElement payload = command.getPayload();
341 if (payload != null) {
342 packet.add("payload", payload);
344 this.sendMessage(packet);
351 logger.debug("Skipping {} command {} for {} in state {}", command.getType(), command,
352 command.getTarget(), state);
357 public void unsubscribe(ServiceSubscription<?> subscription) {
358 Optional<Entry<Integer, ServiceCommand<?>>> entry = this.requests.entrySet().stream()
359 .filter(e -> e.getValue().equals(subscription)).findFirst();
360 if (entry.isPresent()) {
361 int requestId = entry.get().getKey();
362 this.requests.remove(requestId);
363 JsonObject packet = new JsonObject();
364 packet.addProperty("type", "unsubscribe");
365 packet.addProperty("id", requestId);
370 private void sendMessage(JsonObject json) {
371 sendMessage(json, false);
374 private void sendMessage(JsonObject json, boolean checkKey) {
375 String msg = GSON.toJson(json);
376 Session s = this.session;
379 if (logger.isTraceEnabled()) {
380 logger.trace("Message [out]: {}", checkKey ? GSON.toJson(maskKeyInJson(json)) : msg);
382 s.getRemote().sendString(msg);
384 logger.warn("No Connection to TV, skipping [out]: {}",
385 checkKey ? GSON.toJson(maskKeyInJson(json)) : msg);
387 } catch (IOException e) {
388 logger.warn("Unable to send message.", e);
392 private JsonObject maskKeyInJson(JsonObject json) {
393 if (json.has("payload") && json.getAsJsonObject("payload").has("client-key")) {
394 JsonObject jsonCopy = json.deepCopy();
395 JsonObject payload = jsonCopy.getAsJsonObject("payload");
396 payload.remove("client-key");
397 payload.addProperty("client-key", "***");
404 public void onMessage(String message) {
405 Response response = GSON.fromJson(message, Response.class);
406 JsonElement payload = response.getPayload();
407 JsonObject jsonPayload = payload == null ? null : payload.getAsJsonObject();
408 String messageToLog = (jsonPayload != null && jsonPayload.has("client-key")) ? "***" : message;
409 logger.trace("Message [in]: {}", messageToLog);
410 ServiceCommand<?> request = null;
412 if (response.getId() != null) {
413 request = requests.get(response.getId());
414 if (request == null) {
415 logger.warn("Received a response with id {}, for which no request was found. This should not happen.",
418 // for subscriptions we want to keep the original
419 // message, so that we have a reference to the response listener
420 if (!(request instanceof ServiceSubscription<?>)) {
421 requests.remove(response.getId());
426 switch (response.getType()) {
428 if (request == null) {
429 logger.debug("No matching request found for response message: {}", messageToLog);
432 if (payload == null) {
433 logger.debug("No payload in response message: {}", messageToLog);
437 request.processResponse(jsonPayload);
438 } catch (RuntimeException ex) {
439 // An uncaught runtime exception in @OnWebSocketMessage annotated method will cause the web socket
440 // implementation to call @OnWebSocketError callback in which we would reset the connection.
441 // Users have the ability to create miss-configurations in which IllegalArgumentException could be
443 logger.warn("Error while processing message: {} - in response to request: {} - Error Message: {}",
444 messageToLog, request, ex.getMessage());
448 logger.debug("Error: {}", messageToLog);
450 if (request == null) {
451 logger.warn("No matching request found for error message: {}", messageToLog);
454 if (payload == null) {
455 logger.warn("No payload in error message: {}", messageToLog);
459 request.processError(response.getError());
460 } catch (RuntimeException ex) {
461 // An uncaught runtime exception in @OnWebSocketMessage annotated method will cause the web socket
462 // implementation to call @OnWebSocketError callback in which we would reset the connection.
463 // Users have the ability to create miss-configurations in which IllegalArgumentException could be
465 logger.warn("Error while processing error: {} - in response to request: {} - Error Message: {}",
466 messageToLog, request, ex.getMessage());
470 if (state != State.CONNECTING) {
471 logger.debug("Skipping response {}, not in CONNECTING state, state was {}", messageToLog, state);
474 if (jsonPayload == null) {
475 logger.warn("No payload in error message: {}", messageToLog);
478 Map<String, String> map = new HashMap<>();
479 map.put(PROPERTY_DEVICE_OS, jsonPayload.get("deviceOS").getAsString());
480 map.put(PROPERTY_DEVICE_OS_VERSION, jsonPayload.get("deviceOSVersion").getAsString());
481 map.put(PROPERTY_DEVICE_OS_RELEASE_VERSION, jsonPayload.get("deviceOSReleaseVersion").getAsString());
482 map.put(PROPERTY_LAST_CONNECTED, Instant.now().toString());
483 config.storeProperties(map);
487 if (state != State.REGISTERING) {
488 logger.debug("Skipping response {}, not in REGISTERING state, state was {}", messageToLog, state);
491 if (jsonPayload == null) {
492 logger.warn("No payload in registered message: {}", messageToLog);
495 this.requests.remove(response.getId());
496 config.storeKey(jsonPayload.get("client-key").getAsString());
497 setState(State.REGISTERED);
502 public interface WebOSTVSocketListener {
504 public void onStateChanged(State state);
506 public void onError(String errorMessage);
509 public ServiceSubscription<Boolean> subscribeMute(ResponseListener<Boolean> listener) {
510 ServiceSubscription<Boolean> request = new ServiceSubscription<>(MUTE, null,
511 (jsonObj) -> jsonObj.get("mute").getAsBoolean(), listener);
512 sendCommand(request);
516 public ServiceCommand<Boolean> getMute(ResponseListener<Boolean> listener) {
517 ServiceCommand<Boolean> request = new ServiceCommand<>(MUTE, null,
518 (jsonObj) -> jsonObj.get("mute").getAsBoolean(), listener);
519 sendCommand(request);
523 private Float volumeFromResponse(JsonObject jsonObj) {
524 JsonObject parent = jsonObj.has("volumeStatus") ? jsonObj.getAsJsonObject("volumeStatus") : jsonObj;
525 return parent.get("volume").getAsInt() >= 0 ? (float) (parent.get("volume").getAsInt() / 100.0) : Float.NaN;
528 public ServiceSubscription<Float> subscribeVolume(ResponseListener<Float> listener) {
529 ServiceSubscription<Float> request = new ServiceSubscription<>(VOLUME, null, this::volumeFromResponse,
531 sendCommand(request);
535 public ServiceCommand<Float> getVolume(ResponseListener<Float> listener) {
536 ServiceCommand<Float> request = new ServiceCommand<>(VOLUME, null, this::volumeFromResponse, listener);
537 sendCommand(request);
541 public void setMute(boolean isMute, ResponseListener<CommandConfirmation> listener) {
542 String uri = "ssap://audio/setMute";
543 JsonObject payload = new JsonObject();
544 payload.addProperty("mute", isMute);
546 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, payload,
547 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
548 sendCommand(request);
551 public void setVolume(float volume, ResponseListener<CommandConfirmation> listener) {
552 String uri = "ssap://audio/setVolume";
553 JsonObject payload = new JsonObject();
554 int intVolume = Math.round(volume * 100.0f);
555 payload.addProperty("volume", intVolume);
556 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, payload,
557 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
558 sendCommand(request);
561 public void volumeUp(ResponseListener<CommandConfirmation> listener) {
562 String uri = "ssap://audio/volumeUp";
563 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
564 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
565 sendCommand(request);
568 public void volumeDown(ResponseListener<CommandConfirmation> listener) {
569 String uri = "ssap://audio/volumeDown";
570 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
571 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
572 sendCommand(request);
575 public ServiceSubscription<ChannelInfo> subscribeCurrentChannel(ResponseListener<ChannelInfo> listener) {
576 ServiceSubscription<ChannelInfo> request = new ServiceSubscription<>(CHANNEL, null,
577 jsonObj -> GSON.fromJson(jsonObj, ChannelInfo.class), listener);
578 sendCommand(request);
583 public ServiceCommand<ChannelInfo> getCurrentChannel(ResponseListener<ChannelInfo> listener) {
584 ServiceCommand<ChannelInfo> request = new ServiceCommand<>(CHANNEL, null,
585 jsonObj -> GSON.fromJson(jsonObj, ChannelInfo.class), listener);
586 sendCommand(request);
591 public void setChannel(ChannelInfo channelInfo, ResponseListener<CommandConfirmation> listener) {
592 JsonObject payload = new JsonObject();
593 if (channelInfo.getId() != null) {
594 payload.addProperty("channelId", channelInfo.getId());
596 if (channelInfo.getChannelNumber() != null) {
597 payload.addProperty("channelNumber", channelInfo.getChannelNumber());
599 setChannel(payload, listener);
602 private void setChannel(JsonObject payload, ResponseListener<CommandConfirmation> listener) {
603 String uri = "ssap://tv/openChannel";
604 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, payload,
605 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
606 sendCommand(request);
609 public void channelUp(ResponseListener<CommandConfirmation> listener) {
610 String uri = "ssap://tv/channelUp";
611 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
612 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
613 sendCommand(request);
616 public void channelDown(ResponseListener<CommandConfirmation> listener) {
617 String uri = "ssap://tv/channelDown";
618 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
619 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
620 sendCommand(request);
623 public void getChannelList(ResponseListener<List<ChannelInfo>> listener) {
624 ServiceCommand<List<ChannelInfo>> request = new ServiceCommand<>(CHANNEL_LIST, null,
625 jsonObj -> GSON.fromJson(jsonObj.get("channelList"), new TypeToken<ArrayList<ChannelInfo>>() {
626 }.getType()), listener);
627 sendCommand(request);
632 public void showToast(String message, ResponseListener<CommandConfirmation> listener) {
633 showToast(message, null, null, listener);
636 public void showToast(String message, @Nullable String iconData, @Nullable String iconExtension,
637 ResponseListener<CommandConfirmation> listener) {
638 JsonObject payload = new JsonObject();
639 payload.addProperty("message", message);
641 if (iconData != null && iconExtension != null) {
642 payload.addProperty("iconData", iconData);
643 payload.addProperty("iconExtension", iconExtension);
646 sendToast(payload, listener);
649 private void sendToast(JsonObject payload, ResponseListener<CommandConfirmation> listener) {
650 String uri = "ssap://system.notifications/createToast";
651 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, payload,
652 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
653 sendCommand(request);
657 public void powerOff(ResponseListener<CommandConfirmation> listener) {
658 String uri = "ssap://system/turnOff";
660 ResponseListener<CommandConfirmation> interceptor = new ResponseListener<CommandConfirmation>() {
663 public void onSuccess(CommandConfirmation confirmation) {
664 if (confirmation.getReturnValue()) {
667 listener.onSuccess(confirmation);
671 public void onError(String message) {
672 listener.onError(message);
675 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
676 x -> GSON.fromJson(x, CommandConfirmation.class), interceptor);
677 sendCommand(request);
681 public void play(ResponseListener<CommandConfirmation> listener) {
682 String uri = "ssap://media.controls/play";
683 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
684 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
685 sendCommand(request);
688 public void pause(ResponseListener<CommandConfirmation> listener) {
689 String uri = "ssap://media.controls/pause";
690 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
691 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
692 sendCommand(request);
695 public void stop(ResponseListener<CommandConfirmation> listener) {
696 String uri = "ssap://media.controls/stop";
697 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
698 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
699 sendCommand(request);
702 public void rewind(ResponseListener<CommandConfirmation> listener) {
703 String uri = "ssap://media.controls/rewind";
704 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
705 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
706 sendCommand(request);
709 public void fastForward(ResponseListener<CommandConfirmation> listener) {
710 String uri = "ssap://media.controls/fastForward";
711 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
712 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
713 sendCommand(request);
718 public void getAppList(final ResponseListener<List<AppInfo>> listener) {
719 String uri = "ssap://com.webos.applicationManager/listApps";
721 ServiceCommand<List<AppInfo>> request = new ServiceCommand<>(uri, null,
722 jsonObj -> GSON.fromJson(jsonObj.get("apps"), new TypeToken<ArrayList<AppInfo>>() {
723 }.getType()), listener);
725 sendCommand(request);
728 public void launchAppWithInfo(AppInfo appInfo, ResponseListener<LaunchSession> listener) {
729 launchAppWithInfo(appInfo, null, listener);
732 public void launchAppWithInfo(final AppInfo appInfo, @Nullable JsonObject params,
733 final ResponseListener<LaunchSession> listener) {
734 String uri = "ssap://system.launcher/launch";
735 JsonObject payload = new JsonObject();
737 final String appId = appInfo.getId();
739 String contentId = null;
741 if (params != null) {
742 contentId = params.get("contentId").getAsString();
745 payload.addProperty("id", appId);
747 if (contentId != null) {
748 payload.addProperty("contentId", contentId);
751 if (params != null) {
752 payload.add("params", params);
755 ServiceCommand<LaunchSession> request = new ServiceCommand<>(uri, payload, obj -> {
756 LaunchSession launchSession = new LaunchSession();
757 launchSession.setService(this);
758 launchSession.setAppId(appId); // note that response uses id to mean appId
759 if (obj.has("sessionId")) {
760 launchSession.setSessionId(obj.get("sessionId").getAsString());
761 launchSession.setSessionType(LaunchSessionType.App);
763 launchSession.setSessionType(LaunchSessionType.Unknown);
765 return launchSession;
767 sendCommand(request);
770 public void launchBrowser(String url, final ResponseListener<LaunchSession> listener) {
771 String uri = "ssap://system.launcher/open";
772 JsonObject payload = new JsonObject();
773 payload.addProperty("target", url);
775 ServiceCommand<LaunchSession> request = new ServiceCommand<>(uri, payload, obj -> {
776 LaunchSession launchSession = new LaunchSession();
777 launchSession.setService(this);
778 launchSession.setAppId(obj.get("id").getAsString()); // note that response uses id to mean appId
779 if (obj.has("sessionId")) {
780 launchSession.setSessionId(obj.get("sessionId").getAsString());
781 launchSession.setSessionType(LaunchSessionType.App);
783 launchSession.setSessionType(LaunchSessionType.Unknown);
785 return launchSession;
787 sendCommand(request);
790 public void closeLaunchSession(LaunchSession launchSession, ResponseListener<CommandConfirmation> listener) {
791 LGWebOSTVSocket service = launchSession.getService();
793 switch (launchSession.getSessionType()) {
795 case ExternalInputPicker:
796 service.closeApp(launchSession, listener);
800 * If we want to extend support for MediaPlayer or WebAppLauncher at some point, this is how it was handeled
804 * if (service instanceof MediaPlayer) {
805 * ((MediaPlayer) service).closeMedia(launchSession, listener);
811 * if (service instanceof WebAppLauncher) {
812 * ((WebAppLauncher) service).closeWebApp(launchSession, listener);
818 listener.onError("This DeviceService does not know ho to close this LaunchSession");
823 public void closeApp(LaunchSession launchSession, ResponseListener<CommandConfirmation> listener) {
824 String uri = "ssap://system.launcher/close";
826 JsonObject payload = new JsonObject();
827 payload.addProperty("id", launchSession.getAppId());
828 payload.addProperty("sessionId", launchSession.getSessionId());
830 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, payload,
831 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
832 launchSession.getService().sendCommand(request);
835 public ServiceSubscription<AppInfo> subscribeRunningApp(ResponseListener<AppInfo> listener) {
836 ResponseListener<AppInfo> interceptor = new ResponseListener<AppInfo>() {
839 public void onSuccess(AppInfo appInfo) {
840 if (appInfo.getId().isEmpty()) {
841 scheduleDisconectingJob();
843 stopDisconnectingJob();
844 if (state == State.DISCONNECTING) {
845 setState(State.REGISTERED);
848 listener.onSuccess(appInfo);
852 public void onError(String message) {
853 listener.onError(message);
856 ServiceSubscription<AppInfo> request = new ServiceSubscription<>(FOREGROUND_APP, null,
857 jsonObj -> GSON.fromJson(jsonObj, AppInfo.class), interceptor);
858 sendCommand(request);
862 public ServiceCommand<AppInfo> getRunningApp(ResponseListener<AppInfo> listener) {
863 ServiceCommand<AppInfo> request = new ServiceCommand<>(FOREGROUND_APP, null,
864 jsonObj -> GSON.fromJson(jsonObj, AppInfo.class), listener);
865 sendCommand(request);
871 public ServiceSubscription<TextInputStatusInfo> subscribeTextInputStatus(
872 ResponseListener<TextInputStatusInfo> listener) {
873 return keyboardInput.connect(listener);
876 public void sendText(String input) {
877 keyboardInput.sendText(input);
880 public void sendEnter() {
881 keyboardInput.sendEnter();
884 public void sendDelete() {
885 keyboardInput.sendDel();
890 public void executeMouse(Consumer<LGWebOSTVMouseSocket> onConnected) {
891 LGWebOSTVMouseSocket mouseSocket = new LGWebOSTVMouseSocket(this.client);
892 mouseSocket.setListener(new WebOSTVMouseSocketListener() {
895 public void onStateChanged(LGWebOSTVMouseSocket.State oldState, LGWebOSTVMouseSocket.State newState) {
898 onConnected.accept(mouseSocket);
899 mouseSocket.disconnect();
907 public void onError(String errorMessage) {
908 logger.debug("Error in communication with Mouse Socket: {}", errorMessage);
912 String uri = "ssap://com.webos.service.networkinput/getPointerInputSocket";
914 ResponseListener<JsonObject> listener = new ResponseListener<JsonObject>() {
917 public void onSuccess(@Nullable JsonObject jsonObj) {
918 if (jsonObj != null) {
919 String socketPath = jsonObj.get("socketPath").getAsString().replace("wss:", "ws:").replace(":3001/",
922 mouseSocket.connect(new URI(socketPath));
923 } catch (URISyntaxException e) {
924 logger.warn("Connect mouse error: {}", e.getMessage());
930 public void onError(String error) {
931 logger.warn("Connect mouse error: {}", error);
935 ServiceCommand<JsonObject> request = new ServiceCommand<>(uri, null, x -> x, listener);
936 sendCommand(request);
939 // Simulate Remote Control Button press
941 public void sendRCButton(String rcButton, ResponseListener<CommandConfirmation> listener) {
942 executeMouse(s -> s.button(rcButton));
945 public interface ConfigProvider {
946 void storeKey(String key);
948 void storeProperties(Map<String, String> properties);