diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/exercise/Exercise.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/exercise/Exercise.java index 7beb007..e4a8fb6 100644 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/exercise/Exercise.java +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/exercise/Exercise.java @@ -3,12 +3,13 @@ package com.example.fitbot.exercise; import android.util.Log; import com.example.fitbot.util.path.GesturePath; -import com.example.fitbot.util.server.IWebSocketHandler; -import com.example.fitbot.util.server.WebSocket; +import com.example.fitbot.util.server.IWebServerHandler; +import com.example.fitbot.util.server.WebServer; import java.util.Objects; +import java.util.function.Consumer; -public class Exercise implements IWebSocketHandler { +public class Exercise implements Consumer { private EMuscleGroup muscleGroup; private GesturePath path; @@ -17,7 +18,7 @@ public class Exercise implements IWebSocketHandler { private float segmentsPerSecond; // Static fields. - private static WebSocket webSocket; + private static WebServer webSocket; private static Exercise currentExercise = null; @@ -57,10 +58,9 @@ public class Exercise implements IWebSocketHandler { } try { - webSocket = WebSocket.createServer(); + webSocket = WebServer.createServer(); Objects.requireNonNull(webSocket, "WebSocket server could not be created."); - webSocket.startListening(); webSocket.setEventHandler(this); currentExercise = this; } catch (Exception e) { @@ -126,4 +126,9 @@ public class Exercise implements IWebSocketHandler { public double getSegmentsPerSecond() { return segmentsPerSecond; } + + @Override + public void accept(String message) { + + } } diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/exercise/ExerciseBuilder.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/exercise/ExerciseBuilder.java deleted file mode 100644 index a770475..0000000 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/exercise/ExerciseBuilder.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.example.fitbot.exercise; - -import com.example.fitbot.util.processing.IMotionDataConsumer; -import com.example.fitbot.util.processing.MotionData; -import com.example.fitbot.util.processing.MotionProcessor; -import com.example.fitbot.util.server.IWebSocketHandler; -import com.example.fitbot.util.server.WebSocket; - -import org.joml.Vector3f; - -import java.net.Socket; - -public class ExerciseBuilder implements IWebSocketHandler, IMotionDataConsumer { - - private MotionProcessor processor; - - public ExerciseBuilder() { - this.processor = new MotionProcessor(); - this.processor.setMotionDataEventHandler(this); - } - - @Override - public void onDisconnected(Socket socket) { - IWebSocketHandler.super.onDisconnected(socket); - } - - @Override - public void onMessageReceived(WebSocket.Message message, WebSocket.MessageReply replier) { - IWebSocketHandler.super.onMessageReceived(message, replier); - } - - @Override - public void onError(Socket socket, String error) { - IWebSocketHandler.super.onError(socket, error); - } - - @Override - public void accept(Vector3f transformedVector, MotionData motionData, int sampleIndex, double sampleRate, int sensorId) { - - } -} diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/speech/SpeechGenerator.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/speech/SpeechGenerator.java deleted file mode 100644 index c731a72..0000000 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/speech/SpeechGenerator.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.example.fitbot.speech; - -import com.aldebaran.qi.sdk.QiContext; -import com.aldebaran.qi.sdk.builder.SayBuilder; -import com.aldebaran.qi.sdk.object.locale.Language; -import com.aldebaran.qi.sdk.object.locale.Locale; -import com.aldebaran.qi.sdk.object.locale.Region; - -/** - * SpeechGenerator class for generating speech for the robot - */ -public class SpeechGenerator { - - private static final Locale DUTCH_LOCALE = new Locale(Language.DUTCH, Region.NETHERLANDS); - private SayBuilder builder; - - /** - * Function for making the robot say something with DUTCH_LOCALE as locale - * @param phrase The phrase to make the robot say - * @param ctx The QiContext to use - */ - public static void say(String phrase, QiContext ctx) - { - say(phrase, ctx, DUTCH_LOCALE); - } - - /** - * Function for making the robot say something with a specific locale - * @param phrase The phrase to make the robot say - * @param ctx The QiContext to use - * @param locale The locale to use - */ - public static void say(String phrase, QiContext ctx, Locale locale) - { - SayBuilder - .with(ctx) - .withLocale(locale) - .withText(phrase) - .build() - .run(); - } - -} diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/FitnessActivity.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/FitnessActivity.java index 13f0152..354f04c 100644 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/FitnessActivity.java +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/FitnessActivity.java @@ -1,7 +1,6 @@ package com.example.fitbot.ui.activities; import android.os.Bundle; -import android.os.Handler; import android.util.Log; import android.widget.VideoView; @@ -11,16 +10,17 @@ import com.aldebaran.qi.sdk.RobotLifecycleCallbacks; import com.aldebaran.qi.sdk.design.activity.RobotActivity; import com.aldebaran.qi.sdk.design.activity.conversationstatus.SpeechBarDisplayStrategy; import com.example.fitbot.R; -import com.example.fitbot.sports.FitnessCycle; import com.example.fitbot.exercise.EMuscleGroup; import com.example.fitbot.exercise.Exercise; import com.example.fitbot.ui.components.PersonalMotionPreviewElement; -import com.example.fitbot.util.Animations; import com.example.fitbot.util.ButtonNavigation; +import com.example.fitbot.util.FitnessCycle; import com.example.fitbot.util.path.GesturePath; import org.joml.Vector3f; +import java.util.concurrent.CompletableFuture; + public class FitnessActivity extends RobotActivity implements RobotLifecycleCallbacks { PersonalMotionPreviewElement personalMotionPreviewElement; @@ -71,11 +71,9 @@ public class FitnessActivity extends RobotActivity implements RobotLifecycleCall public void onRobotFocusGained(QiContext qiContext) { // Find the VideoView by its ID - FitnessCycle.RobotMovement("bicepcurl", 10, qiContext); - -// FitnessCycle.playVideo(qiContext, videoView, this); - + CompletableFuture.runAsync(() -> FitnessCycle.executeMovement("bicepcurl", 10, qiContext)); personalMotionPreviewElement.provideQiContext(qiContext); + // FitnessCycle.playVideo(qiContext, videoView, this); } @Override @@ -88,14 +86,8 @@ public class FitnessActivity extends RobotActivity implements RobotLifecycleCall // Implement your logic when the robot focus is refused } - private Handler handler; - private Runnable runnable; - @Override protected void onDestroy() { super.onDestroy(); - if (handler != null && runnable != null) { - handler.removeCallbacks(runnable); - } } } \ No newline at end of file diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/components/PersonalMotionPreviewElement.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/components/PersonalMotionPreviewElement.java index 2708b01..8eb1234 100644 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/components/PersonalMotionPreviewElement.java +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/components/PersonalMotionPreviewElement.java @@ -2,6 +2,7 @@ package com.example.fitbot.ui.components; import android.content.Context; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.util.AttributeSet; @@ -10,7 +11,7 @@ import android.view.View; import com.aldebaran.qi.sdk.QiContext; import com.example.fitbot.exercise.Exercise; -import com.example.fitbot.speech.SpeechGenerator; +import com.example.fitbot.util.FitnessCycle; import com.example.fitbot.util.path.GesturePath; import com.example.fitbot.util.path.PathSegment; import com.example.fitbot.util.processing.MotionData; @@ -24,20 +25,22 @@ import org.joml.Vector4f; public class PersonalMotionPreviewElement extends View { private GesturePath path; - private double pathTime = 0.0D; // The timestamp at which the path is currently at. private MotionProcessor motionProcessor; + private double pathTime = 0.0D; // The timestamp at which the path is currently at. + private double exerciseProgress = 0.0D; // The progress of the exercise. Ranges from 0 to 1. + private QiContext qiContext; private Exercise exercise; - private Path referencePath; // The path the user is supposed to follow. - private Path performingPath; // The path the user is currently following. - private Path stickmanPath; // The path of the stickman that is drawn on the screen. + private Path targetPath; // The path the user is supposed to follow. + private Path actualPath; // The path the user is currently following. + + private final Paint referencePaint = new Paint(); + private final Paint targetPaint = new Paint(); + private final Paint backgroundColor = new Paint(); - private Paint referencePaint; - private Paint performingPaint; - private Paint textPaint; private static final String[] USER_PHRASES = { "Veel success met de oefening!", @@ -45,76 +48,24 @@ public class PersonalMotionPreviewElement extends View { "Veel plezier!" }; - // Matrices for the projection of the path segments onto the screen. - // Depth buffering sadly is not supported yet due to brain dysfunction - private Matrix4f modelViewMatrix = new Matrix4f(); - private Matrix4f projectionMatrix = new Matrix4f(); - private double timePassed = 0.0D; // The time that has passed since the start of the exercise, in seconds. private long startingTime = 0L; - private Paint backgroundColor = new Paint(); - - /** - * Constants for the preview path projection. - */ - private final float FOV = 80.0f; // The field of view of the preview path - private final float Z_NEAR = 0.1f; // The near clipping plane - private final float Z_FAR = 1000.0f; // The far clipping plane - private Vector3f objectPosition = new Vector3f(0.0f, 0.0f, -4.0f); // The position of the camera private Vector2f screenDimensions = new Vector2f(); // Width and height dimensions of the screen - private Vector2f rotation = new Vector2f(); // Rotation vector (yaw, pitch) public PersonalMotionPreviewElement(Context context, AttributeSet attrs) { super(context, attrs); - this.referencePaint = new Paint(); this.referencePaint.setColor(0xFFFF0000); // Red - this.referencePaint.setStyle(Paint.Style.STROKE); + this.referencePaint.setStyle(Paint.Style.FILL); this.referencePaint.setStrokeWidth(5.0f); + this.referencePaint.setAntiAlias(true); - this.performingPaint = new Paint(); - this.performingPaint.setColor(0xFF0000FF); // Blue - this.performingPaint.setStyle(Paint.Style.STROKE); - this.performingPaint.setStrokeWidth(5.0f); - - this.textPaint = new Paint(); - this.textPaint.setColor(-1); - this.textPaint.setStyle(Paint.Style.FILL); - this.textPaint.setTextSize(50.0f); - } - - /** - * Method for updating the stickman gestures. - * - * This method will update the stickman gestures based on the current - * motion data that is being processed. - */ - private void updateStickmanGestures() { - // Reset previous path - stickmanPath.reset(); - - // TODO: Define all arm segments: - // - Upper left and right arm - // - Lower left and right arm - // - Upper left and right leg - // - Lower left and right leg - // Update all segments based on the perceived motion data. - PathSegment upperLeftArm = new PathSegment( - new Vector3f(), - new Vector3f() - ); - - PathSegment[] bodySegments = new PathSegment[] { - new PathSegment(new Vector3f(0.0f, -.5f, -.5f), new Vector3f(0, 0, 0)), // Left leg - new PathSegment(new Vector3f(0.0f, -.5f, .5f), new Vector3f(0, 0, 0)), // Right leg - new PathSegment(new Vector3f(0.0f, .5f, 0.0f), new Vector3f(0, 0, 0)), // Body - new PathSegment(new Vector3f(-.25f, .25f, 0f), new Vector3f(0, 0, 0)), // Left arm - new PathSegment(new Vector3f(.25f, .25f, 0f), new Vector3f(0, 0, 0)) // Right arm - }; - - // TODO: Generate new path for stickman - + // Target paint is the filling of the target path. + this.targetPaint.setColor(-1); + this.targetPaint.setStyle(Paint.Style.STROKE); + this.targetPaint.setStrokeWidth(5.0f); + this.targetPaint.setAntiAlias(true); } /** @@ -127,13 +78,12 @@ public class PersonalMotionPreviewElement extends View { */ public void initialize(Exercise exercise) { Log.i("PersonalMotionPreviewElement", "Creating new PersonalMotionPreviewElement."); - this.backgroundColor = new Paint(); this.backgroundColor.setColor(0xFF000000); // Black this.screenDimensions.x = this.getWidth(); this.screenDimensions.y = this.getHeight(); - this.performingPath = new Path(); - this.referencePath = new Path(); + this.actualPath = new Path(); + this.targetPath = new Path(); this.startingTime = System.nanoTime(); // Set the last time to the current time @@ -148,6 +98,9 @@ public class PersonalMotionPreviewElement extends View { /** * Function for providing a QiContext to the PersonalMotionPreviewElement. + * This function will be called by the parent activity when the QiContext is available. + * Also say something nice to the user :) + * * @param context The QiContext to provide. */ public void provideQiContext(QiContext context) { @@ -163,90 +116,16 @@ public class PersonalMotionPreviewElement extends View { if (this.qiContext == null) return; - SpeechGenerator.say(USER_PHRASES[(int) Math.floor(Math.random() * USER_PHRASES.length)], this.qiContext); - } - - /** - * Method that calculates the path that will be drawn on the - * canvas. This method will be called every time new motion data is received. - */ - private void calculateDrawingPath(Vector3f transformedVector, MotionData motionData, int sampleIndex, double sampleRate) { - // Recalculate the personal path based on the new motion data - // TODO: Implement + FitnessCycle.say(USER_PHRASES[(int) Math.floor(Math.random() * USER_PHRASES.length)], this.qiContext); } /** * Method for setting the gesture path that will be drawn on the canvas. * - * @param path The gesture path to draw. + * @param exercise The exercise that the user is currently performing. */ - public void setGesturePath(GesturePath path) { - this.path = path; - this.referencePath = getDrawablePath(path.getSegments()); - } - - /** - * Method for projecting a 3D point onto the screen. - * This method converts the 3D point to 2D space using a Model-View-Projection matrix transformation. - * - * @param point The point to cast to the screen. - * @param virtualWidth The width of the virtual screen. - * This is used to normalize the screen coordinates. - * @param virtualHeight The height of the virtual screen. - * @return The transformed vector in screen coordinates ranging from (0, 0) to (virtualWidth, virtualHeight). - */ - private Vector2f projectVertex(Vector3f point, int virtualWidth, int virtualHeight) { - - modelViewMatrix - .identity() - .translate(-objectPosition.x, -objectPosition.y, -objectPosition.z) - .rotateX((float) Math.toRadians(rotation.y)) - .rotateY((float) Math.toRadians(rotation.x)); - - // Transform the projection matrix to a perspective projection matrix - // Perspective transformation conserves the depth of the object - projectionMatrix - .identity() - .perspective((float) Math.toRadians(FOV), (float) virtualWidth / virtualHeight, Z_NEAR, Z_FAR); - - // Convert world coordinates to screen-space using MVP matrix - Vector4f screenCoordinates = new Vector4f(point, 1.0f) - .mul(this.modelViewMatrix) - .mul(this.projectionMatrix); - - // Normalize screen coordinates from (-1, 1) to (0, virtualWidth) and (0, virtualHeight) - float normalizedX = (screenCoordinates.x / screenCoordinates.w + 1.0f) * 0.5f * virtualWidth; - float normalizedY = (1.0f - screenCoordinates.y / screenCoordinates.w) * 0.5f * virtualHeight; - Log.i("VertexProjection", "Projected vertex to screen coordinates: (" + normalizedX + ", " + normalizedY + ")."); - return new Vector2f(normalizedX, normalizedY); - } - - /** - * Method that converts a sequence of vectors to a Path object. - * This path is a set of bezier curves that will be drawn on the canvas. - * - * @param segments The path segments in the path. - * These segments will be connected by bezier curves, which - * all have unique curvature values. - * @return The generated path object. - */ - private Path getDrawablePath(PathSegment... segments) { - - Path calculatedPath = new Path(); - - // Starting point - Vector2f origin = projectVertex(segments[0].getStart(), getWidth(), getHeight()); - calculatedPath.moveTo(origin.x, origin.y); - - // Draw the path segments - for (PathSegment segment : segments) { - Vector2f startProjected = projectVertex(segment.getStart(), getWidth(), getHeight()); - Vector2f endProjected = projectVertex(segment.getEnd(), getWidth(), getHeight()); - calculatedPath.lineTo(startProjected.x, startProjected.y); - calculatedPath.lineTo(endProjected.x, endProjected.y); - } - - return calculatedPath; + public void setExercise(Exercise exercise) { + this.exercise = exercise; } @@ -256,14 +135,23 @@ public class PersonalMotionPreviewElement extends View { this.setBackgroundColor(0xFF000000); // Black if (this.exercise == null) return; - // Draw the sport preview canvas - canvas.drawPath(referencePath, referencePaint); - canvas.drawPath(performingPath, performingPaint); + + // Draw target circle + float targetRadius = (this.screenDimensions.x + this.screenDimensions.y) / 5.0f; + canvas.drawCircle(this.screenDimensions.x / 2, this.screenDimensions.y / 2, targetRadius, this.targetPaint); + canvas.drawCircle(this.screenDimensions.x / 2, this.screenDimensions.y / 2, (float)(targetRadius * exerciseProgress), this.referencePaint); + referencePaint.setColor( + Color.argb( + 255, + (int)(255 * (1.0 - exerciseProgress)), + (int)(255 * exerciseProgress), + 0 + ) + ); timePassed = (System.nanoTime() - startingTime) / 1E9D; + this.exerciseProgress = timePassed*.4 % 1.0d; - this.rotation.x = (float) (Math.sin(timePassed) * 45); - this.referencePath = getDrawablePath(this.path.getSegments()); this.invalidate(); // Causes a redraw. } } diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/FitnessCycle.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/FitnessCycle.java index 133454e..a55b445 100644 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/FitnessCycle.java +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/FitnessCycle.java @@ -1,13 +1,15 @@ -package com.example.fitbot.sports; - -import static com.example.fitbot.sports.Animations.PlayAnimation; +package com.example.fitbot.util; import android.content.Context; import android.net.Uri; -import android.os.Handler; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.widget.VideoView; + +import com.aldebaran.qi.sdk.builder.SayBuilder; +import com.aldebaran.qi.sdk.object.locale.Language; +import com.aldebaran.qi.sdk.object.locale.Locale; +import com.aldebaran.qi.sdk.object.locale.Region; import com.example.fitbot.R; import com.aldebaran.qi.sdk.QiContext; import com.aldebaran.qi.sdk.builder.AnimateBuilder; @@ -19,7 +21,18 @@ import java.util.concurrent.atomic.AtomicInteger; public class FitnessCycle extends AppCompatActivity { - public static void RobotMovement(String Exercise, int Reps, QiContext qiContext) { + private static final Locale DUTCH_LOCALE = new Locale(Language.DUTCH, Region.NETHERLANDS); + + + /** + * Function for executing a movement animation a certain number of times + * on the robot + * + * @param Exercise The name of the exercise to perform + * @param Reps The number of repetitions to perform + * @param qiContext The QiContext to use + */ + public static void executeMovement(String Exercise, int Reps, QiContext qiContext) { AtomicInteger repCount = new AtomicInteger(0); Animation animation = AnimationBuilder.with(qiContext) @@ -44,6 +57,38 @@ public class FitnessCycle extends AppCompatActivity { } } + /** + * Function for making the robot say something with DUTCH_LOCALE as locale + * @param phrase The phrase to make the robot say + * @param ctx The QiContext to use + */ + public static void say(String phrase, QiContext ctx) + { + say(phrase, ctx, DUTCH_LOCALE); + } + + /** + * Function for making the robot say something with a specific locale + * @param phrase The phrase to make the robot say + * @param ctx The QiContext to use + * @param locale The locale to use + */ + public static void say(String phrase, QiContext ctx, Locale locale) + { + SayBuilder + .with(ctx) + .withLocale(locale) + .withText(phrase) + .build() + .run(); + } + + /** + * Function for playing a video in a VideoView + * + * @param videoView The VideoView to play the video in + * @param context The context to use + */ public static void playVideo(VideoView videoView, Context context) { // Set up the video player if (videoView != null) { diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/MotionProcessor.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/MotionProcessor.java index b5d2b2b..5703c6e 100644 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/MotionProcessor.java +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/MotionProcessor.java @@ -3,8 +3,8 @@ package com.example.fitbot.util.processing; import android.util.Log; import com.example.fitbot.util.path.GesturePath; -import com.example.fitbot.util.server.IWebSocketHandler; -import com.example.fitbot.util.server.WebSocket; +import com.example.fitbot.util.server.IWebServerHandler; +import com.example.fitbot.util.server.WebServer; import org.jetbrains.annotations.NotNull; import org.joml.Vector3f; @@ -24,7 +24,7 @@ public class MotionProcessor { private float sampleRate = 1.0F; // samples/second private IMotionDataConsumer motionDataConsumer = (p1, p2, p3, p4, p5) -> {}; private GesturePath path; - private WebSocket socket; + private WebServer server; public MotionProcessor() {} @@ -37,20 +37,14 @@ public class MotionProcessor { */ public void startListening() { // Create socket server - this.socket = WebSocket.createServer(); + this.server = WebServer.createServer(); Log.i("MotionProcessor", "Listening for incoming connections."); // Check if the socket - if (socket != null) { + if (server != null) { // Update event handler to match our functionality. - socket.setEventHandler(new IWebSocketHandler() { - @Override - public void onMessageReceived(WebSocket.Message message, WebSocket.MessageReply replier) { - parsePacket(message.message); - } - }); - socket.startListening(); + server.setEventHandler(this::parsePacket); } } @@ -60,8 +54,8 @@ public class MotionProcessor { * the WebSocket server. */ public void stopListening() { - if (socket != null) { - socket.stop(); + if (server != null) { + server.stop(); } } diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/server/IWebServerHandler.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/server/IWebServerHandler.java new file mode 100644 index 0000000..98739b4 --- /dev/null +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/server/IWebServerHandler.java @@ -0,0 +1,11 @@ +package com.example.fitbot.util.server; + +import java.net.Socket; + +/** + * Interface for handling WebSocket events. + */ +public interface IWebServerHandler { + + void onReceive(String body); +} diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/server/IWebSocketHandler.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/server/IWebSocketHandler.java deleted file mode 100644 index fa4cc98..0000000 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/server/IWebSocketHandler.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.fitbot.util.server; - -import java.net.Socket; - -/** - * Interface for handling WebSocket events. - */ -public interface IWebSocketHandler { - - // Function for handling the connection of the WebSocket. - default void onConnected(Socket socket) {} - - default void onDisconnected(Socket socket) {} - - default void onMessageReceived(WebSocket.Message message, WebSocket.MessageReply replier) {} - - default void onError(Socket socket, String error) {} -} diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/server/WebServer.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/server/WebServer.java new file mode 100644 index 0000000..f3462f7 --- /dev/null +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/server/WebServer.java @@ -0,0 +1,129 @@ +package com.example.fitbot.util.server; + +import android.util.Log; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +public class WebServer implements Runnable { + + private ServerSocket serverSocket; + protected Consumer eventHandler = (input) -> {}; // No-op. + + private Thread thread; + private AtomicBoolean forceClose = new AtomicBoolean(false); + + /** + * Constructor for creating a new WebSocket server. + */ + private WebServer() { + + } + + /** + * Function for creating a new WebSocket server given the provided port. + * + * @return A WebSocket connection, or null if something went wrong. + */ + public static WebServer createServer() { + try { + WebServer server = new WebServer(); + server.serverSocket = new ServerSocket(); + server.serverSocket.bind(server.serverSocket.getLocalSocketAddress()); + server.serverSocket.setSoTimeout(0); + + Log.i("WebServer -- Creating new Web Server", "Server created: " + server.serverSocket.getLocalSocketAddress() + ", " + server.serverSocket.getLocalPort()); + + server.thread = new Thread(server); + server.thread.start(); + + return server; + } catch (IOException error) { + String cause = error.getMessage() == null ? "Unknown reason" : error.getMessage(); + Log.e("WebServer -- Creating new Web Server", cause); + return null; + } + } + + @Override + public void run() { + // Listen for new connections until the socket closes. + while (this.isConnected() && !this.forceClose.get()) { + try { + // Find a new connection + Socket newSocket = this.serverSocket.accept(); + InputStream streamIn = newSocket.getInputStream(); + + // Read the incoming data + BufferedReader reader = new BufferedReader(new InputStreamReader(streamIn)); + StringBuilder builder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) + builder.append(line); + + newSocket.close(); + + this.eventHandler.accept(builder.toString()); + + } catch (IOException error) { + String reason = error.getMessage() == null ? "Unknown reason" : error.getMessage(); + Log.e("WebServerConnectionHandler", "Error listening to Socket connections: " + reason); + break; + } + } + } + + /** + * Method for stopping the WebSocket server. + */ + public void stop() { + try { + this.serverSocket.close(); + this.forceClose.set(true); + } catch (IOException error) { + String cause = error.getMessage() == null ? "Unknown reason" : error.getMessage(); + Log.e("WebSocket -- Closing server connection", cause); + } + } + + /** + * Method for setting the event handler for this WebSocket server. + * + * @param handler The handler to use. This handler will parse all events + * that occur in this WebSocket connection. The events are the followed: + * - onMessageReceived(Socket, String) + * - onConnected(Socket) + * - onDisconnected(Socket) + * - onError(Socket, String) + */ + public void setEventHandler(Consumer handler) { + this.eventHandler = handler; + } + + /** + * Method for getting the ServerSocket connection + * + * @return The ServerSocket connection. + */ + public ServerSocket getSocket() { + return this.serverSocket; + } + + /** + * Method for checking whether this WebSocket connection is connected. + * + * @return The connection status of the WebSocket. + */ + public boolean isConnected() { + return !this.serverSocket.isClosed(); + } +} diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/server/WebSocket.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/server/WebSocket.java deleted file mode 100644 index 60fbf6b..0000000 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/server/WebSocket.java +++ /dev/null @@ -1,150 +0,0 @@ -package com.example.fitbot.util.server; - -import android.support.annotation.Nullable; -import android.util.Log; - -import java.io.IOException; -import java.net.ServerSocket; -import java.net.Socket; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -public class WebSocket { - - private ServerSocket serverSocket; - private WebSocketConnectionHandler connectionHandler; - private final Set clients = Collections.synchronizedSet(new HashSet<>()); - protected IWebSocketHandler eventHandler = new IWebSocketHandler() {}; // NO-OP event handler. - - /** - * Constructor for creating a new WebSocket server. - */ - private WebSocket() {} - - /** - * Function for creating a new WebSocket server given the provided port. - * @return A WebSocket connection, or null if something went wrong. - */ - public static @Nullable WebSocket createServer() { - try { - WebSocket webSocket = new WebSocket(); - webSocket.serverSocket = new ServerSocket(); - webSocket.serverSocket.bind(webSocket.serverSocket.getLocalSocketAddress()); - Log.i("WebSocket -- Creating new WebSocket server", "Server created: " + webSocket.serverSocket.getLocalSocketAddress() + ", " + webSocket.serverSocket.getLocalPort()); - return webSocket; - } catch (IOException error) - { - String cause = error.getMessage() == null ? "Unknown reason" : error.getMessage(); - Log.e("WebSocket -- Creating new WebSocket server", cause); - return null; - } - } - - /** - * Method for listening for incoming connections. - */ - public void startListening() { - this.connectionHandler = new WebSocketConnectionHandler(this); - this.connectionHandler.listen(); - } - - /** - * Method for stopping the WebSocket server. - */ - public void stop() { - try { - this.serverSocket.close(); - this.connectionHandler.stop(); - } catch (IOException error) { - String cause = error.getMessage() == null ? "Unknown reason" : error.getMessage(); - Log.e("WebSocket -- Closing server connection", cause); - } - } - - /** - * Method for setting the event handler for this WebSocket server. - * @param handler The handler to use. This handler will parse all events - * that occur in this WebSocket connection. The events are the followed: - * - onMessageReceived(Socket, String) - * - onConnected(Socket) - * - onDisconnected(Socket) - * - onError(Socket, String) - */ - public void setEventHandler(IWebSocketHandler handler) { - this.eventHandler = handler; - } - - /** - * Method for getting the ServerSocket connection - * @return The ServerSocket connection. - */ - public ServerSocket getSocket() { - return this.serverSocket; - } - - /** - * Method for checking whether this WebSocket connection is connected. - * @return The connection status of the WebSocket. - */ - public boolean isConnected() { - return !this.serverSocket.isClosed(); - } - - /** - * Class representing a message received from a WebSocket connection. - */ - public static class Message { - - // Enumerable representing message type (opcode). - public enum Opcode { - - CONTINUING((byte) 0x0), - TEXT((byte) 0x1), - BINARY((byte) 0x2), - RES0((byte) 0x3), RES1((byte) 0x4), RES2((byte) 0x5), RES3((byte) 0x6), RES4((byte) 0x7), - CLOSE_CONNECTION((byte) 0x8), - PING((byte) 0x9), - PONG((byte) 0xA), - RES5((byte) 0xB), RES6((byte) 0xC), RES7((byte) 0xD), RES8((byte) 0xE), RES9((byte) 0xF); - - byte opcode; - Opcode(final byte opcode) { - this.opcode = opcode; - } - - /** - * Method for decoding the opcode of a message. - * @param opcode The opcode to decode. - * @return The message type. - */ - public static Opcode decode(byte opcode) { - return Opcode.values()[opcode & 0xF]; - } - // Returns the opcode of this message type. - public byte getOpcode() { return this.opcode; } - } - - public String message; - public WebSocketConnection connection; - - /** - * Constructor for a WebSocket message. - * @param message The message that was sent - * @param connection The connection where the message came from. - */ - public Message(WebSocketConnection connection, String message) { - this.message = message; - this.connection = connection; - } - } - - /** - * Interface for a message reply. - * This can be used for when a message has been received from a client - * to reply back to the client. - */ - public interface MessageReply { - void reply(String message); - } -} diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/server/WebSocketConnection.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/server/WebSocketConnection.java deleted file mode 100644 index 60e0ae1..0000000 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/server/WebSocketConnection.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.example.fitbot.util.server; - -import java.net.Socket; - -public class WebSocketConnection { - - private final WebSocket origin; - private final Socket socket; - - /** - * Constructor for creating an arbitrary WebSocket connection (Client) - * @param connection The server connection - * @param socket The client socket - */ - public WebSocketConnection(WebSocket connection, Socket socket) { - this.origin = connection; - this.socket = socket; - } - - /** - * Getter method for retrieving the WebSocket connection - * @return The WebSocket instance. - */ - public WebSocket getOrigin() { - return origin; - } - - /** - * Getter method for retrieving the Client Socket connection. - * @return The Socket connection. - */ - public Socket getSocket() { - return socket; - } -} diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/server/WebSocketConnectionHandler.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/server/WebSocketConnectionHandler.java deleted file mode 100644 index 7dc0329..0000000 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/server/WebSocketConnectionHandler.java +++ /dev/null @@ -1,217 +0,0 @@ -package com.example.fitbot.util.server; - -import android.util.Log; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.Socket; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.Base64; -import java.util.Collections; -import java.util.List; -import java.util.Scanner; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class WebSocketConnectionHandler implements Runnable { - - private final WebSocket theSocket; - private List clients = Collections.synchronizedList(new ArrayList<>()); - private Thread thread; - private boolean forceClose = false; - - /** - * Constructor for WebSocketConnectionHandler. - * This class handles all new incoming Socket connections. - * - * @param webSocket The socket to check for new connections. - */ - protected WebSocketConnectionHandler(WebSocket webSocket) { - this.theSocket = webSocket; - } - - @Override - public void run() { - // Listen for new connections until the socket closes. - while (theSocket.isConnected()) { - try { - // Find a new connection - Socket newSocket = this.theSocket.getSocket().accept(); - this.theSocket.eventHandler.onConnected(newSocket); - clients.add(newSocket); - - InputStream streamIn = newSocket.getInputStream(); - OutputStream streamOut = newSocket.getOutputStream(); - - // Check if the connection was successfully upgraded to WebSocket - if (upgradeConnection(streamIn, streamOut)) { - applyMessageDecoder(streamIn); - } - } catch (IOException error) { - String reason = error.getMessage() == null ? "Unknown reason" : error.getMessage(); - Log.e("WebSocketConnectionHandler", "Error listening to Socket connections: " + reason); - break; - } - } - } - - /** - * Method for upgrading a HTTP connection to a WebSocket connection. - * This checks whether the client sent a GET header and sends back - * the required headers to upgrade the connection. - * @param streamIn The InputStream of the client socket connection. - * @param streamOut The OutputStream of the client socket connection. - * @return Whether or not the connection was successfully upgraded. - */ - private boolean upgradeConnection(InputStream streamIn, OutputStream streamOut) { - Scanner scanner = new Scanner(streamIn, "UTF-8"); - String data = scanner.useDelimiter("\\r\\n\\r\\n").next(); - Matcher header = Pattern.compile("^GET").matcher(data); - - // Check if the header contains the GET keyword - // If this is the case, upgrade the HTTP connection to WebSocket. - if (!header.find()) - return false; - - Matcher match = Pattern.compile("Sec-WebSocket-Key: (.*)").matcher(data); - match.find(); // Get next match - - try { - String SECAccept = Base64.getEncoder().encodeToString( - MessageDigest.getInstance("SHA-1").digest((match.group(1) + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").getBytes(StandardCharsets.UTF_8))); - - byte[] response = ( - "HTTP/1.1 101 Switching Protocols\r\n" + - "Connection: Upgrade\r\n" + - "Upgrade: websocket\r\n" + - "Sec-WebSocket-Accept: " + - SECAccept + "\r\n\r\n").getBytes(StandardCharsets.UTF_8); - streamOut.write(response, 0, response.length); - - } catch (IOException | NoSuchAlgorithmException error) { - Log.e("WebSocketConnectionHandler", "Failed upgrading HTTP to WebSocket connection" + error.getMessage()); - return false; - } - return true; - } - - /** - * Method for applying a message decoder for whenever a socket receives data. - * This method attemps to decode a message received from a WebSocket connection. - * This message is in the - * @param streamIn The message stream to decode. - */ - private void applyMessageDecoder(InputStream streamIn) { - // TODO: Implement - } - - /** - * Method for decoding an encoded WebSocket message - * @param bytes The message to decode, in UTF-8 format. - * @return The decoded message. - * @throws IllegalArgumentException When the `frame` content is in an incorrect format. - */ - public static String decodeWebSocketMessage(byte[] bytes) { - // Check if the packet isn't corrupted - if (bytes.length <= 2 || (bytes[0] & 0b1110) != 0) - throw new IllegalArgumentException("Attempted to decode corrupted WebSocket frame data"); - - WebSocket.Message.Opcode opcode = WebSocket.Message.Opcode.decode((byte) (bytes[0] & 0b11110000)); - byte payloadLength = (byte) (bytes[1] & 0b01111111); // Payload size (7 bits) - boolean fin = (bytes[0] & 0b1) != 0; // Whether this is the whole message - boolean masked = (bytes[1] & 0b10000000) != 0; // Whether the 9th bit is masked - - long extendedPayloadLength = 0; - int byteOffset = 2; - - // Check whether the payload length is 16-bit - if (payloadLength == 126) { - // 16-bit extended payload length (byte 2 and 3) - extendedPayloadLength = ((bytes[2] & 0xFF) << 8) | (bytes[3] & 0xFF); - byteOffset += 2; - - // Check whether payload length is 64-bit - } else if (payloadLength == 127) { - // 64-bit extended payload length - for (int i = 0; i < 8; i++) - extendedPayloadLength |= (long) (bytes[2 + i] & 0xFF) << ((7 - i) * 8); - byteOffset += 8; - - } else { - extendedPayloadLength = payloadLength; - } - - byte[] maskingKey = null; - byte[] payloadData = new byte[(int) extendedPayloadLength]; - - - // Check if the MASK bit was set, if so, copy the key to the `maskingKey` array. - if (masked) { - maskingKey = new byte[4]; - System.arraycopy(bytes, byteOffset, maskingKey, 0, 4); // move mask bytes - byteOffset += 4; - } - - // Copy payload bytes into `payloadData` array. - System.arraycopy(bytes, byteOffset, payloadData, 0, payloadData.length); - - // If mask is present, decode the payload data with the mask. - if (masked) - for (int i = 0; i < payloadData.length; i++) - payloadData[i] ^= maskingKey[i % 4]; - - // Convert payload data to string - return new String(payloadData, StandardCharsets.UTF_8); - } - - /** - * Method for checking whether the connection handler is actively listening. - * @return Whether it's listening. - */ - public boolean isActive() { - return this.thread.isAlive(); - } - - /** - * Method for listening to all new incoming socket connections. - */ - public void listen() { - this.thread = new Thread(this); - this.thread.start(); - Log.i("WebSocketConnectionHandler", "Listening started."); - } - - /** - * Method for stopping the connection handler. - */ - public void stop() { - // Close the socket connection if not already closed - if (!this.theSocket.getSocket().isClosed()) { - try { - this.theSocket.getSocket().close(); - } catch (IOException error) { - Log.e("WebSocketConnectionHandler", "Failed to close the socket connection: " + error.getMessage()); - } - } - - // Interrupt the thread - this.thread.interrupt(); - - // Close all connections - this.clients.forEach(client -> { - try { - client.close(); - } catch (IOException error) { - Log.e("WebSocketConnectionHandler", "Failed to close client: " + error.getMessage()); - } - }); - this.clients.clear(); - - - Log.i("WebSocketConnectionHandler", "Listening stopped."); - } -} diff --git a/code/src/Fitbot/app/src/test/java/com/example/fitbot/WebSocketMessageParsingTest.java b/code/src/Fitbot/app/src/test/java/com/example/fitbot/WebSocketMessageParsingTest.java deleted file mode 100644 index 87f6beb..0000000 --- a/code/src/Fitbot/app/src/test/java/com/example/fitbot/WebSocketMessageParsingTest.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.example.fitbot; - - -import static org.junit.Assert.assertEquals; - -import com.example.fitbot.util.server.WebSocketConnectionHandler; - -import org.junit.Test; - -/** - * Created on 07/05/2024 at 18:27 - * by Luca Warmenhoven. - */ -public class WebSocketMessageParsingTest { - - - - @Test - public void parseWebSocketMessage() { - - String reference = "abcdef"; - final byte[] encoded = { - (byte) 129, (byte) 134, (byte) 167, - (byte) 225, (byte) 225, (byte) 210, - (byte) 198, (byte) 131, (byte) 130, - (byte) 182, (byte) 194, (byte) 135 - }; - String decoded = ""; - try { - decoded = WebSocketConnectionHandler.decodeWebSocketMessage(encoded); - } catch (Exception e) { - System.err.println("Error occurred whilst attempting to parse input" + e.getMessage()); - } - assertEquals(reference, decoded); - } -}