diff --git a/app/src/test/java/org/xbmc/kore/tests/jsonrpc/method/ApplicationTest.java b/app/src/test/java/org/xbmc/kore/tests/jsonrpc/method/ApplicationTest.java new file mode 100644 index 0000000..b901085 --- /dev/null +++ b/app/src/test/java/org/xbmc/kore/tests/jsonrpc/method/ApplicationTest.java @@ -0,0 +1,207 @@ +/* + * Copyright 2016 Martijn Brekhof. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xbmc.kore.tests.jsonrpc.method; + +import android.os.Handler; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.xbmc.kore.BuildConfig; +import org.xbmc.kore.host.HostInfo; +import org.xbmc.kore.jsonrpc.ApiCallback; +import org.xbmc.kore.jsonrpc.HostConnection; +import org.xbmc.kore.jsonrpc.type.ApplicationType; +import org.xbmc.kore.jsonrpc.type.GlobalType; +import org.xbmc.kore.testutils.tcpserver.MockTcpServer; +import org.xbmc.kore.testutils.tcpserver.handlers.ApplicationHandler; +import org.xbmc.kore.testutils.tcpserver.handlers.JSONConnectionHandlerManager; +import org.xbmc.kore.utils.RoboThreadRunner; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = 23) +public class ApplicationTest { + + private HostConnection hostConnection; + private MockTcpServer server; + private ApplicationHandler applicationHandler; + private JSONConnectionHandlerManager manager; + + @Before + public void setup() throws Exception { + applicationHandler = new ApplicationHandler(); + + manager = new JSONConnectionHandlerManager(); + manager.addHandler(applicationHandler); + + server = new MockTcpServer(manager); + server.start(); + + HostInfo hostInfo = new HostInfo("TESTHOST", server.getHostName(), HostConnection.PROTOCOL_TCP, + HostInfo.DEFAULT_HTTP_PORT, server.getPort(), null, null, true, + HostInfo.DEFAULT_EVENT_SERVER_PORT, + false); + + hostConnection = new HostConnection(hostInfo); + } + + @After + public void tearDown() throws Exception { + server.shutdown(); + hostConnection.disconnect(); + } + + @Test + public void getPropertiesTest() throws Exception { + org.xbmc.kore.jsonrpc.method.Application.GetProperties properties = + new org.xbmc.kore.jsonrpc.method.Application.GetProperties(org.xbmc.kore.jsonrpc.method.Application.GetProperties.MUTED); + applicationHandler.setMuted(true, false); + + hostConnection.execute(properties, new ApiCallback() { + @Override + public void onSuccess(ApplicationType.PropertyValue result) { + assertNotNull(result); + assertTrue(result.muted); + RoboThreadRunner.stop(); + } + + @Override + public void onError(int errorCode, String description) { + fail("errorCode="+errorCode+", description="+description); + RoboThreadRunner.stop(); + } + }, new Handler()); + + assertTrue(RoboThreadRunner.run(10)); + } + + @Test + public void setMuteTrueTest() throws Exception { + applicationHandler.setMuted(false, false); + sendSetMute(true); + assertTrue(RoboThreadRunner.run(10)); + } + + @Test + public void setMuteFalseTest() throws Exception { + applicationHandler.setMuted(true, false); + sendSetMute(false); + assertTrue(RoboThreadRunner.run(10)); + } + + @Test + public void incrementVolumeTest() throws Exception { + org.xbmc.kore.jsonrpc.method.Application.SetVolume setVolume = + new org.xbmc.kore.jsonrpc.method.Application.SetVolume(GlobalType.IncrementDecrement.INCREMENT); + applicationHandler.setVolume(77, false); + hostConnection.execute(setVolume, new ApiCallback() { + @Override + public void onSuccess(Integer result) { + assertNotNull(result); + assertTrue(result == 78); + RoboThreadRunner.stop(); + } + + @Override + public void onError(int errorCode, String description) { + fail("errorCode="+errorCode+", description="+description); + RoboThreadRunner.stop(); + } + }, new Handler()); + + assertTrue(RoboThreadRunner.run(10)); + } + + + @Test + public void decrementVolumeTest() throws Exception { + org.xbmc.kore.jsonrpc.method.Application.SetVolume setVolume = + new org.xbmc.kore.jsonrpc.method.Application.SetVolume(GlobalType.IncrementDecrement.DECREMENT); + applicationHandler.setVolume(77, false); + hostConnection.execute(setVolume, new ApiCallback() { + @Override + public void onSuccess(Integer result) { + assertNotNull(result); + assertTrue(result == 76); + RoboThreadRunner.stop(); + } + + @Override + public void onError(int errorCode, String description) { + fail("errorCode="+errorCode+", description="+description); + RoboThreadRunner.stop(); + } + }, new Handler()); + + assertTrue(RoboThreadRunner.run(10)); + } + + @Test + public void setVolumeTest() throws Exception { + org.xbmc.kore.jsonrpc.method.Application.SetVolume setVolume = + new org.xbmc.kore.jsonrpc.method.Application.SetVolume(83); + applicationHandler.setVolume(77, false); + hostConnection.execute(setVolume, new ApiCallback() { + @Override + public void onSuccess(Integer result) { + assertNotNull(result); + assertTrue(result == 83); + RoboThreadRunner.stop(); + } + + @Override + public void onError(int errorCode, String description) { + fail("errorCode="+errorCode+", description="+description); + RoboThreadRunner.stop(); + } + }, new Handler()); + + assertTrue(RoboThreadRunner.run(10)); + } + + /** + * Sends the SetMute method to toggle the mute state + * @throws InterruptedException + */ + private void sendSetMute(final boolean expectedMuteState) throws InterruptedException { + org.xbmc.kore.jsonrpc.method.Application.SetMute mute = + new org.xbmc.kore.jsonrpc.method.Application.SetMute(); + + hostConnection.execute(mute, new ApiCallback() { + @Override + public void onSuccess(Boolean result) { + assertTrue(result == expectedMuteState); + RoboThreadRunner.stop(); + } + + @Override + public void onError(int errorCode, String description) { + fail(description); + RoboThreadRunner.stop(); + } + }, new Handler()); + + } +} diff --git a/app/src/test/java/org/xbmc/kore/tests/jsonrpc/notifications/ApplicationTest.java b/app/src/test/java/org/xbmc/kore/tests/jsonrpc/notifications/ApplicationTest.java new file mode 100644 index 0000000..de40cf9 --- /dev/null +++ b/app/src/test/java/org/xbmc/kore/tests/jsonrpc/notifications/ApplicationTest.java @@ -0,0 +1,169 @@ +/* + * Copyright 2016 Martijn Brekhof. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xbmc.kore.tests.jsonrpc.notifications; + +import android.os.Handler; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowLog; +import org.xbmc.kore.BuildConfig; +import org.xbmc.kore.host.HostInfo; +import org.xbmc.kore.jsonrpc.ApiCallback; +import org.xbmc.kore.jsonrpc.HostConnection; +import org.xbmc.kore.jsonrpc.notification.Application; +import org.xbmc.kore.testutils.tcpserver.MockTcpServer; +import org.xbmc.kore.testutils.tcpserver.handlers.ApplicationHandler; +import org.xbmc.kore.testutils.tcpserver.handlers.JSONConnectionHandlerManager; +import org.xbmc.kore.utils.RoboThreadRunner; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = 23) +public class ApplicationTest { + + private HostConnection hostConnection; + private MockTcpServer server; + private ApplicationHandler applicationHandler; + + @Before + public void setup() throws Exception { + ShadowLog.stream = System.out; + + applicationHandler = new ApplicationHandler(); + + JSONConnectionHandlerManager manager = new JSONConnectionHandlerManager(); + manager.addHandler(applicationHandler); + + server = new MockTcpServer(manager); + server.start(); + + HostInfo hostInfo = new HostInfo("TESTHOST", server.getHostName(), HostConnection.PROTOCOL_TCP, + HostInfo.DEFAULT_HTTP_PORT, server.getPort(), null, null, true, + HostInfo.DEFAULT_EVENT_SERVER_PORT, false); + + hostConnection = new HostConnection(hostInfo); + } + + @After + public void tearDown() throws Exception { + server.shutdown(); + hostConnection.disconnect(); + } + + @Test + public void onVolumeChanged() throws InterruptedException { + HostConnection.ApplicationNotificationsObserver observer = + new HostConnection.ApplicationNotificationsObserver() { + @Override + public void onVolumeChanged(Application.OnVolumeChanged notification) { + RoboThreadRunner.stop(); + assertTrue(notification.volume == 84); + } + + }; + + hostConnection.registerApplicationNotificationsObserver(observer, new Handler()); + + applicationHandler.setVolume(82, false); + sendSetVolumeCommand(84); + + assertTrue(RoboThreadRunner.run(10)); + } + + @Test + public void onVolumeChangedMuted() throws InterruptedException { + HostConnection.ApplicationNotificationsObserver observer = + new HostConnection.ApplicationNotificationsObserver() { + @Override + public void onVolumeChanged(Application.OnVolumeChanged notification) { + RoboThreadRunner.stop(); + assertTrue(notification.muted); + } + + }; + + hostConnection.registerApplicationNotificationsObserver(observer, new Handler()); + + applicationHandler.setMuted(false, false); + sendToggleMuteCommand(); + + assertTrue(RoboThreadRunner.run(10)); + } + + @Test + public void onVolumeChangedNotMuted() throws InterruptedException { + HostConnection.ApplicationNotificationsObserver observer = + new HostConnection.ApplicationNotificationsObserver() { + @Override + public void onVolumeChanged(Application.OnVolumeChanged notification) { + RoboThreadRunner.stop(); + assertFalse(notification.muted); + } + + }; + + hostConnection.registerApplicationNotificationsObserver(observer, new Handler()); + + applicationHandler.setMuted(true, false); + sendToggleMuteCommand(); + + assertTrue(RoboThreadRunner.run(10)); + } + + + private void sendToggleMuteCommand() { + org.xbmc.kore.jsonrpc.method.Application.SetMute mute = + new org.xbmc.kore.jsonrpc.method.Application.SetMute(); + + hostConnection.execute(mute, new ApiCallback() { + @Override + public void onSuccess(Boolean result) { + } + + @Override + public void onError(int errorCode, String description) { + RoboThreadRunner.stop(); + fail("errorCode="+errorCode+", description="+description); + } + }, new Handler()); + } + + private void sendSetVolumeCommand(int volume) { + org.xbmc.kore.jsonrpc.method.Application.SetVolume setVolume = + new org.xbmc.kore.jsonrpc.method.Application.SetVolume(volume); + + hostConnection.execute(setVolume, new ApiCallback() { + @Override + public void onSuccess(Integer result) { + } + + @Override + public void onError(int errorCode, String description) { + RoboThreadRunner.stop(); + fail("errorCode="+errorCode+", description="+description); + } + }, new Handler()); + } +} diff --git a/app/src/test/java/org/xbmc/kore/utils/RoboThreadRunner.java b/app/src/test/java/org/xbmc/kore/utils/RoboThreadRunner.java new file mode 100644 index 0000000..4ff3c87 --- /dev/null +++ b/app/src/test/java/org/xbmc/kore/utils/RoboThreadRunner.java @@ -0,0 +1,46 @@ +/* + * Copyright 2016 Martijn Brekhof. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xbmc.kore.utils; + +import org.robolectric.Robolectric; + +public class RoboThreadRunner { + + private static boolean running; + + /** + * Runs the thread until {@link #stop()} is called or timeout is exceeded. + * This call blocks until either {@link #stop()} is called or timeout is exceeded. + * @param timeoutSeconds timeout in seconds + * @return false if timeout exceeded, true otherwise + * @throws InterruptedException + */ + public static boolean run(long timeoutSeconds) throws InterruptedException { + running = true; + long timeout = timeoutSeconds * 100; + while ( running && ( timeout-- > 0 )) { + Robolectric.getForegroundThreadScheduler().advanceToLastPostedRunnable(); + Thread.sleep(100); + } + + return timeout > 0; + } + + public static void stop() { + running = false; + } +} diff --git a/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/MockTcpServer.java b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/MockTcpServer.java new file mode 100644 index 0000000..0165576 --- /dev/null +++ b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/MockTcpServer.java @@ -0,0 +1,236 @@ +/* + * Copyright 2016 Martijn Brekhof. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xbmc.kore.testutils.tcpserver; + +import com.squareup.okhttp.internal.Util; + +import org.xbmc.kore.utils.LogUtils; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.util.Collections; +import java.util.Iterator; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import javax.net.ServerSocketFactory; + + + +public class MockTcpServer { + public static final String TAG = LogUtils.makeLogTag(MockTcpServer.class); + + private ServerSocketFactory serverSocketFactory = ServerSocketFactory.getDefault(); + private ServerSocket serverSocket; + private boolean started; + private ExecutorService executor; + private int port = -1; + private StringBuffer request; + private InetSocketAddress inetSocketAddress; + + private final Set openClientSockets = + Collections.newSetFromMap(new ConcurrentHashMap()); + + private final TcpServerConnectionHandler connectionHandler; + + public interface TcpServerConnectionHandler { + /** + * Processes received input + * @param c character received + * @return id of associated response if any, -1 if more input is needed. + */ + void processInput(char c); + + /** + * Gets the answer for this handler that should be returned to the server after input has been + * processed successfully + * @return answer or null if no answer is available + */ + String getResponse(); + } + + public MockTcpServer(TcpServerConnectionHandler handler) { + connectionHandler = handler; + } + + /** + * Starts the server on localhost on a random free port + * @throws IOException + */ + public void start() throws IOException { + start(new InetSocketAddress(InetAddress.getByName("localhost"), 0)); + } + + /** + * + * @param inetSocketAddress set portnumber to 0 to select a random free port + * @throws IOException + */ + public void start(InetSocketAddress inetSocketAddress) throws IOException { + if (started) throw new IllegalStateException("start() already called"); + started = true; + this.inetSocketAddress = inetSocketAddress; + + serverSocket = serverSocketFactory.createServerSocket(); + // Reuse port if not using a random port + serverSocket.setReuseAddress(inetSocketAddress.getPort() != 0); + serverSocket.bind(inetSocketAddress, 50); + + executor = Executors.newCachedThreadPool(Util.threadFactory("MockTcpServer", false)); + + port = serverSocket.getLocalPort(); + + executor.execute(new Runnable() { + @Override + public void run() { + try { + acceptConnection(); + } catch (Throwable e) { + LogUtils.LOGE(TAG, " failed unexpectedly: " + e); + } + + // Release all sockets and all threads, even if any close fails. + Util.closeQuietly(serverSocket); + for (Iterator s = openClientSockets.iterator(); s.hasNext(); ) { + Util.closeQuietly(s.next()); + s.remove(); + } + + executor.shutdown(); + } + + private void acceptConnection() throws Exception { + while (true) { + Socket socket; + try { + socket = serverSocket.accept(); + } catch (SocketException e) { + //Socket closed + return; + } + + openClientSockets.add(socket); + serveConnection(socket); + } + } + }); + } + + public synchronized void shutdown() throws IOException { + if (!started) return; + if (serverSocket == null) throw new IllegalStateException("shutdown() before start()"); + + serverSocket.close(); + + // Await shutdown. + try { + if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { + throw new IOException("Gave up waiting for executor to shut down"); + } + } catch (InterruptedException e) { + throw new AssertionError(); + } + } + + /** + * Gets the local port of this server socket or -1 if it is not bound + * @return the local port this server is listening on. + */ + public int getPort() { + return port; + } + + public String getHostName() { + if ( inetSocketAddress == null ) + throw new RuntimeException("Must start server before getting hostname"); + + return inetSocketAddress.getHostName(); + } + + private void serveConnection(final Socket socket) { + executor.execute(new Runnable() { + + @Override + public void run() { + try { + handleInput(); + } catch (IOException e) { + LogUtils.LOGW(TAG, "processing input from " + socket.getInetAddress() + " failed: " + e); + } + } + + private void handleInput() throws IOException { + InputStreamReader in = new InputStreamReader(socket.getInputStream()); + + request = new StringBuffer(); + int i; + while ((i = in.read()) != -1) { + request.append((char) i); + + synchronized (connectionHandler) { + connectionHandler.processInput((char) i); + } + } + + socket.close(); + openClientSockets.remove(socket); + } + }); + + executor.execute(new Runnable() { + @Override + public void run() { + try { + while (true) { + sendResponse(); + Thread.sleep(1000); + if ( serverSocket.isClosed() ) + return; + } + } catch (IOException e) { + LogUtils.LOGW(TAG, " sending response from " + socket.getInetAddress() + " failed: " + e); + } catch (InterruptedException e) { + LogUtils.LOGW(TAG, " wait interrupted" + e); + } + } + + private void sendResponse() throws IOException { + PrintWriter out = + new PrintWriter(socket.getOutputStream(), true); + String answer = connectionHandler.getResponse(); + if (answer != null) { + out.print(answer); + out.flush(); + } + } + }); + } + + @Override + public String toString() { + return "MockTcpServer[" + port + "]"; + } +} diff --git a/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/ApplicationHandler.java b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/ApplicationHandler.java new file mode 100644 index 0000000..7641098 --- /dev/null +++ b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/ApplicationHandler.java @@ -0,0 +1,138 @@ +/* + * Copyright 2016 Martijn Brekhof. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xbmc.kore.testutils.tcpserver.handlers; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.xbmc.kore.jsonrpc.type.GlobalType; +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.JsonResponse; +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.methods.Application; +import org.xbmc.kore.utils.LogUtils; + +import java.util.ArrayList; + +import static org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.notifications.Application.OnVolumeChanged; + +/** + * Simulates Application JSON-RPC API + */ +public class ApplicationHandler implements JSONConnectionHandlerManager.ConnectionHandler { + private static final String TAG = LogUtils.makeLogTag(ApplicationHandler.class); + + private boolean muted; + private int volume; + private static final String ID_NODE = "id"; + private static final String PARAMS_NODE = "params"; + private static final String PROPERTIES_NODE = "properties"; + + private ArrayList jsonNotifications = new ArrayList<>(); + + /** + * Sets the muted state and sends a notification + * @param muted + * @param notify true if OnVolumeChanged should be sent, false otherwise + */ + public void setMuted(boolean muted, boolean notify) { + this.muted = muted; + + if (notify) + jsonNotifications.add(new OnVolumeChanged(muted, volume)); + } + + /** + * Sets the volume and sends a notification + * @param volume + * @param notify true if OnVolumeChanged should be sent, false otherwise + */ + public void setVolume(int volume, boolean notify) { + this.volume = volume; + + if (notify) + jsonNotifications.add(new OnVolumeChanged(muted, volume)); + } + + @Override + public ArrayList getNotification() { + ArrayList jsonResponses = new ArrayList<>(jsonNotifications); + jsonNotifications.clear(); + return jsonResponses; + } + + @Override + public void reset() { + this.volume = 0; + this.muted = false; + } + + @Override + public String[] getType() { + return new String[]{Application.GetProperties.METHOD_NAME, + Application.SetMute.METHOD_NAME, + Application.SetVolume.METHOD_NAME}; + } + + @Override + public ArrayList getResponse(String method, ObjectNode jsonRequest) { + ArrayList jsonResponses = new ArrayList<>(); + + int methodId = jsonRequest.get(ID_NODE).asInt(-1); + + switch (method) { + case Application.GetProperties.METHOD_NAME: + Application.GetProperties response = new Application.GetProperties(methodId); + + JsonNode jsonNode = jsonRequest.get(PARAMS_NODE).get(PROPERTIES_NODE); + for (JsonNode node : jsonNode) { + switch(node.asText()) { + case Application.GetProperties.MUTED: + response.addMuteState(muted); + break; + case Application.GetProperties.VOLUME: + response.addVolume(volume); + break; + } + } + + jsonResponses.add(response); + break; + case Application.SetMute.METHOD_NAME: + setMuted(!muted, true); + jsonResponses.add(new Application.SetMute(methodId, muted)); + break; + case Application.SetVolume.METHOD_NAME: + JsonNode params = jsonRequest.get(PARAMS_NODE); + String value = params.get("volume").asText(); + switch (value) { + case GlobalType.IncrementDecrement.INCREMENT: + setVolume(volume + 1, true); + break; + case GlobalType.IncrementDecrement.DECREMENT: + setVolume(volume - 1, true); + break; + default: + setVolume(Integer.parseInt(value), true); + break; + } + jsonResponses.add(new Application.SetVolume(methodId, volume)); + break; + default: + LogUtils.LOGD(TAG, "method: " + method + ", not implemented"); + } + return jsonResponses; + } +} diff --git a/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/JSONConnectionHandlerManager.java b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/JSONConnectionHandlerManager.java new file mode 100644 index 0000000..eb0794a --- /dev/null +++ b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/JSONConnectionHandlerManager.java @@ -0,0 +1,171 @@ +/* + * Copyright 2016 Martijn Brekhof. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xbmc.kore.testutils.tcpserver.handlers; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.xbmc.kore.testutils.tcpserver.MockTcpServer; +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.JsonResponse; +import org.xbmc.kore.utils.LogUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.concurrent.TimeoutException; + +import static org.xbmc.kore.jsonrpc.ApiMethod.ID_NODE; +import static org.xbmc.kore.jsonrpc.ApiMethod.METHOD_NODE; + +public class JSONConnectionHandlerManager implements MockTcpServer.TcpServerConnectionHandler { + public static final String TAG = LogUtils.makeLogTag(JSONConnectionHandlerManager.class); + + private HashMap handlersByType = new HashMap<>(); + private HashSet handlers = new HashSet<>(); + private StringBuffer buffer = new StringBuffer(); + private int amountOfOpenBrackets = 0; + private final ObjectMapper objectMapper = new ObjectMapper(); + + private int responseCount; + + private HashMap> clientResponses = new HashMap<>(); + + public interface ConnectionHandler { + /** + * Used to determine which methods the handler implements + * @return list of JSON method names + */ + String[] getType(); + + /** + * Used to get the response from a handler implementing the requested + * method. + * @param method requested method + * @param jsonRequest json node containing the original request + * @return {@link JsonResponse} that should be sent to the client + */ + ArrayList getResponse(String method, ObjectNode jsonRequest); + + /** + * Used to get any notifications from the handler. + * @return {@link JsonResponse} that should be sent to the client + */ + ArrayList getNotification(); + + /** + * Should set the state of the handler to its initial state + */ + void reset(); + } + + public void addHandler(ConnectionHandler handler) throws Exception { + for(String type : handler.getType()) { + handlersByType.put(type, handler); + } + handlers.add(handler); + } + + @Override + public void processInput(char c) { + buffer.append(c); + if ( c == '{' ) { + amountOfOpenBrackets++; + } else if ( c == '}' ) { + amountOfOpenBrackets--; + } + + if ( amountOfOpenBrackets == 0 ) { + String input = buffer.toString(); + buffer = new StringBuffer(); + processJSONInput(input); + } + } + + private void processJSONInput(String input) { + try { + JsonParser parser = objectMapper.getFactory().createParser(input); + ObjectNode jsonRequest = objectMapper.readTree(parser); + + int methodId = jsonRequest.get(ID_NODE).asInt(); + String method = jsonRequest.get(METHOD_NODE).asText(); + ConnectionHandler connectionHandler = handlersByType.get(method); + if ( connectionHandler != null ) { + ArrayList responses = connectionHandler.getResponse(method, jsonRequest); + if (responses != null) { + addResponse(methodId, responses); + } + } + } catch (IOException e) { + LogUtils.LOGE(getClass().getSimpleName(), e.getMessage()); + } + } + + @Override + public String getResponse() { + StringBuffer stringBuffer = new StringBuffer(); + + //Handle responses + Collection> jsonResponses = clientResponses.values(); + for(ArrayList arrayList : jsonResponses) { + for (JsonResponse response : arrayList) { + stringBuffer.append(response.toJsonString() + "\n"); + } + } + clientResponses.clear(); + + //Handle notifications + for(ConnectionHandler handler : handlers) { + ArrayList jsonNotifications = handler.getNotification(); + if (jsonNotifications != null) { + for (JsonResponse jsonResponse : jsonNotifications) { + stringBuffer.append(jsonResponse.toJsonString() +"\n"); + } + } + } + responseCount++; + return stringBuffer.toString(); + } + + /** + * Waits until at least one response has been processed before returning + */ + public void waitForNextResponse(long timeOutMillis) throws TimeoutException { + responseCount = 0; + while ((responseCount < 2) && (timeOutMillis > 0)) { + try { + Thread.sleep(500); + timeOutMillis -= 500; + } catch (InterruptedException e) { + + } + } + if (timeOutMillis < 0) + throw new TimeoutException(); + } + + private void addResponse(int id, ArrayList jsonResponses) { + ArrayList responses = clientResponses.get(String.valueOf(id)); + if (responses == null) { + responses = new ArrayList<>(); + clientResponses.put(String.valueOf(id), responses); + } + responses.addAll(jsonResponses); + } +} diff --git a/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/JsonResponse.java b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/JsonResponse.java new file mode 100644 index 0000000..503785d --- /dev/null +++ b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/JsonResponse.java @@ -0,0 +1,199 @@ +/* + * Copyright 2016 Martijn Brekhof. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.xbmc.kore.utils.LogUtils; + +public abstract class JsonResponse { + private final String TAG = LogUtils.makeLogTag(JsonResponse.class); + + private final ObjectNode jsonResponse; + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static final String RESULT_NODE = "result"; + private static final String PARAMS_NODE = "params"; + private static final String METHOD_NODE = "method"; + private static final String DATA_NODE = "data"; + private static final String ID_NODE = "id"; + private static final String JSONRPC_NODE = "jsonrpc"; + + public enum TYPE { + OBJECT, + ARRAY + }; + + public JsonResponse() { + jsonResponse = objectMapper.createObjectNode(); + jsonResponse.put(JSONRPC_NODE, "2.0"); + } + + public JsonResponse(int id) { + this(); + jsonResponse.put(ID_NODE, id); + } + + protected ObjectNode createObjectNode() { + return objectMapper.createObjectNode(); + } + + protected ArrayNode createArrayNode() { + return objectMapper.createArrayNode(); + } + + /** + * Returns the node used to hold the result. First call will create the + * result node for the given type + * @param type that result node should be when first created + * @return result node + */ + protected JsonNode getResultNode(TYPE type) { + JsonNode result; + if(jsonResponse.has(RESULT_NODE)) { + result = jsonResponse.get(RESULT_NODE); + if( result.isArray() && type != TYPE.ARRAY ) { + LogUtils.LOGE(TAG, "requested result node of type Object but response contains result node of type Array"); + return null; + } + } else { + switch (type) { + case ARRAY: + result = objectMapper.createArrayNode(); + break; + case OBJECT: + default: + result = objectMapper.createObjectNode(); + break; + } + jsonResponse.set(RESULT_NODE, result); + } + + return result; + } + + /** + * Returns the parameters node of the json request object + * Creates one if necessary + * @return Parameters node + */ + private ObjectNode getParametersNode() { + ObjectNode params; + if (jsonResponse.has(PARAMS_NODE)) { + params = (ObjectNode)jsonResponse.get(PARAMS_NODE); + } else { + params = objectMapper.createObjectNode(); + jsonResponse.set(PARAMS_NODE, params); + } + + return params; + } + + private ObjectNode getDataNode() { + ObjectNode data = null; + if (jsonResponse.has(PARAMS_NODE)) { + ObjectNode params = (ObjectNode)jsonResponse.get(PARAMS_NODE); + if(params.has(DATA_NODE)) { + data = (ObjectNode) params.get(DATA_NODE); + } + } + + if ( data == null ) { + data = objectMapper.createObjectNode(); + ObjectNode params = getParametersNode(); + params.set(DATA_NODE, data); + } + + return data; + } + + protected void setResultToResponse(boolean value) { + jsonResponse.put(RESULT_NODE, value); + } + + protected void setResultToResponse(int value) { + jsonResponse.put(RESULT_NODE, value); + } + + protected void setResultToResponse(String value) { + jsonResponse.put(RESULT_NODE, value); + } + + /** + * Adds the value to the array in node with the given key. + * If the array does not exist it will be created + * and added. + * @param node ObjectNode that should contain an entry with key with an array as value + * @param key the key of the item in ObjectNode that should hold the array + * @param value the value to be added to the array + */ + protected void addToArrayNode(ObjectNode node, String key, String value) { + JsonNode jsonNode = node.get(key); + if (jsonNode == null) { + jsonNode = objectMapper.createArrayNode(); + node.set(key, jsonNode); + } + + if (jsonNode.isArray()) { + ((ArrayNode) jsonNode).add(value); + } else { + LogUtils.LOGE(TAG, "JsonNode at key: " + key + " not of type ArrayNode." ); + } + } + + protected void addToArrayNode(ObjectNode node, String key, ObjectNode value) { + JsonNode jsonNode = node.get(key); + if (jsonNode == null) { + jsonNode = objectMapper.createArrayNode(); + node.set(key, jsonNode); + } + + if (jsonNode.isArray()) { + ((ArrayNode) jsonNode).add(value); + } else { + LogUtils.LOGE(TAG, "JsonNode at key: " + key + " not of type ArrayNode." ); + } + } + + protected void addDataToResponse(String parameter, boolean value) { + getDataNode().put(parameter, value); + } + + protected void addDataToResponse(String parameter, int value) { + getDataNode().put(parameter, value); + } + + protected void addParameterToResponse(String parameter, String value) { + getParametersNode().put(parameter, value); + } + + protected void addMethodToResponse(String method) { + jsonResponse.put(METHOD_NODE, method); + } + + public ObjectNode getResponseNode() { + return jsonResponse; + } + + public String toJsonString() { + return jsonResponse.toString(); + } +} diff --git a/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/methods/Application.java b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/methods/Application.java new file mode 100644 index 0000000..3071da4 --- /dev/null +++ b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/methods/Application.java @@ -0,0 +1,99 @@ +/* + * Copyright 2016 Martijn Brekhof. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.methods; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.JsonResponse; + +/** + * Serverside JSON RPC responses in Application.* + */ +public class Application { + + /** + * JSON response for Application.SetMute request + * + * Example: + * Query: {"jsonrpc":"2.0","method":"Application.SetMute","id":1,"params":{"mute":"toggle"}} + * Answer: muted: {"id":1,"jsonrpc":"2.0","result":false} + * not muted: {"id":1,"jsonrpc":"2.0","result":true} + * + * @return JSON string + */ + public static class SetMute extends JsonResponse { + public final static String METHOD_NAME = "Application.SetMute"; + + public SetMute(int id, boolean muteState) { + super(id); + setResultToResponse(muteState); + } + } + + /** + * JSON response for GetProperties requests + * + * Example: + * Query: {"jsonrpc":"2.0","method":"Application.GetProperties","id":1,"params":{"properties":["muted"]}} + * Answer: {"id":1,"jsonrpc":"2.0","result":{"muted":true}} + * + * @return JSON string + */ + public static class GetProperties extends JsonResponse { + public final static String METHOD_NAME = "Application.GetProperties"; + + public final static String MUTED = "muted"; + public final static String VOLUME = "volume"; + + private ObjectNode node = null; + + public GetProperties(int id) { + super(id); + } + + public void addMuteState(boolean muteState) { + node = (ObjectNode) getResultNode(TYPE.OBJECT); + node.put(MUTED, muteState); + } + + public void addVolume(int volume) { + node = (ObjectNode) getResultNode(TYPE.OBJECT); + node.put(VOLUME, volume); + } + } + + /** + * JSON response for Application.SetVolume request + * + * Examples: + * Query: {"jsonrpc":"2.0","method":"Application.SetVolume","id":1,"params":{"volume":100}} + * Answer: {"id":1,"jsonrpc":"2.0","result":100} + * + * Query: {"jsonrpc":"2.0","method":"Application.SetVolume","id":1,"params":{"volume":"decrement"}} + * Answer: {"id":1,"jsonrpc":"2.0","result":99} + * + * @return JSON string + */ + public static class SetVolume extends JsonResponse { + public final static String METHOD_NAME = "Application.SetVolume"; + + public SetVolume(int id, int volume) { + super(id); + setResultToResponse(volume); + } + } +} diff --git a/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/notifications/Application.java b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/notifications/Application.java new file mode 100644 index 0000000..0d97f17 --- /dev/null +++ b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/notifications/Application.java @@ -0,0 +1,41 @@ +/* + * Copyright 2016 Martijn Brekhof. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.notifications; + +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.JsonResponse; + +public class Application { + /** + * JSON response for Application.OnVolumeChanged notification + * + * Example: + * Answer: {"jsonrpc":"2.0","method":"Application.OnVolumeChanged","params":{"data":{"muted":false,"volume":100},"sender":"xbmc"}} + * + * @return JSON string + */ + public static class OnVolumeChanged extends JsonResponse { + public final static String METHOD_NAME = "Application.OnVolumeChanged"; + + public OnVolumeChanged(boolean muteState, int volume) { + super(); + addMethodToResponse(METHOD_NAME); + addDataToResponse("volume", volume); + addDataToResponse("muted", muteState); + addParameterToResponse("sender", "xbmc"); + } + } +}