2 * Copyright (c) 2010-2020 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 Optional.ofNullable(this.listener).ifPresent(l -> l.onError(cause.getMessage()));
250 public void onClose(int statusCode, String reason) {
251 logger.debug("WebSocket Closed - Code: {}, Reason: {}", statusCode, reason);
252 this.requests.clear();
254 setState(State.DISCONNECTED);
258 * WebOS WebSocket API specific Communication
261 setState(State.CONNECTING);
263 JsonObject packet = new JsonObject();
264 packet.addProperty("id", nextRequestId());
265 packet.addProperty("type", "hello");
267 JsonObject payload = new JsonObject();
268 payload.addProperty("appId", "org.openhab");
269 payload.addProperty("appName", "openHAB");
270 payload.addProperty("appRegion", Locale.getDefault().getDisplayCountry());
271 packet.add("payload", payload);
272 // the hello response will not contain id, therefore not registering in requests
276 void sendRegister() {
277 setState(State.REGISTERING);
279 JsonObject packet = new JsonObject();
280 int id = nextRequestId();
281 packet.addProperty("id", id);
282 packet.addProperty("type", "register");
284 JsonObject manifest = new JsonObject();
285 manifest.addProperty("manifestVersion", 1);
287 String[] permissions = { "LAUNCH", "LAUNCH_WEBAPP", "APP_TO_APP", "CONTROL_AUDIO",
288 "CONTROL_INPUT_MEDIA_PLAYBACK", "CONTROL_POWER", "READ_INSTALLED_APPS", "CONTROL_DISPLAY",
289 "CONTROL_INPUT_JOYSTICK", "CONTROL_INPUT_MEDIA_RECORDING", "CONTROL_INPUT_TV", "READ_INPUT_DEVICE_LIST",
290 "READ_NETWORK_STATE", "READ_TV_CHANNEL_LIST", "WRITE_NOTIFICATION_TOAST", "CONTROL_INPUT_TEXT",
291 "CONTROL_MOUSE_AND_KEYBOARD", "READ_CURRENT_CHANNEL", "READ_RUNNING_APPS" };
293 manifest.add("permissions", GSON.toJsonTree(permissions));
295 JsonObject payload = new JsonObject();
296 String key = config.getKey();
297 if (!key.isEmpty()) {
298 payload.addProperty("client-key", key);
300 payload.addProperty("pairingType", "PROMPT"); // PIN, COMBINED
301 payload.add("manifest", manifest);
302 packet.add("payload", payload);
303 ResponseListener<JsonObject> dummyListener = new ResponseListener<JsonObject>() {
306 public void onSuccess(@Nullable JsonObject payload) {
307 // Noting to do here. TV shows PROMPT dialog.
308 // Waiting for message of type error or registered
312 public void onError(String message) {
313 logger.debug("Registration failed with message: {}", message);
318 this.requests.put(id, new ServiceSubscription<>("dummy", payload, x -> x, dummyListener));
319 sendMessage(packet, !key.isEmpty());
322 private int nextRequestId() {
325 requestId = nextRequestId++;
326 } while (requests.containsKey(requestId));
330 public void sendCommand(ServiceCommand<?> command) {
333 int requestId = nextRequestId();
334 requests.put(requestId, command);
335 JsonObject packet = new JsonObject();
336 packet.addProperty("type", command.getType());
337 packet.addProperty("id", requestId);
338 packet.addProperty("uri", command.getTarget());
339 JsonElement payload = command.getPayload();
340 if (payload != null) {
341 packet.add("payload", payload);
343 this.sendMessage(packet);
350 logger.debug("Skipping {} command {} for {} in state {}", command.getType(), command,
351 command.getTarget(), state);
356 public void unsubscribe(ServiceSubscription<?> subscription) {
357 Optional<Entry<Integer, ServiceCommand<?>>> entry = this.requests.entrySet().stream()
358 .filter(e -> e.getValue().equals(subscription)).findFirst();
359 if (entry.isPresent()) {
360 int requestId = entry.get().getKey();
361 this.requests.remove(requestId);
362 JsonObject packet = new JsonObject();
363 packet.addProperty("type", "unsubscribe");
364 packet.addProperty("id", requestId);
369 private void sendMessage(JsonObject json) {
370 sendMessage(json, false);
373 private void sendMessage(JsonObject json, boolean checkKey) {
374 String msg = GSON.toJson(json);
375 Session s = this.session;
378 if (logger.isTraceEnabled()) {
379 logger.trace("Message [out]: {}", checkKey ? GSON.toJson(maskKeyInJson(json)) : msg);
381 s.getRemote().sendString(msg);
383 logger.warn("No Connection to TV, skipping [out]: {}",
384 checkKey ? GSON.toJson(maskKeyInJson(json)) : msg);
386 } catch (IOException e) {
387 logger.warn("Unable to send message.", e);
391 private JsonObject maskKeyInJson(JsonObject json) {
392 if (json.has("payload") && json.getAsJsonObject("payload").has("client-key")) {
393 JsonObject jsonCopy = json.deepCopy();
394 JsonObject payload = jsonCopy.getAsJsonObject("payload");
395 payload.remove("client-key");
396 payload.addProperty("client-key", "***");
403 public void onMessage(String message) {
404 Response response = GSON.fromJson(message, Response.class);
405 JsonElement payload = response.getPayload();
406 JsonObject jsonPayload = payload == null ? null : payload.getAsJsonObject();
407 String messageToLog = (jsonPayload != null && jsonPayload.has("client-key")) ? "***" : message;
408 logger.trace("Message [in]: {}", messageToLog);
409 ServiceCommand<?> request = null;
411 if (response.getId() != null) {
412 request = requests.get(response.getId());
413 if (request == null) {
414 logger.warn("Received a response with id {}, for which no request was found. This should not happen.",
417 // for subscriptions we want to keep the original
418 // message, so that we have a reference to the response listener
419 if (!(request instanceof ServiceSubscription<?>)) {
420 requests.remove(response.getId());
425 switch (response.getType()) {
427 if (request == null) {
428 logger.debug("No matching request found for response message: {}", messageToLog);
431 if (payload == null) {
432 logger.debug("No payload in response message: {}", messageToLog);
436 request.processResponse(jsonPayload);
437 } catch (RuntimeException ex) {
438 // An uncaught runtime exception in @OnWebSocketMessage annotated method will cause the web socket
439 // implementation to call @OnWebSocketError callback in which we would reset the connection.
440 // Users have the ability to create miss-configurations in which IllegalArgumentException could be
442 logger.warn("Error while processing message: {} - in response to request: {} - Error Message: {}",
443 messageToLog, request, ex.getMessage());
447 logger.debug("Error: {}", messageToLog);
449 if (request == null) {
450 logger.warn("No matching request found for error message: {}", messageToLog);
453 if (payload == null) {
454 logger.warn("No payload in error message: {}", messageToLog);
458 request.processError(response.getError());
459 } catch (RuntimeException ex) {
460 // An uncaught runtime exception in @OnWebSocketMessage annotated method will cause the web socket
461 // implementation to call @OnWebSocketError callback in which we would reset the connection.
462 // Users have the ability to create miss-configurations in which IllegalArgumentException could be
464 logger.warn("Error while processing error: {} - in response to request: {} - Error Message: {}",
465 messageToLog, request, ex.getMessage());
469 if (state != State.CONNECTING) {
470 logger.debug("Skipping response {}, not in CONNECTING state, state was {}", messageToLog, state);
473 if (jsonPayload == null) {
474 logger.warn("No payload in error message: {}", messageToLog);
477 Map<String, String> map = new HashMap<>();
478 map.put(PROPERTY_DEVICE_OS, jsonPayload.get("deviceOS").getAsString());
479 map.put(PROPERTY_DEVICE_OS_VERSION, jsonPayload.get("deviceOSVersion").getAsString());
480 map.put(PROPERTY_DEVICE_OS_RELEASE_VERSION, jsonPayload.get("deviceOSReleaseVersion").getAsString());
481 map.put(PROPERTY_LAST_CONNECTED, Instant.now().toString());
482 config.storeProperties(map);
486 if (state != State.REGISTERING) {
487 logger.debug("Skipping response {}, not in REGISTERING state, state was {}", messageToLog, state);
490 if (jsonPayload == null) {
491 logger.warn("No payload in registered message: {}", messageToLog);
494 this.requests.remove(response.getId());
495 config.storeKey(jsonPayload.get("client-key").getAsString());
496 setState(State.REGISTERED);
501 public interface WebOSTVSocketListener {
503 public void onStateChanged(State state);
505 public void onError(String errorMessage);
508 public ServiceSubscription<Boolean> subscribeMute(ResponseListener<Boolean> listener) {
509 ServiceSubscription<Boolean> request = new ServiceSubscription<>(MUTE, null,
510 (jsonObj) -> jsonObj.get("mute").getAsBoolean(), listener);
511 sendCommand(request);
515 public ServiceCommand<Boolean> getMute(ResponseListener<Boolean> listener) {
516 ServiceCommand<Boolean> request = new ServiceCommand<>(MUTE, null,
517 (jsonObj) -> jsonObj.get("mute").getAsBoolean(), listener);
518 sendCommand(request);
522 public ServiceSubscription<Float> subscribeVolume(ResponseListener<Float> listener) {
523 ServiceSubscription<Float> request = new ServiceSubscription<>(VOLUME, null,
524 jsonObj -> jsonObj.get("volume").getAsInt() >= 0 ? (float) (jsonObj.get("volume").getAsInt() / 100.0)
527 sendCommand(request);
531 public ServiceCommand<Float> getVolume(ResponseListener<Float> listener) {
532 ServiceCommand<Float> request = new ServiceCommand<>(VOLUME, null,
533 jsonObj -> jsonObj.get("volume").getAsInt() >= 0 ? (float) (jsonObj.get("volume").getAsInt() / 100.0)
536 sendCommand(request);
540 public void setMute(boolean isMute, ResponseListener<CommandConfirmation> listener) {
541 String uri = "ssap://audio/setMute";
542 JsonObject payload = new JsonObject();
543 payload.addProperty("mute", isMute);
545 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, payload,
546 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
547 sendCommand(request);
550 public void setVolume(float volume, ResponseListener<CommandConfirmation> listener) {
551 String uri = "ssap://audio/setVolume";
552 JsonObject payload = new JsonObject();
553 int intVolume = Math.round(volume * 100.0f);
554 payload.addProperty("volume", intVolume);
555 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, payload,
556 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
557 sendCommand(request);
560 public void volumeUp(ResponseListener<CommandConfirmation> listener) {
561 String uri = "ssap://audio/volumeUp";
562 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
563 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
564 sendCommand(request);
567 public void volumeDown(ResponseListener<CommandConfirmation> listener) {
568 String uri = "ssap://audio/volumeDown";
569 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
570 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
571 sendCommand(request);
574 public ServiceSubscription<ChannelInfo> subscribeCurrentChannel(ResponseListener<ChannelInfo> listener) {
575 ServiceSubscription<ChannelInfo> request = new ServiceSubscription<>(CHANNEL, null,
576 jsonObj -> GSON.fromJson(jsonObj, ChannelInfo.class), listener);
577 sendCommand(request);
582 public ServiceCommand<ChannelInfo> getCurrentChannel(ResponseListener<ChannelInfo> listener) {
583 ServiceCommand<ChannelInfo> request = new ServiceCommand<>(CHANNEL, null,
584 jsonObj -> GSON.fromJson(jsonObj, ChannelInfo.class), listener);
585 sendCommand(request);
590 public void setChannel(ChannelInfo channelInfo, ResponseListener<CommandConfirmation> listener) {
591 JsonObject payload = new JsonObject();
592 if (channelInfo.getId() != null) {
593 payload.addProperty("channelId", channelInfo.getId());
595 if (channelInfo.getChannelNumber() != null) {
596 payload.addProperty("channelNumber", channelInfo.getChannelNumber());
598 setChannel(payload, listener);
601 private void setChannel(JsonObject payload, ResponseListener<CommandConfirmation> listener) {
602 String uri = "ssap://tv/openChannel";
603 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, payload,
604 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
605 sendCommand(request);
608 public void channelUp(ResponseListener<CommandConfirmation> listener) {
609 String uri = "ssap://tv/channelUp";
610 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
611 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
612 sendCommand(request);
615 public void channelDown(ResponseListener<CommandConfirmation> listener) {
616 String uri = "ssap://tv/channelDown";
617 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
618 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
619 sendCommand(request);
622 public void getChannelList(ResponseListener<List<ChannelInfo>> listener) {
623 ServiceCommand<List<ChannelInfo>> request = new ServiceCommand<>(CHANNEL_LIST, null,
624 jsonObj -> GSON.fromJson(jsonObj.get("channelList"), new TypeToken<ArrayList<ChannelInfo>>() {
625 }.getType()), listener);
626 sendCommand(request);
631 public void showToast(String message, ResponseListener<CommandConfirmation> listener) {
632 showToast(message, null, null, listener);
635 public void showToast(String message, @Nullable String iconData, @Nullable String iconExtension,
636 ResponseListener<CommandConfirmation> listener) {
637 JsonObject payload = new JsonObject();
638 payload.addProperty("message", message);
640 if (iconData != null && iconExtension != null) {
641 payload.addProperty("iconData", iconData);
642 payload.addProperty("iconExtension", iconExtension);
645 sendToast(payload, listener);
648 private void sendToast(JsonObject payload, ResponseListener<CommandConfirmation> listener) {
649 String uri = "ssap://system.notifications/createToast";
650 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, payload,
651 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
652 sendCommand(request);
656 public void powerOff(ResponseListener<CommandConfirmation> listener) {
657 String uri = "ssap://system/turnOff";
659 ResponseListener<CommandConfirmation> interceptor = new ResponseListener<CommandConfirmation>() {
662 public void onSuccess(CommandConfirmation confirmation) {
663 if (confirmation.getReturnValue()) {
666 listener.onSuccess(confirmation);
670 public void onError(String message) {
671 listener.onError(message);
674 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
675 x -> GSON.fromJson(x, CommandConfirmation.class), interceptor);
676 sendCommand(request);
680 public void play(ResponseListener<CommandConfirmation> listener) {
681 String uri = "ssap://media.controls/play";
682 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
683 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
684 sendCommand(request);
687 public void pause(ResponseListener<CommandConfirmation> listener) {
688 String uri = "ssap://media.controls/pause";
689 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
690 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
691 sendCommand(request);
694 public void stop(ResponseListener<CommandConfirmation> listener) {
695 String uri = "ssap://media.controls/stop";
696 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
697 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
698 sendCommand(request);
701 public void rewind(ResponseListener<CommandConfirmation> listener) {
702 String uri = "ssap://media.controls/rewind";
703 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
704 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
705 sendCommand(request);
708 public void fastForward(ResponseListener<CommandConfirmation> listener) {
709 String uri = "ssap://media.controls/fastForward";
710 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, null,
711 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
712 sendCommand(request);
717 public void getAppList(final ResponseListener<List<AppInfo>> listener) {
718 String uri = "ssap://com.webos.applicationManager/listApps";
720 ServiceCommand<List<AppInfo>> request = new ServiceCommand<>(uri, null,
721 jsonObj -> GSON.fromJson(jsonObj.get("apps"), new TypeToken<ArrayList<AppInfo>>() {
722 }.getType()), listener);
724 sendCommand(request);
727 public void launchAppWithInfo(AppInfo appInfo, ResponseListener<LaunchSession> listener) {
728 launchAppWithInfo(appInfo, null, listener);
731 public void launchAppWithInfo(final AppInfo appInfo, @Nullable JsonObject params,
732 final ResponseListener<LaunchSession> listener) {
733 String uri = "ssap://system.launcher/launch";
734 JsonObject payload = new JsonObject();
736 final String appId = appInfo.getId();
738 String contentId = null;
740 if (params != null) {
741 contentId = params.get("contentId").getAsString();
744 payload.addProperty("id", appId);
746 if (contentId != null) {
747 payload.addProperty("contentId", contentId);
750 if (params != null) {
751 payload.add("params", params);
754 ServiceCommand<LaunchSession> request = new ServiceCommand<>(uri, payload, obj -> {
755 LaunchSession launchSession = new LaunchSession();
756 launchSession.setService(this);
757 launchSession.setAppId(appId); // note that response uses id to mean appId
758 if (obj.has("sessionId")) {
759 launchSession.setSessionId(obj.get("sessionId").getAsString());
760 launchSession.setSessionType(LaunchSessionType.App);
762 launchSession.setSessionType(LaunchSessionType.Unknown);
764 return launchSession;
766 sendCommand(request);
769 public void launchBrowser(String url, final ResponseListener<LaunchSession> listener) {
770 String uri = "ssap://system.launcher/open";
771 JsonObject payload = new JsonObject();
772 payload.addProperty("target", url);
774 ServiceCommand<LaunchSession> request = new ServiceCommand<>(uri, payload, obj -> {
775 LaunchSession launchSession = new LaunchSession();
776 launchSession.setService(this);
777 launchSession.setAppId(obj.get("id").getAsString()); // note that response uses id to mean appId
778 if (obj.has("sessionId")) {
779 launchSession.setSessionId(obj.get("sessionId").getAsString());
780 launchSession.setSessionType(LaunchSessionType.App);
782 launchSession.setSessionType(LaunchSessionType.Unknown);
784 return launchSession;
786 sendCommand(request);
789 public void closeLaunchSession(LaunchSession launchSession, ResponseListener<CommandConfirmation> listener) {
790 LGWebOSTVSocket service = launchSession.getService();
792 switch (launchSession.getSessionType()) {
794 case ExternalInputPicker:
795 service.closeApp(launchSession, listener);
799 * If we want to extend support for MediaPlayer or WebAppLauncher at some point, this is how it was handeled
803 * if (service instanceof MediaPlayer) {
804 * ((MediaPlayer) service).closeMedia(launchSession, listener);
810 * if (service instanceof WebAppLauncher) {
811 * ((WebAppLauncher) service).closeWebApp(launchSession, listener);
817 listener.onError("This DeviceService does not know ho to close this LaunchSession");
822 public void closeApp(LaunchSession launchSession, ResponseListener<CommandConfirmation> listener) {
823 String uri = "ssap://system.launcher/close";
825 JsonObject payload = new JsonObject();
826 payload.addProperty("id", launchSession.getAppId());
827 payload.addProperty("sessionId", launchSession.getSessionId());
829 ServiceCommand<CommandConfirmation> request = new ServiceCommand<>(uri, payload,
830 x -> GSON.fromJson(x, CommandConfirmation.class), listener);
831 launchSession.getService().sendCommand(request);
834 public ServiceSubscription<AppInfo> subscribeRunningApp(ResponseListener<AppInfo> listener) {
835 ResponseListener<AppInfo> interceptor = new ResponseListener<AppInfo>() {
838 public void onSuccess(AppInfo appInfo) {
839 if (appInfo.getId().isEmpty()) {
840 scheduleDisconectingJob();
842 stopDisconnectingJob();
843 if (state == State.DISCONNECTING) {
844 setState(State.REGISTERED);
847 listener.onSuccess(appInfo);
851 public void onError(String message) {
852 listener.onError(message);
855 ServiceSubscription<AppInfo> request = new ServiceSubscription<>(FOREGROUND_APP, null,
856 jsonObj -> GSON.fromJson(jsonObj, AppInfo.class), interceptor);
857 sendCommand(request);
861 public ServiceCommand<AppInfo> getRunningApp(ResponseListener<AppInfo> listener) {
862 ServiceCommand<AppInfo> request = new ServiceCommand<>(FOREGROUND_APP, null,
863 jsonObj -> GSON.fromJson(jsonObj, AppInfo.class), listener);
864 sendCommand(request);
870 public ServiceSubscription<TextInputStatusInfo> subscribeTextInputStatus(
871 ResponseListener<TextInputStatusInfo> listener) {
872 return keyboardInput.connect(listener);
875 public void sendText(String input) {
876 keyboardInput.sendText(input);
879 public void sendEnter() {
880 keyboardInput.sendEnter();
883 public void sendDelete() {
884 keyboardInput.sendDel();
889 public void executeMouse(Consumer<LGWebOSTVMouseSocket> onConnected) {
890 LGWebOSTVMouseSocket mouseSocket = new LGWebOSTVMouseSocket(this.client);
891 mouseSocket.setListener(new WebOSTVMouseSocketListener() {
894 public void onStateChanged(LGWebOSTVMouseSocket.State oldState, LGWebOSTVMouseSocket.State newState) {
897 onConnected.accept(mouseSocket);
898 mouseSocket.disconnect();
906 public void onError(String errorMessage) {
907 logger.debug("Error in communication with Mouse Socket: {}", errorMessage);
911 String uri = "ssap://com.webos.service.networkinput/getPointerInputSocket";
913 ResponseListener<JsonObject> listener = new ResponseListener<JsonObject>() {
916 public void onSuccess(@Nullable JsonObject jsonObj) {
917 if (jsonObj != null) {
918 String socketPath = jsonObj.get("socketPath").getAsString().replace("wss:", "ws:").replace(":3001/",
921 mouseSocket.connect(new URI(socketPath));
922 } catch (URISyntaxException e) {
923 logger.warn("Connect mouse error: {}", e.getMessage());
929 public void onError(String error) {
930 logger.warn("Connect mouse error: {}", error);
934 ServiceCommand<JsonObject> request = new ServiceCommand<>(uri, null, x -> x, listener);
935 sendCommand(request);
938 // Simulate Remote Control Button press
940 public void sendRCButton(String rcButton, ResponseListener<CommandConfirmation> listener) {
941 executeMouse(s -> s.button(rcButton));
944 public interface ConfigProvider {
945 void storeKey(String key);
947 void storeProperties(Map<String, String> properties);