diff --git a/.vscode/settings.json b/.vscode/settings.json index c5f3f6b..0e14d8e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "java.configuration.updateBuildConfiguration": "interactive" + "java.configuration.updateBuildConfiguration": "disabled" } \ No newline at end of file diff --git a/code/arduino/Movement-sensor-code/Connectivity.cpp b/code/arduino/Movement-sensor-code/Connectivity.cpp index a1c30c4..bfc3c04 100644 --- a/code/arduino/Movement-sensor-code/Connectivity.cpp +++ b/code/arduino/Movement-sensor-code/Connectivity.cpp @@ -4,29 +4,53 @@ void Connectivity::connectWiFi(char* ssid, char* pass){ WiFi.mode(WIFI_STA); WiFi.begin(ssid, pass); while (WiFi.status() != WL_CONNECTED) { - Serial.println("connecting to wifi"); delay(1000); } - Serial.println(WiFi.localIP()); } -void Connectivity::websocketSetup(char* ip, uint16_t port, char* adress){ - //ws server address, port and URL - webSocket.begin(ip , port, adress); - // try every 500 again if connection has failed - webSocket.setReconnectInterval(500); -} +// void Connectivity::websocketSetup(char* ip, uint16_t port, char* adress){ +// //ws server address, port and URL +// webSocket.begin(ip , port, adress); +// // try every 500 again if connection has failed +// webSocket.setReconnectInterval(500); +// } -void Connectivity::sendData(float roll, float pitch, float yaw){ - String message = "{\"Sensor\": 1, \"roll\":\"" + String(roll) + "\",\"pitch\":\"" + String(pitch) + "\",\"yaw\":\"" + String(yaw) + "\"}"; - webSocket.sendTXT(message); +// void Connectivity::sendData(float roll, float pitch, float yaw){ +// String message = "{\"Sensor\": 1, \"roll\":\"" + String(roll) + "\",\"pitch\":\"" + String(pitch) + "\",\"yaw\":\"" + String(yaw) + "\"}"; +// webSocket.sendTXT(message); +// } + +const char* getServerURL = "http://145.92.8.132:443/get-ip"; +String ipAddress = ""; + +String Connectivity::fetchIPAddress() { + if (WiFi.status() == WL_CONNECTED) { + HTTPClient http; + WiFiClient client; + http.begin(client, getServerURL); + + int httpCode = http.GET(); + if (httpCode > 0) { + if (httpCode == HTTP_CODE_OK) { + ipAddress = http.getString(); + } + } else { + Serial.printf("GET request failed, error: %s\n", http.errorToString(httpCode).c_str()); + } + + http.end(); + } else { + Serial.println("WiFi not connected"); + } + return ipAddress; // Add this return statement } /** Send a POST request to a server with provided data */ int Connectivity::httpPost(const char *serverAddress, const char *serverSubPath, const unsigned short serverPort, const char *data, const size_t dataLength, const char *contentType) { - if ( wifi_client.connect(serverAddress, serverPort)) { + WiFiClient wifi_client; // Ensure WiFiClient is declared and initialized + if (wifi_client.connect(serverAddress, serverPort)) { wifi_client.printf("POST %s HTTP/1.1\r\n", serverSubPath); wifi_client.printf("Content-Type: %s\r\n", contentType); wifi_client.printf("Content-Length: %d\r\n", dataLength); @@ -37,4 +61,4 @@ int Connectivity::httpPost(const char *serverAddress, const char *serverSubPath, } return 1; -} \ No newline at end of file +} diff --git a/code/arduino/Movement-sensor-code/Connectivity.h b/code/arduino/Movement-sensor-code/Connectivity.h index 90742aa..6de0109 100644 --- a/code/arduino/Movement-sensor-code/Connectivity.h +++ b/code/arduino/Movement-sensor-code/Connectivity.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -18,12 +19,12 @@ public: void websocketSetup(char* ip, uint16_t port, char* adress); void sendData(float roll, float pitch, float yaw); int httpPost(const char *serverAddress, const char *serverSubPath, const unsigned short serverPort, const char *data, const size_t dataLength, const char *contentType); - + String fetchIPAddress(); private: ESP8266WiFiMulti wifi; WiFiClient wifi_client; - WebSocketsClient webSocket; + // WebSocketsClient webSocket; }; diff --git a/code/arduino/Movement-sensor-code/Movement-sensor-code.ino b/code/arduino/Movement-sensor-code/Movement-sensor-code.ino index 89ac75b..061f12d 100644 --- a/code/arduino/Movement-sensor-code/Movement-sensor-code.ino +++ b/code/arduino/Movement-sensor-code/Movement-sensor-code.ino @@ -1,42 +1,40 @@ #include "headerFile.h" -// SensorManager::Rotation offset; -#define BUFFER_SIZE 1024 - -#define DEVICE_ID 1 - -char *buffer = (char *)malloc(sizeof(char) * BUFFER_SIZE); - void setup() { - Serial.begin(9600); - // Serial.println("startup"); //connect to internet and start sensor connectivity.connectWiFi(ssid, pass); sensorManager.sensorSetup(); + Serial.begin(9600); } +unsigned long lastTime = 0; // will store the last time the code was run + void loop() { SensorManager::eulerAngles eulerRotation = sensorManager.getEulerAngles(); - SensorManager::acceleration rotationAcceleration = sensorManager.getAcelleration(); - unsigned long lastTime = 0; // will store the last time the code was run + // SensorManager::acceleration rotationAcceleration = sensorManager.getAcelleration(); + +struct acceleration { + float x = 9; + float y = 9; + float z = 9; +} accelData; unsigned long currentTime = millis(); if (currentTime - lastTime >= 100) { // 100 ms has passed memset(buffer, 0, BUFFER_SIZE); sprintf( buffer, - "{\"deviceId\": %d, \"rotationX\": %d, \"rotationY\": %d, \"rotationZ\": %d, \"accelerationX\": %d, \"accelerationY\": %d, \"accelerationZ\": %d, \"type\": %s}", + "{\"deviceId\": %d, \"rotationX\": %f, \"rotationY\": %f, \"rotationZ\": %f, \"accelerationX\": %f, \"accelerationY\": %f, \"accelerationZ\": %f, \"type\": %s}", DEVICE_ID, eulerRotation.roll, eulerRotation.pitch, eulerRotation.yaw, - rotationAcceleration.x, - rotationAcceleration.y, - rotationAcceleration.z, + accelData.x, + accelData.y, + accelData.z, "data"); - // Serial.println(connectivity.httpPost("192.168.137.45", "/", 3445, message.c_str(), message.length(), "json")); - // Serial.println(message); - connectivity.httpPost("192.168.137.45", "/", 3445, buffer, strlen(buffer), "application/json"); + // %d = int, %f = floatation, %s = string + connectivity.httpPost(connectivity.fetchIPAddress(), "/", 3445, buffer, strlen(buffer), "application/json"); lastTime = currentTime; } } diff --git a/code/arduino/Movement-sensor-code/SensorManager.cpp b/code/arduino/Movement-sensor-code/SensorManager.cpp index 77836e4..7fee39d 100644 --- a/code/arduino/Movement-sensor-code/SensorManager.cpp +++ b/code/arduino/Movement-sensor-code/SensorManager.cpp @@ -9,15 +9,12 @@ void SensorManager::sensorSetup() { //wait for the sensor to start before continue if (myIMU.begin() == false) { delay(1000); - // Serial.println("."); } //start sensorfunction and start autocalibration //once calibration is enabled it attempts to every 5 min - myIMU.enableGyroIntegratedRotationVector(100); //send data every 100ms - myIMU.enableAccelerometer(100); //Send data update every 100ms - Serial.println(F("magnetometer rotation enabled")); - myIMU.enableStepCounter(500); //Send data update every 500ms + // myIMU.enableAccelerometer(100); //Send data update every 100ms + // myIMU.enableStepCounter(500); //Send data update every 500ms } //get sensordata SensorManager::RotationQuintillions SensorManager::getQuintillions() { diff --git a/code/arduino/Movement-sensor-code/headerFIle.h b/code/arduino/Movement-sensor-code/headerFIle.h index b7b8220..8e3480e 100644 --- a/code/arduino/Movement-sensor-code/headerFIle.h +++ b/code/arduino/Movement-sensor-code/headerFIle.h @@ -8,6 +8,10 @@ Connectivity connectivity; WebSocketsClient webSocket; #define USE_SERIAL Serial - #define ssid "1235678i" #define pass "12345678" +#define BUFFER_SIZE 1024 +#define DEVICE_ID 1 +#define IP_ADDRESS "192.168.137.12" + +char *buffer = (char *)malloc(sizeof(char) * BUFFER_SIZE); diff --git a/code/arduino/bluetoothEsp/bluetoothEsp.ino b/code/arduino/bluetoothEsp/bluetoothEsp.ino deleted file mode 100644 index 0e0eaaf..0000000 --- a/code/arduino/bluetoothEsp/bluetoothEsp.ino +++ /dev/null @@ -1,33 +0,0 @@ -#include -#include - -// Define the service UUID -#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b" - -// Define the characteristic UUID -#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8" - -void setup() { - // Create a BLE server - BLEServer *pServer = BLEDevice::createServer(); - - // Create a BLE service - BLEService *pService = pServer->createService(SERVICE_UUID); - - // Create a BLE characteristic - BLECharacteristic *pCharacteristic = pService->createCharacteristic( - CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_READ); - - // Set the characteristic value - pCharacteristic->setValue("Hello, Bluetooth!"); - - // Start the service - pService->start(); - - // Start advertising the service - pServer->getAdvertising()->start(); -} - -void loop() { - // Nothing to do here -} \ No newline at end of file diff --git a/code/web/incoming_request_handlers.js b/code/server/incoming_request_handlers.js similarity index 85% rename from code/web/incoming_request_handlers.js rename to code/server/incoming_request_handlers.js index 6049b3b..6fa0f41 100644 --- a/code/web/incoming_request_handlers.js +++ b/code/server/incoming_request_handlers.js @@ -1,4 +1,3 @@ - /** * * @param {Request} request The incoming request @@ -14,7 +13,7 @@ function handleIncoming(request, response, app, pool) if (!request.hasOwnProperty('uid') || typeof request.uid !== 'number') { - query = 'SELECT * FROM Exercise'; + query = 'SELECT * FROM Exercise ORDER BY RAND() LIMIT 1'; } else parameters.push(request.uid); // Acquire database connection @@ -33,13 +32,17 @@ function handleIncoming(request, response, app, pool) // Send back the data in the right format let converted = rows.map(row => { return { + exerciseId: row.ExerciseID, name: row.Name, - description: row.Description, muscleGroup: row.MuscleGroup, + shortDescription: row.ShortDescription, + description: row.Description, imageUrl: row.ImageURL, - videoUrl: row.VideoURL + videoUrl: row.VideoURL, + path: row.Path, + duration: row.Duration }; - }); + })[0]; response .status(200) diff --git a/code/server/ipSender/index.js b/code/server/ipSender/index.js new file mode 100644 index 0000000..dc68c2d --- /dev/null +++ b/code/server/ipSender/index.js @@ -0,0 +1,22 @@ +const express = require('express'); +const app = express(); + +app.use(express.json()); // for parsing application/json + +let ipAddress = ''; // to store the received IP address + +// endpoint to receive an IP address from an external source +app.post('/set-ip', (req, res) => { + ipAddress = req.body.ip; + console.log('IP address received:', ipAddress); +}); + +// endpoint for the ESP32 to fetch the IP address +app.get('/get-ip', (req, res) => { + res.json({ ip: ipAddress }); + console.log('IP address sent to ESP32'); +}); + +app.listen(42069, () => { + console.log('Server is running on port 42069'); +}); \ No newline at end of file diff --git a/code/web/server.js b/code/server/server.js similarity index 61% rename from code/web/server.js rename to code/server/server.js index 886846e..c31eded 100644 --- a/code/web/server.js +++ b/code/server/server.js @@ -21,6 +21,20 @@ const pool = mariadb.createPool(databaseCredentials); // Register incoming HTTP request handlers require('./incoming_request_handlers')(app, pool); +let ipAddress = ''; // to store the received IP address + +// endpoint to receive an IP address from an external source +app.post('/set-ip', (req, res) => { + ipAddress = req.body.ip; + console.log('IP address received:', ipAddress); +}); + +// endpoint for the ESP32 to fetch the IP address +app.get('/get-ip', (req, res) => { + res.json({ ip: ipAddress }); + console.log('IP address sent to ESP32'); +}); + // Start server app.listen(serverPort, () => { console.log(`Server running on port ${serverPort}`); diff --git a/code/server/test/config.js b/code/server/test/config.js new file mode 100644 index 0000000..87c69b0 --- /dev/null +++ b/code/server/test/config.js @@ -0,0 +1,8 @@ +// config.js +module.exports = { + host: '127.0.0.1', + user: 'fitbot', + password: 'fitbot123', + database: 'fitbot', + port: 3306, // Default MariaDB port + }; diff --git a/code/server/test/testConnection.js b/code/server/test/testConnection.js new file mode 100644 index 0000000..4eb953a --- /dev/null +++ b/code/server/test/testConnection.js @@ -0,0 +1,35 @@ +const express = require('express'); +const mariadb = require('mariadb'); +const config = require('./config'); + +const app = express(); +const port = 443; // Use port 443 + +const pool = mariadb.createPool({ + host: config.host, + user: config.user, + password: config.password, + database: config.database, + connectionLimit: 5, + acquireTimeout: 30000, // Increase timeout to 30 seconds +}); + +app.get('/', async (req, res) => { + let conn; + try { + conn = await pool.getConnection(); + console.log("Connected to MariaDB!"); + const rows = await conn.query("SELECT * FROM Exercise ORDER BY RAND() LIMIT 1"); + console.log(rows); // [{val: 1}] + res.json(rows); // Send the result as JSON response + } catch (err) { + console.error("Unable to connect to MariaDB:", err); + res.status(500).send("Unable to connect to MariaDB"); + } finally { + if (conn) conn.release(); // Release the connection back to the pool + } +}); + +app.listen(port, '145.92.8.132', () => { + console.log(`Server is listening on http://145.92.8.132:${port}`); +}); diff --git a/code/src/Fitbot/.idea/misc.xml b/code/src/Fitbot/.idea/misc.xml index 4496bac..b1965d6 100644 --- a/code/src/Fitbot/.idea/misc.xml +++ b/code/src/Fitbot/.idea/misc.xml @@ -16,25 +16,37 @@ - + + + + + + + - + + + + + - - + + + + diff --git a/code/src/Fitbot/app/build.gradle b/code/src/Fitbot/app/build.gradle index e63614b..1348df8 100644 --- a/code/src/Fitbot/app/build.gradle +++ b/code/src/Fitbot/app/build.gradle @@ -38,9 +38,6 @@ dependencies { implementation 'org.joml:joml:1.10.5' implementation 'com.google.code.gson:gson:2.8.6' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.junit.jupiter:junit-jupiter' - testImplementation 'org.junit.jupiter:junit-jupiter' - testImplementation 'org.junit.jupiter:junit-jupiter' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' implementation 'com.aldebaran:qisdk:1.7.5' diff --git a/code/src/Fitbot/app/src/main/AndroidManifest.xml b/code/src/Fitbot/app/src/main/AndroidManifest.xml index 326887c..32bdaaf 100644 --- a/code/src/Fitbot/app/src/main/AndroidManifest.xml +++ b/code/src/Fitbot/app/src/main/AndroidManifest.xml @@ -10,19 +10,21 @@ + + + + - - + * Before any of the functions in this class can be used, the Pepper class needs a QiContext to be + * set. This is retrieved in the `onRobotFocusGained` method of the RobotLifecycleCallbacks + * interface, and can then provided using the `provideContext` method. + *

+ */ +public class Pepper { + + // Queue containing all Pepper actions that need to be executed + // This is to prevent the app from crashing when attempting to perform multiple actions at once + private static final ConcurrentLinkedQueue pepperActionEventQueue = + new ConcurrentLinkedQueue<>(); + + private static final AtomicBoolean isAnimating = new AtomicBoolean(false); + private static final AtomicBoolean isSpeaking = new AtomicBoolean(false); + + private static final Locale DEFAULT_LOCALE = new Locale(Language.DUTCH, Region.NETHERLANDS); + + /** + * The latest QiContext used by Pepper. + * This can be publicly accessed to get the latest QiContext used by Pepper. + */ + public static QiContext latestContext = null; + + /** + * Make the Pepper robot speak a phrase. + * This function adds a SpeechEvent to the event queue, + * and will speak the phrase once Pepper is ready to process it. + * + * @param phrase The phrase to speak + */ + public static void say(String phrase) { + addToEventQueue(new PepperSpeechEvent(phrase, DEFAULT_LOCALE)); + } + + /** + * Make the Pepper robot perform an animation. + * This function adds an AnimationEvent to the event queue, + * and will perform the animation once Pepper is ready to process it. + * + * @param animationName The name of the animation to play + */ + public static void animate(String animationName) { + addToEventQueue(new PepperAnimationEvent(animationName)); + } + + /** + * Add an event to the event queue. + * The event will be executed once Pepper is ready to process it. + * + * @param event The event to add to the queue + */ + public static void addToEventQueue(AbstractPepperActionEvent event) { + pepperActionEventQueue.add(event); + processEventQueue(); + } + + /** + * Process the event queue. + * This method will process the next event in the queue if Pepper is not currently + * speaking or animating. + * This prevents multiple actions from being executed at the same time, which can + * cause the application to crash. + */ + private static synchronized void processEventQueue() { + if (!pepperActionEventQueue.isEmpty()) { + AbstractPepperActionEvent event = pepperActionEventQueue.poll(); + + // Prevent null pointer exceptions + if (event == null || latestContext == null) + return; + + switch (event.getAction()) { + /* Event for speaking a phrase **/ + case ACTION_SPEAK: + if (!(event instanceof PepperSpeechEvent) || isSpeaking.get()) + break; + + PepperSpeechEvent speechEvent = (PepperSpeechEvent) event; + isSpeaking.set(true); + speechEvent + .getSay(latestContext) + .async() + .run() + .andThenConsume(future -> { + isSpeaking.set(false); + processEventQueue(); + }); + break; + + /* Event for animating the robot **/ + case ACTION_ANIMATE: + if (!(event instanceof PepperAnimationEvent) || isAnimating.get()) + break; + + PepperAnimationEvent animationEvent = (PepperAnimationEvent) event; + animationEvent + .getAnimation(latestContext) + .async() + .run() + .andThenConsume(future -> { + isAnimating.set(false); + processEventQueue(); + }); + break; + default: + // Do nothing + break; + } + } + } + + /** + * Function for providing the latest QiContext to Pepper. + * + * @param context The QiContext to use. + * This can be null if the context is not available. + * @param origin The origin of the context. + * This parameter is to prevent other classes from + * unnecessarily providing a context, or setting + * the context to null. + */ + public static void provideContext(QiContext context, Class origin) { + latestContext = context; + if (context != null) { + processEventQueue(); + } + } + +} diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/pepper/PepperAnimationEvent.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/pepper/PepperAnimationEvent.java new file mode 100644 index 0000000..455be28 --- /dev/null +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/pepper/PepperAnimationEvent.java @@ -0,0 +1,65 @@ +package com.example.fitbot.pepper; + +import com.aldebaran.qi.sdk.QiContext; +import com.aldebaran.qi.sdk.builder.AnimateBuilder; +import com.aldebaran.qi.sdk.builder.AnimationBuilder; +import com.aldebaran.qi.sdk.object.actuation.Animate; +import com.aldebaran.qi.sdk.object.actuation.Animation; + +public class PepperAnimationEvent extends AbstractPepperActionEvent { + + public final String animationName; + private final IAnimationCompletedListener onLabelReachedListener; + + + /** + * Constructor for PepperAnimationEvent + * + * @param animationName The name of the animation to play + * @param onLabelReachedListener The listener to call when the animation is completed + */ + public PepperAnimationEvent(String animationName, IAnimationCompletedListener onLabelReachedListener) { + this.animationName = animationName; + this.onLabelReachedListener = onLabelReachedListener; + } + + /** + * Constructor for PepperAnimationEvent + * + * @param animationName The name of the animation to play + */ + public PepperAnimationEvent(String animationName) { + this(animationName, null); + } + + @Override + public EPepperAction getAction() { + return null; + } + + /** + * Returns an animation object, which can be used to play the animation + * in the Pepper class. + */ + public Animate getAnimation(QiContext context) { + Animation animation = AnimationBuilder.with(context) + .withResources(context.getResources().getIdentifier(animationName, "raw", context.getPackageName())) + .build(); + + Animate animate = AnimateBuilder.with(context) + .withAnimation(animation) + .build(); + + // Add a listener for when a label is reached + animate.addOnLabelReachedListener((label, time) -> { + if (onLabelReachedListener != null && "end".equals(label)) { + onLabelReachedListener.onComplete(); + } + }); + return animate; + } + + public interface IAnimationCompletedListener { + void onComplete(); + } +} diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/pepper/PepperSpeechEvent.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/pepper/PepperSpeechEvent.java new file mode 100644 index 0000000..ecf8eab --- /dev/null +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/pepper/PepperSpeechEvent.java @@ -0,0 +1,32 @@ +package com.example.fitbot.pepper; + +import com.aldebaran.qi.sdk.QiContext; +import com.aldebaran.qi.sdk.builder.SayBuilder; +import com.aldebaran.qi.sdk.object.conversation.Say; +import com.aldebaran.qi.sdk.object.locale.Locale; + +public class PepperSpeechEvent extends AbstractPepperActionEvent { + + public final String phrase; + public final Locale locale; + + public PepperSpeechEvent(String phrase, Locale locale) { + this.locale = locale; + this.phrase = phrase; + } + + @Override + public EPepperAction getAction() { + return EPepperAction.ACTION_SPEAK; + } + + /** + * Returns a Say object, which can then be executed. + */ + public Say getSay(QiContext context) { + return SayBuilder.with(context) + .withText(this.phrase) + .withLocale(this.locale) + .build(); + } +} diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/EndScreenActivity.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/EndScreenActivity.java index 157bebf..f2185de 100644 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/EndScreenActivity.java +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/EndScreenActivity.java @@ -4,8 +4,7 @@ import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import com.example.fitbot.R; -import com.example.fitbot.ui.activities.FitnessActivity; -import com.example.fitbot.ui.activities.MainActivity; +import com.example.fitbot.util.NavigationManager; public class EndScreenActivity extends AppCompatActivity { @@ -14,7 +13,7 @@ public class EndScreenActivity extends AppCompatActivity { super.onCreate(savedInstanceState); setContentView(R.layout.activity_end_screen); - com.example.fitbot.util.ButtonNavigation.setupButtonNavigation(this, R.id.homeButton, MainActivity.class); - com.example.fitbot.util.ButtonNavigation.setupButtonNavigation(this, R.id.continueButton, FitnessActivity.class); + NavigationManager.setupButtonNavigation(this, R.id.homeButtonEndScreen, MainActivity.class); + NavigationManager.setupButtonNavigation(this, R.id.startButtonEndScreen, FitnessActivity.class); } } \ No newline at end of file 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 89a857f..abde556 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,14 @@ package com.example.fitbot.ui.activities; +import android.app.Dialog; +import android.content.Context; +import android.graphics.drawable.ColorDrawable; +import android.net.Uri; import android.os.Bundle; import android.util.Log; +import android.view.View; +import android.view.WindowManager; +import android.widget.Button; import android.widget.VideoView; import com.aldebaran.qi.sdk.QiContext; @@ -10,25 +17,41 @@ 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.exercise.EMuscleGroup; import com.example.fitbot.exercise.Exercise; -import com.example.fitbot.ui.components.PersonalMotionPreviewElement; -import com.example.fitbot.util.ButtonNavigation; -import com.example.fitbot.util.FitnessCycle; -import com.example.fitbot.util.path.GesturePath; +import com.example.fitbot.exercise.ExerciseManager; +import com.example.fitbot.pepper.Pepper; +import com.example.fitbot.ui.components.ExerciseStatusElement; +import com.example.fitbot.util.NavigationManager; +import com.example.fitbot.util.processing.InputProcessor; import org.joml.Vector3f; -import java.util.concurrent.CompletableFuture; - public class FitnessActivity extends RobotActivity implements RobotLifecycleCallbacks { - PersonalMotionPreviewElement personalMotionPreviewElement; + // Private fields for the FitnessActivity class. + private ExerciseStatusElement personalMotionPreviewElement; + private InputProcessor motionProcessor; + private Exercise currentExercise; + + // Some nice little messages for the user + private static final String[] EXERCISE_NOT_FOUND_MESSAGES = new String[]{ + "Ik heb momenteel helaas wat moeite met het ophalen van oefeningen, sorry.", + "Het lijkt erop dat de oefeningen op een misterieus avontuur zijn. Even wachten tot ze terug zijn.", + ", de oefeningen slapen nog, probeer het later nog eens." + }; + + private static final String EXERCISE_NOT_FOUND_SEEK_HELP_MESSAGE = + "Indien dit probleem zich voortzet, neem contact op met de ontwikkelaar."; + + private static final float SENSOR_SAMPLE_RATE = 10.0f; + private static final int EXERCISE_COUNT = 5; + private static final float EXERCISE_SPEED_MULTIPLIER = 1.0f; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); QiSDK.register(this, this); + setContentView(R.layout.activity_fitness); // Remove the ugly ass bar on top of the view @@ -36,62 +59,137 @@ public class FitnessActivity extends RobotActivity implements RobotLifecycleCall // Find the VideoView by its ID VideoView videoView = findViewById(R.id.videoView); + playVideo(videoView, this); + NavigationManager.setupButtonNavigation(this, R.id.homeButtonFitness, MainActivity.class); + NavigationManager.setupButtonNavigation(this, R.id.skipButtonFitness, MainActivity.class); //Needs to skip exercises once those are implemented - FitnessCycle.playVideo(videoView, this); + NavigationManager.hideSystemUI(this); - ButtonNavigation.setupButtonNavigation(this, R.id.homeButton, MainActivity.class); - ButtonNavigation.setupButtonNavigation(this, R.id.buttonComplete, EndScreenActivity.class); - // Implement your logic when the robot focus is gained - - GesturePath.Builder gesturePathBuilder = new GesturePath.Builder(); - - /* Generate a random path to test the tracking system */ - for ( int i = 0; i < 40; i++) - { - gesturePathBuilder.addVector( - new Vector3f( - (float)Math.cos(Math.PI + (Math.PI / 40.0f) * i), - (float)Math.sin(Math.PI + (Math.PI / 40.0f) * i), - 0 - ) - ); - } - - personalMotionPreviewElement = findViewById(R.id.personalMotionPreviewElement); - personalMotionPreviewElement.post(() -> { - Log.i("FitnessActivity", "PersonalMotionPreviewElement.post()"); - - Exercise exercise = new Exercise(EMuscleGroup.ARMS, "Bicep Curls", "Oefening voor de biceps.", gesturePathBuilder.build(), gesturePathBuilder.build()); - - personalMotionPreviewElement.initialize(exercise); + Button infoButtonFitness = findViewById(R.id.infoButtonFitness); + infoButtonFitness.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + showInfoDialog(); + } }); } @Override public void onRobotFocusGained(QiContext qiContext) { - // Find the VideoView by its ID -// CompletableFuture.runAsync(() -> FitnessCycle.executeMovement("bicepcurl", 10, qiContext)); - Log.i("Motion", "qiContext provided"); - personalMotionPreviewElement.provideQiContext(qiContext); + // Provide the context so that all queued actions can be performed. + Pepper.provideContext(qiContext, this.getClass()); + + personalMotionPreviewElement = findViewById(R.id.personalMotionPreviewElement); + + // Initialize the element whenever it has been added to the screen. + // This will provide the element with the appropriate dimensions for drawing + // the canvas properly. + personalMotionPreviewElement.post(() -> { + this.fetchExerciseAsync((exercise) -> { + // Acquire paths from the exercise and provide them to the motion processor + Vector3f[][] vectors = new Vector3f[][]{exercise.leftPath.getVectors(), exercise.rightPath.getVectors()}; + + motionProcessor = new InputProcessor(vectors, exercise.exerciseTimeInSeconds, SENSOR_SAMPLE_RATE); + + personalMotionPreviewElement.initialize(exercise, motionProcessor, EXERCISE_COUNT); + + motionProcessor.startListening(); + motionProcessor.setInputHandler(personalMotionPreviewElement); + }, (n) -> { + int randomMessageIndex = (int) Math.floor(Math.random() * EXERCISE_NOT_FOUND_MESSAGES.length); + Pepper.say(EXERCISE_NOT_FOUND_MESSAGES[randomMessageIndex]); + Pepper.say(EXERCISE_NOT_FOUND_SEEK_HELP_MESSAGE); + NavigationManager.navigateToActivity(this, EndScreenActivity.class); + }); + }); // FitnessCycle.playVideo(qiContext, videoView, this); } + /** + * Acquire an exercise from the ExerciseManager. + * Whenever the retrieval failed, it will have the robot say something to the user + * to inform them about the issue. + */ + public void fetchExerciseAsync(Exercise.ExerciseFetchHandler onSuccessfulFetch, Exercise.ExerciseFetchHandler onFailedFetch) { + // For some stupid reason we cannot perform network operations on the main thread. + // therefore we'll have to do it like this... + (new Thread(() -> { + Exercise exercise = ExerciseManager.fetchExerciseFromDatabase(); + if (exercise == null) { + onFailedFetch.handle(null); + } else { + onSuccessfulFetch.handle(exercise); + } + })).start(); + } + + /** + * 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) { + Uri videoUri = Uri.parse("android.resource://" + context.getPackageName() + "/" + R.raw.bicepvideo); + videoView.setVideoURI(videoUri); + + videoView.setOnCompletionListener(mp -> { + // Repeat the video when it finishes playing + videoView.start(); + }); + + videoView.start(); + } else { + Log.e("FitnessActivity", "VideoView is null. Check your layout XML."); + } + } + @Override public void onRobotFocusLost() { - // Implement your logic when the robot focus is lost + QiSDK.unregister(this, this); + Pepper.provideContext(null, this.getClass()); // Remove the context (unavailable) + + // Go to the main screen + NavigationManager.navigateToActivity(this, MainActivity.class); } @Override public void onRobotFocusRefused(String reason) { - // Implement your logic when the robot focus is refused } @Override protected void onDestroy() { - super.onDestroy(); + if (this.motionProcessor != null) { + this.motionProcessor.stopListening(); + this.motionProcessor = null; + } QiSDK.unregister(this, this); - this.personalMotionPreviewElement.onDestroy(); + Pepper.provideContext(null, this.getClass()); + + super.onDestroy(); + } + + private void showInfoDialog() { + final Dialog dialog = new Dialog(this); + dialog.setContentView(R.layout.dialog_info); + + NavigationManager.hideSystemUI(this); + + dialog.getWindow().setDimAmount(0.5f); + dialog.getWindow().setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT)); + dialog.setCancelable(true); + + Button closeButton = dialog.findViewById(R.id.closeButtonDialog); + closeButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dialog.dismiss(); + } + }); + dialog.show(); } } \ No newline at end of file diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/HelpActivity.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/HelpActivity.java index 3ffa2e3..5d0a444 100644 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/HelpActivity.java +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/HelpActivity.java @@ -1,10 +1,10 @@ package com.example.fitbot.ui.activities; -import android.support.v7.app.AppCompatActivity; import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; import com.example.fitbot.R; -import com.example.fitbot.util.ButtonNavigation; +import com.example.fitbot.util.NavigationManager; public class HelpActivity extends AppCompatActivity { @@ -13,7 +13,8 @@ public class HelpActivity extends AppCompatActivity { super.onCreate(savedInstanceState); setContentView(R.layout.activity_help); - ButtonNavigation.setupButtonNavigation(this, R.id.homeButton, MainActivity.class); + NavigationManager.setupButtonNavigation(this, R.id.homeButtonHelp, MainActivity.class); + NavigationManager.hideSystemUI(this); } } \ No newline at end of file diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/MainActivity.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/MainActivity.java index b2fa7ba..3aaca38 100644 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/MainActivity.java +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/MainActivity.java @@ -1,5 +1,6 @@ package com.example.fitbot.ui.activities; +import static com.example.fitbot.util.Networking.sendIpAddress; import android.annotation.SuppressLint; import android.content.Intent; import android.net.Uri; @@ -10,28 +11,36 @@ import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; -import android.util.Log; +import android.view.View; +import android.view.WindowManager; import android.widget.Button; import com.example.fitbot.R; -import com.example.fitbot.util.ButtonNavigation; +import com.example.fitbot.util.NavigationManager; + +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; public class MainActivity extends AppCompatActivity { - //Variables + // Variables DrawerLayout drawerLayout; NavigationView navigationView; Toolbar toolbar; Button startButton; @SuppressLint("WrongViewCast") - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); + // Set full screen mode to hide status bar + getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, + WindowManager.LayoutParams.FLAG_FULLSCREEN); - Button startButton = findViewById(R.id.startButton); + + startButton = findViewById(R.id.startButtonMain); startButton.setOnClickListener(v -> { Uri videoUri = Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.bicepvideo); Intent intent = new Intent(MainActivity.this, FitnessActivity.class); @@ -39,7 +48,12 @@ public class MainActivity extends AppCompatActivity { startActivity(intent); }); - setUpUi(); // Set up the UI + // Set up the UI + setUpUi(); + + // Hide system UI + NavigationManager.hideSystemUI(this); + sendIpAddress(this); } private void setUpUi() { @@ -47,31 +61,58 @@ public class MainActivity extends AppCompatActivity { drawerLayout = findViewById(R.id.drawer_layout); navigationView = findViewById(R.id.nav_view); toolbar = findViewById(R.id.toolbar); - startButton = findViewById(R.id.startButton); + startButton = findViewById(R.id.startButtonMain); - ButtonNavigation.setupButtonNavigation(this, R.id.startButton, FitnessActivity.class); - ButtonNavigation.setupButtonNavigation(this, R.id.helpButton, HelpActivity.class); + // Hide the action bar + if (getSupportActionBar() != null) { + getSupportActionBar().hide(); + } + NavigationManager.setupButtonNavigation(this, R.id.startButtonMain, FitnessActivity.class); + NavigationManager.setupButtonNavigation(this, R.id.helpButtonMain, HelpActivity.class); /*---Tool Bar---*/ setSupportActionBar(toolbar); // Make the toolbar act as the action bar - getSupportActionBar().setDisplayShowTitleEnabled(false); // Remove the title from the toolbar + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayShowTitleEnabled(false); // Remove the title from the toolbar + } /*---Navigation Drawer Menu---*/ navigationView.bringToFront(); // Make the navigation drawer menu clickable - ActionBarDrawerToggle toggle=new // Create a toggle for the navigation drawer - ActionBarDrawerToggle(this,drawerLayout,toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close); + ActionBarDrawerToggle toggle = new // Create a toggle for the navigation drawer + ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close) { + @Override + public void onDrawerOpened(View drawerView) { + super.onDrawerOpened(drawerView); + } + + @Override + public void onDrawerClosed(View drawerView) { + super.onDrawerClosed(drawerView); + } + }; drawerLayout.addDrawerListener(toggle); toggle.syncState(); // Synchronize the state of the navigation drawer } @Override - public void onBackPressed(){ // Close the navigation drawer when the back button is pressed - if(drawerLayout.isDrawerOpen(GravityCompat.START)){ - drawerLayout.closeDrawer(GravityCompat.START); - } - else - {super.onBackPressed(); + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + if (hasFocus) { + NavigationManager.hideSystemUI(this); } } + + @Override + public void onBackPressed() { // Close the navigation drawer when the back button is pressed + if (drawerLayout.isDrawerOpen(GravityCompat.START)) { + drawerLayout.closeDrawer(GravityCompat.START); + } else { + super.onBackPressed(); + } + } + + } + + diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/components/ExerciseStatusElement.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/components/ExerciseStatusElement.java new file mode 100644 index 0000000..606321b --- /dev/null +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/components/ExerciseStatusElement.java @@ -0,0 +1,227 @@ +package com.example.fitbot.ui.components; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; + +import com.example.fitbot.exercise.Exercise; +import com.example.fitbot.pepper.Pepper; +import com.example.fitbot.ui.activities.EndScreenActivity; +import com.example.fitbot.ui.activities.FitnessActivity; +import com.example.fitbot.ui.activities.MainActivity; +import com.example.fitbot.util.NavigationManager; +import com.example.fitbot.util.processing.IInputHandler; +import com.example.fitbot.util.processing.InputProcessor; + +import org.joml.Matrix4f; +import org.joml.Vector2f; +import org.joml.Vector3f; +import org.joml.Vector4f; + +import java.util.concurrent.ConcurrentLinkedQueue; + +public class ExerciseStatusElement extends View implements IInputHandler { + + // Fields regarding Exercise and speech handling. + private InputProcessor motionProcessor; + private Exercise exercise; + private int exerciseCount; + + private FitnessActivity parentActivity; + + private final Paint userProgressPaint = new Paint(); + private final Paint borderPaint = new Paint(); + private final Paint backgroundPaint = new Paint(); + + // TODO: Remove + private final Matrix4f viewMatrix = new Matrix4f(); // The view matrix for the 3D to 2D transformation. + private final Matrix4f projectionMatrix = new Matrix4f(); // The projection matrix for the 3D to 2D transformation. + private final Vector4f objectPosition = new Vector4f(0, 0, 0, 1); // The location of the object in 3D space. + private final ConcurrentLinkedQueue vectors = new ConcurrentLinkedQueue<>(); + + // TODO: Remove + private Vector2f[] axisVectors = new Vector2f[0]; + + + private static final String[] STARTING_PHRASES = { + "Veel success met de oefening!", + "Je kan het!", + "Veel plezier!" + }; + + + public ExerciseStatusElement(Context context, AttributeSet attrs) { + super(context, attrs); + + if (context instanceof Activity) { + this.parentActivity = (FitnessActivity) context; + } + + this.userProgressPaint.setColor(0xFFFF0000); // Red + this.userProgressPaint.setStyle(Paint.Style.FILL); + this.userProgressPaint.setStrokeWidth(5.0f); + this.userProgressPaint.setAntiAlias(true); + + // Target paint is the filling of the target path. + this.borderPaint.setColor(-1); + this.borderPaint.setStyle(Paint.Style.STROKE); + this.borderPaint.setStrokeWidth(5.0f); + this.borderPaint.setAntiAlias(true); + + this.backgroundPaint.setColor(0xFF000000); // Black + } + + /** + * Method for initializing the PersonalMotionPreviewElement. + * This method has to be called with a "post" function when the element has been + * created, otherwise the dimensions of the element aren't initialized yet, which + * will cause the vertex projections to fail (0 width and height). + * + * @param exercise The exercise that the user is currently performing. + * @param motionProcessor The motion processor that will be used to process the user's motion. + * @param exerciseCount The total amount of exercises that the user has to perform. + */ + public void initialize(@Nullable Exercise exercise, InputProcessor motionProcessor, int exerciseCount) { + Log.i("PersonalMotionPreviewElement", "Creating new PersonalMotionPreviewElement."); + + this.motionProcessor = motionProcessor; + this.exercise = exercise; + this.exerciseCount = exerciseCount; + + /* TODO: Remove */ + this.axisVectors = new Vector2f[]{ + projectVertex(new Vector3f(-5.0f, 0, 0), getWidth(), getHeight()), + projectVertex(new Vector3f(5.0f, 0, 0), getWidth(), getHeight()), + projectVertex(new Vector3f(0, -5.0f, 0), getWidth(), getHeight()), + projectVertex(new Vector3f(0, 5.0f, 0), getWidth(), getHeight()), + projectVertex(new Vector3f(0, 0, -5.0f), getWidth(), getHeight()), + projectVertex(new Vector3f(0, 0, 5.0f), getWidth(), getHeight()) + }; + + Pepper.say(STARTING_PHRASES[(int) Math.floor(Math.random() * STARTING_PHRASES.length)]); + + // Handler that is called every time the motion processor receives new data. + this.motionProcessor.setInputHandler((rotationVector, deviceId) -> { + + Log.i("MotionProcessor", "Rotation vector received: " + rotationVector); + Log.i("MotionProcessor", "Last error offset:" + this.motionProcessor.getError(deviceId, this.motionProcessor.secondsPassed())); + + if (this.motionProcessor.hasFinished()) { + // If for some reason the parent activity is not defined, + // move back to the main screen. + if (this.parentActivity == null) { + // Move to main screen + NavigationManager.navigateToActivity(getContext(), MainActivity.class); + return; + } + // Move on to the next exercise, or finish. + if (this.exerciseCount > 0) { + this.exerciseCount--; + this.parentActivity.fetchExerciseAsync((newExercise) -> { + this.motionProcessor.useExercise(newExercise); + }, (failed) -> { + // Move to main screen + NavigationManager.navigateToActivity(parentActivity, MainActivity.class); + }); + } else { + // Finish the exercise. + NavigationManager.navigateToActivity(parentActivity, EndScreenActivity.class); + return; + } + } + + /* TODO: Use raw vector */ + vectors.add(projectVertex(rotationVector, this.getWidth(), this.getHeight())); + + /* TODO: Remove */ + Vector2f parsed = projectVertex(rotationVector, this.getWidth(), this.getHeight()); + + this.vectors.add(parsed /* TODO: Add regular vertex once exercise data is stored in DB*/); + + // Remove the first element if the array is too big + if (this.vectors.size() > 100) + this.vectors.poll(); + }); + } + + /** + * Method for setting the gesture path that will be drawn on the canvas. + * + * @param exercise The exercise that the user is currently performing. + */ + public void setExercise(Exercise exercise) { + this.motionProcessor.useExercise(exercise); + this.exercise = exercise; + } + + + private Vector2f projectVertex(Vector3f point, int virtualWidth, int virtualHeight) { + viewMatrix + .identity() + .lookAt(new Vector3f(0, 0, -2), new Vector3f(0, 0, 0), new Vector3f(0, 1, 0)); + + // Transform the projection matrix to a perspective projection matrix + // Perspective transformation conserves the depth of the object + projectionMatrix + .identity() + .perspective((float) Math.toRadians(70), (float) virtualWidth / virtualHeight, .01f, 1000.0f); + + // Convert world coordinates to screen-space using MVP matrix + Vector4f screenCoordinates = new Vector4f(point, 1.0f) + .mul(this.viewMatrix) + .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; + return new Vector2f(normalizedX, normalizedY); + } + + + @Override + public void onDraw(Canvas canvas) { + canvas.drawRect(0, 0, getWidth(), getHeight(), backgroundPaint); + this.setBackgroundColor(0xFF000000); // Black + /*if (this.exercise == null) + return;*/ + + + /* TODO: Remove */ + for (int i = 0, startX, endX, startY, endY; i < axisVectors.length / 2; i++) { + startX = (int) Math.max(0, Math.min(this.getWidth(), (int) axisVectors[i * 2].x)); + endX = (int) Math.max(0, Math.min(this.getWidth(), (int) axisVectors[i * 2 + 1].x)); + startY = (int) Math.max(0, Math.min(this.getHeight(), (int) axisVectors[i * 2].y)); + endY = (int) Math.max(0, Math.min(this.getHeight(), (int) axisVectors[i * 2 + 1].y)); + canvas.drawLine(startX, startY, endX, endY, this.borderPaint); + } + + for (Vector2f point : this.vectors) { + canvas.drawRect(point.x, point.y, point.x + 5, point.y + 5, this.userProgressPaint); + } +/* + // 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, (targetRadius * exerciseProgress.get()/1000.0f), this.referencePaint); + referencePaint.setColor( + Color.argb( + 255, + (int)(255 * (1.0 - exerciseProgress.get()/1000.0f)), + (int)(255 * exerciseProgress.get()/1000.0f), + 0 + ) + );*/ + + this.invalidate(); + } + + @Override + public void accept(Vector3f rotationVector, int sensorId) { + + } +} 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 deleted file mode 100644 index 61e13a0..0000000 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/components/PersonalMotionPreviewElement.java +++ /dev/null @@ -1,178 +0,0 @@ -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; -import android.util.Log; -import android.view.View; - -import com.aldebaran.qi.sdk.QiContext; -import com.example.fitbot.exercise.Exercise; -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; -import com.example.fitbot.util.processing.MotionProcessor; - -import org.joml.Matrix4f; -import org.joml.Vector2f; -import org.joml.Vector3f; -import org.joml.Vector4f; - -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; - -public class PersonalMotionPreviewElement extends View { - - private GesturePath[] paths; - private MotionProcessor motionProcessor; - - private double pathTime = 0.0D; // The timestamp at which the path is currently at. - private final AtomicInteger exerciseProgress = new AtomicInteger(0); // The progress of the exercise. Ranges from 0 to 1000. - - private QiContext qiContext; - - private Exercise exercise; - - 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 static final String[] USER_PHRASES = { - "Veel success met de oefening!", - "Je kan het!", - "Veel plezier!" - }; - - private double timePassed = 0.0D; // The time that has passed since the start of the exercise, in seconds. - private long startingTime = 0L; - - private Vector2f screenDimensions = new Vector2f(); // Width and height dimensions of the screen - - public PersonalMotionPreviewElement(Context context, AttributeSet attrs) { - super(context, attrs); - - this.referencePaint.setColor(0xFFFF0000); // Red - this.referencePaint.setStyle(Paint.Style.FILL); - this.referencePaint.setStrokeWidth(5.0f); - this.referencePaint.setAntiAlias(true); - - // 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); - } - - /** - * Method for initializing the PersonalMotionPreviewElement. - * This method has to be called with a "post" function when the element has been - * created, otherwise the dimensions of the element aren't initialized yet, which - * will cause the vertex projections to fail (0 width and height). - * - * @param exercise The exercise that the user is currently performing. - */ - public void initialize(Exercise exercise) { - Log.i("PersonalMotionPreviewElement", "Creating new PersonalMotionPreviewElement."); - this.backgroundColor.setColor(0xFF000000); // Black - - this.screenDimensions.x = this.getWidth(); - this.screenDimensions.y = this.getHeight(); - this.actualPath = new Path(); - this.targetPath = new Path(); - - this.startingTime = System.nanoTime(); // Set the last time to the current time - - this.exercise = exercise; - this.paths = exercise.getPath(); - } - - public void onDestroy() - { - if ( this.motionProcessor != null ) - this.motionProcessor.stopListening(); - - this.motionProcessor = null; - } - - /** - * 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) { - try { - this.qiContext = context; - if (this.motionProcessor != null) - this.motionProcessor.stopListening(); - - this.motionProcessor = new MotionProcessor(); - this.motionProcessor.startListening(); - - // Handler that is called every time the motion processor receives new data. - this.motionProcessor.setMotionDataEventHandler((processed, preprocessed, sampleIndex, sampleRate, deviceId) -> { - int progress = (int) this.motionProcessor.getError(this.paths[0], processed); - this.exerciseProgress.set(Math.min(1000, Math.max(0, progress))); - Log.i("MotionProcessor", "Processed data: " + progress + " (" + preprocessed + ")"); - }); - saySomethingNice(); - } catch (Exception e) { - Log.e("MotionProcessor", "An error occurred whilst attempting to provide QiContext:" + e.getMessage()); - } - } - - /** - * Function to say something nice to the user :) - */ - private void saySomethingNice() - { - if (this.qiContext == null) - return; - - 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 exercise The exercise that the user is currently performing. - */ - public void setExercise(Exercise exercise) { - this.exercise = exercise; - } - - - @Override - public void onDraw(Canvas canvas) { - canvas.drawRect(0, 0, getWidth(), getHeight(), backgroundColor); - this.setBackgroundColor(0xFF000000); // Black - if (this.exercise == null) - return; - - // 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, (targetRadius * exerciseProgress.get()/1000.0f), this.referencePaint); - referencePaint.setColor( - Color.argb( - 255, - (int)(255 * (1.0 - exerciseProgress.get()/1000.0f)), - (int)(255 * exerciseProgress.get()/1000.0f), - 0 - ) - ); - - this.invalidate(); - - timePassed = (System.nanoTime() - startingTime) / 1E9D; - } -} diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/Animations.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/Animations.java index 01444f4..6d243c1 100644 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/Animations.java +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/Animations.java @@ -11,8 +11,7 @@ import com.aldebaran.qi.sdk.object.actuation.Animation; public class Animations { - public static void Animate(String AnimationFile, QiContext ctx) - { + public static void Animate(String AnimationFile, QiContext ctx) { int resId = ctx.getResources().getIdentifier(AnimationFile, "raw", ctx.getPackageName()); Animation animation = AnimationBuilder.with(ctx) diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/ButtonNavigation.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/ButtonNavigation.java deleted file mode 100644 index b239b35..0000000 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/ButtonNavigation.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.fitbot.util; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.view.View; -import android.widget.Button; - -public class ButtonNavigation { - - /** - * Sets up a button to navigate to a different activity when clicked. - * - * @param currentActivity The activity that contains the button - * @param buttonId The ID of the button - * @param targetActivity The activity to navigate to - */ - public static void setupButtonNavigation(Activity currentActivity, int buttonId, Class targetActivity) { - Button button = currentActivity.findViewById(buttonId); - button.setOnClickListener(v -> { - Intent intent = new Intent(currentActivity, targetActivity); - currentActivity.startActivity(intent); - currentActivity.finish(); - }); - } -} \ No newline at end of file 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 deleted file mode 100644 index a55b445..0000000 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/FitnessCycle.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.example.fitbot.util; - -import android.content.Context; -import android.net.Uri; -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; -import com.aldebaran.qi.sdk.builder.AnimationBuilder; -import com.aldebaran.qi.sdk.object.actuation.Animate; -import com.aldebaran.qi.sdk.object.actuation.Animation; - -import java.util.concurrent.atomic.AtomicInteger; - -public class FitnessCycle extends AppCompatActivity { - - 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) - .withResources(qiContext.getResources().getIdentifier(Exercise, "raw", qiContext.getPackageName())) - .build(); - - Animate animate = AnimateBuilder.with(qiContext) - .withAnimation(animation) - .build(); - - // Add a listener for when a label is reached - animate.addOnLabelReachedListener((label, time) -> { - // Increment repCount when the end of a repetition is reached - if ("end".equals(label)) { - repCount.incrementAndGet(); - } - }); - - // Run the animation the desired number of times - for (int i = 0; i < Reps; i++) { - animate.run(); - } - } - - /** - * 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) { - Uri videoUri = Uri.parse("android.resource://" + context.getPackageName() + "/" + R.raw.bicepvideo); - videoView.setVideoURI(videoUri); - - videoView.setOnCompletionListener(mp -> { - // Repeat the video when it finishes playing - videoView.start(); - }); - - videoView.start(); - } else { - Log.e("FitnessActivity", "VideoView is null. Check your layout XML."); - } - } -} \ No newline at end of file diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/NavigationManager.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/NavigationManager.java new file mode 100644 index 0000000..a906b53 --- /dev/null +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/NavigationManager.java @@ -0,0 +1,60 @@ +package com.example.fitbot.util; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.view.View; +import android.widget.Button; + +public class NavigationManager { + + /** + * Sets up a button to navigate to a different activity when clicked. + * + * @param currentActivity The activity that contains the button + * @param buttonId The ID of the button + * @param targetActivity The activity to navigate to + */ + public static void setupButtonNavigation(Activity currentActivity, int buttonId, Class targetActivity) { + Button button = currentActivity.findViewById(buttonId); + if (button == null) { + throw new IllegalArgumentException("Button with ID " + buttonId + " not found in " + currentActivity.getClass().getSimpleName()); + } + button.setOnClickListener(v -> NavigationManager.navigateToActivity(currentActivity, targetActivity)); + } + + /** + * Navigates to the target activity. + * + * @param currentActivity The current activity + * @param targetActivity The target activity + */ + public static void navigateToActivity(Activity currentActivity, Class targetActivity) { + Intent intent = new Intent(currentActivity, targetActivity); + // Close previous activity + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + currentActivity.startActivity(intent); + currentActivity.finish(); + } + + /** + * Navigates to the target activity. + * + * @param context The context + * @param targetActivity The target activity + */ + public static void navigateToActivity(Context context, Class targetActivity) { + Intent intent = new Intent(context, targetActivity); + context.startActivity(intent); + } + + public static void hideSystemUI(Activity currentActivity) { + View decorView = currentActivity.getWindow().getDecorView(); + // Hide the status bar and navigation bar + int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + decorView.setSystemUiVisibility(uiOptions); + } + +} \ No newline at end of file diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/Networking.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/Networking.java new file mode 100644 index 0000000..db0e6f2 --- /dev/null +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/Networking.java @@ -0,0 +1,76 @@ +package com.example.fitbot.util; + +import android.content.Context; +import android.net.wifi.WifiManager; +import android.os.AsyncTask; +import android.util.Log; + +import java.io.OutputStream; +import java.math.BigInteger; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.URL; +import java.net.UnknownHostException; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; + +public class Networking { + + public static void sendIpAddress(final Context context) { + new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + String ipAddress = getIPAddress(context); + String jsonInputString = "{\"ip\":\"" + + ipAddress + + "\"}"; + + byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8); + HttpURLConnection conn = null; + try { + URL url = new URL("http://145.92.8.132:443/set-ip"); // Replace with your Node server URL + conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setDoOutput(true); + + OutputStream os = conn.getOutputStream(); + os.write(input, 0, input.length); + os.close(); + + int responseCode = conn.getResponseCode(); + Log.i("NetworkUtils", "Response Code: " + responseCode); + } catch (Exception e) { + Log.e("NetworkUtils", "Error sending IP address", e); + } finally { + if (conn != null) { + conn.disconnect(); + } + } + return null; + } + }.execute(); + } + + + private static String getIPAddress(Context context) { + WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + int ipAddress = wifiManager.getConnectionInfo().getIpAddress(); + + // Convert little-endian to big-endian if needed + if (ByteOrder.nativeOrder().equals(ByteOrder.LITTLE_ENDIAN)) { + ipAddress = Integer.reverseBytes(ipAddress); + } + + byte[] ipByteArray = BigInteger.valueOf(ipAddress).toByteArray(); + + String ip = ""; + try { + ip = InetAddress.getByAddress(ipByteArray).getHostAddress(); + } catch (UnknownHostException ex) { + Log.e("WIFIIP", "Unable to get host address."); + } + + return ip; + } +} \ No newline at end of file diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/path/GesturePath.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/path/GesturePath.java index 06ebe10..14e7706 100644 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/path/GesturePath.java +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/path/GesturePath.java @@ -2,10 +2,6 @@ package com.example.fitbot.util.path; import org.joml.Vector3f; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - public class GesturePath { // The vectors that make up the path. @@ -16,18 +12,18 @@ public class GesturePath { * * @param vectors The vectors that make up the path. */ - public GesturePath(Vector3f[] vectors) - { - if ( vectors.length < 2) + public GesturePath(Vector3f[] vectors) { + if (vectors.length < 2) throw new IllegalArgumentException("A path must have at least two points."); this.segments = new PathSegment[vectors.length - 1]; - for ( int i = 0; i < vectors.length - 1; i++) + for (int i = 0; i < vectors.length - 1; i++) segments[i] = new PathSegment(vectors[i], vectors[i + 1]); } /** * Constructor for a GesturePath with provided PathSegments. + * * @param segments The PathSegments to initialize the path with. */ public GesturePath(PathSegment... segments) { @@ -43,6 +39,18 @@ public class GesturePath { return segments; } + /** + * Method for retrieving the vectors of the GesturePath. + */ + public Vector3f[] getVectors() { + Vector3f[] vectors = new Vector3f[segments.length + 1]; + vectors[0] = segments[0].getStart(); + for (int i = 0; i < segments.length; i++) + vectors[i + 1] = segments[i].getEnd(); + + return vectors; + } + /** * Method for retrieving the closest path segment to a reference point. * @@ -51,11 +59,11 @@ public class GesturePath { */ public PathSegment closest(Vector3f reference) { // If there's only one segment, return that one. - if ( segments.length == 1) + if (segments.length == 1) return segments[0]; PathSegment closest = segments[0]; - for ( int i = 1; i < segments.length; i++) + for (int i = 1; i < segments.length; i++) closest = PathSegment.closer(closest, segments[i], reference); return closest; @@ -71,48 +79,40 @@ public class GesturePath { return closest(referencePoint).difference(referencePoint); // Get the closest segment and calculate the error. } - // Builder class for the GesturePath object. - public static class Builder { - // List of vectors to add to the GesturePath object. - private final List vectors; + /** + * Function for converting a string to a GesturePath object. + * The input string bytes will be directly converted into 3d vectors. + * Every scalar is composed of 32 bits (4 characters), meaning 96 bits per vector. + *

+ * Note: ASCII to Vector conversion is done in Big Endian format (most significant byte first). + * + * @param input The string to convert + * @return The GesturePath object + */ - /** - * Constructor for the Builder object. - * - * @param vectors The list of vectors to add. - */ - public Builder(List vectors) { - this.vectors = vectors; + public static GesturePath fromString(String input) { + byte[] bytes = input.getBytes(); + + // Check if the input string contains a valid amount of bytes (12 bytes per vector) + if (input.length() % 12 != 0) { + throw new IllegalArgumentException("Invalid input string length"); } + Vector3f[] vectors = new Vector3f[input.length() / 12]; - /** - * Default constructor for the Builder object. - */ - public Builder() { - this.vectors = new ArrayList<>(); + float[] xyz = new float[3]; + for (int i = 0; i < bytes.length; i += 12) { + for (int j = 0; j < 3; j++) { + + xyz[j] = Float.intBitsToFloat( + (bytes[i + j * 4] & 0xFF) << 24 | + (bytes[i + j * 4 + 1] & 0xFF) << 16 | + (bytes[i + j * 4 + 2] & 0xFF) << 8 | + (bytes[i + j * 4 + 3] & 0xFF) + ); + } + vectors[i / 12] = new Vector3f(xyz[0], xyz[1], xyz[2]); } - - /** - * Adds a vector to the GesturePath object. - * - * @param vector The vector to add. - * @return The Builder object. - */ - public Builder addVector(Vector3f vector) { - vectors.add(vector); - return this; - } - - /** - * Builds the GesturePath object. - * - * @return The GesturePath object. - */ - public GesturePath build() { - return new GesturePath(vectors.toArray(new Vector3f[0])); - } - + return new GesturePath(vectors); } - } diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/path/PathSegment.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/path/PathSegment.java index 11183ca..4fc9984 100644 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/path/PathSegment.java +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/path/PathSegment.java @@ -12,7 +12,7 @@ public class PathSegment { * pointing straight upwards relative to the line, with a curvature of 0.0. * * @param start The starting point of the line segment - * @param end The end point of the line segment. + * @param end The end point of the line segment. */ public PathSegment(Vector3f start, Vector3f end) { this.start = start; @@ -25,7 +25,7 @@ public class PathSegment { * depending on the curvature of the curve. If the curvature is unset, or set to 0 * then this method will use linear interpolation. Otherwise, it will use a curve. * - * @param t The interpolation value between 0 and 1. + * @param t The interpolation value between 0 and 1. */ public Vector3f interpolate(double t) { return new Vector3f(this.start) @@ -56,7 +56,7 @@ public class PathSegment { (float) (this.start.x + t * (this.end.x - this.start.x)), (float) (this.start.y + t * (this.end.y - this.start.y)), (float) (this.start.z + t * (this.end.z - this.start.z)) - )); + )); } /** @@ -84,7 +84,7 @@ public class PathSegment { * @return The distance to the closest point on the path segment. */ public double distance(Vector3f reference) { - if ( this.start.distanceSquared(reference) > this.end.distanceSquared(reference)) + if (this.start.distanceSquared(reference) > this.end.distanceSquared(reference)) return this.end.distance(reference); return this.start.distance(reference); } @@ -92,8 +92,8 @@ public class PathSegment { /** * Function for returning the closest path segment to a reference point. * - * @param first The first path segment to compare. - * @param second The second path segment to compare. + * @param first The first path segment to compare. + * @param second The second path segment to compare. * @param referencePoint The reference point to compare to. * @return The closest path segment to the reference point. */ diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/IInputHandler.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/IInputHandler.java new file mode 100644 index 0000000..9d265fd --- /dev/null +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/IInputHandler.java @@ -0,0 +1,15 @@ +package com.example.fitbot.util.processing; + +import org.joml.Vector3f; + +public interface IInputHandler { + + /** + * Function for accepting motion data and the transformed vector. + * + * @param rotationVector The rotation vector of the motion data. + * @param sensorId The sensor ID of the motion data. + */ + void accept(Vector3f rotationVector, int sensorId); + +} \ No newline at end of file diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/IMotionDataConsumer.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/IMotionDataConsumer.java deleted file mode 100644 index dd3220c..0000000 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/IMotionDataConsumer.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.fitbot.util.processing; - -import org.joml.Vector3f; - -public interface IMotionDataConsumer { - - /** - * Function for accepting motion data and the transformed vector. - * @param transformedVector The transformed vector. - * @param motionData The input motion data. - * @param sampleIndex The index of the current sample - * @param sampleRate The sample rate. - */ - void accept(Vector3f transformedVector, MotionData motionData, int sampleIndex, double sampleRate, int sensorId); - -} \ No newline at end of file diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/InputProcessor.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/InputProcessor.java new file mode 100644 index 0000000..c78bd89 --- /dev/null +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/InputProcessor.java @@ -0,0 +1,337 @@ +package com.example.fitbot.util.processing; + +import android.util.Log; + +import com.example.fitbot.exercise.Exercise; +import com.example.fitbot.exercise.ExerciseManager; +import com.example.fitbot.util.server.WebServer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import org.jetbrains.annotations.NotNull; +import org.joml.Vector3f; + +import java.util.ArrayList; +import java.util.List; + +public class InputProcessor { + + private Vector3f[][] selfRotationVectorPaths; // Relative path of the motion data + private Vector3f[][] targetRotationVectorPaths; // Target path of the motion data + + private final float sampleRate; // The sample rate of the motion sensor + private float exerciseDurationInSeconds; + + /** + * This field is used to determine if the motion data is being recorded. + * If this is the case, instead of functioning normally, the element + * will record the movement that the user makes, store it in the + * `selfRotationVectorPaths` field and send it to the server. + */ + private boolean recordingMovement = false; + + /** + * Represents the duration of the recording in seconds. + * This field only has effect when the `recordingMovement` field is set to true. + */ + private float recordingDurationInSeconds = 0.0f; + + // How many seconds have passed since the start of the exercise. + // The exercise starts whenever the `startListening` function is called. + private double secondsPassed = 0.0D; + private long lastTime; + + private IInputHandler motionDataConsumer; + + private static final String[] REQUIRED_SENSOR_JSON_PROPERTIES = + {"rotationX", "rotationY", "rotationZ", "type", "deviceId"}; + + // The web server that listens for incoming motion data. + private WebServer server; + + + /** + * Constructor for the motion processor. + * + * @param paths The target paths of the motion data. + * The length of this array must be equal to the + * amount of sensors available. + * @param inputSampleRate The sample rate of the motion sensor. + */ + public InputProcessor(Vector3f[][] paths, float exerciseTime, float inputSampleRate) { + selfRotationVectorPaths = new Vector3f[paths.length][(int) (exerciseTime * inputSampleRate)]; + targetRotationVectorPaths = paths; + + this.sampleRate = inputSampleRate; + this.exerciseDurationInSeconds = exerciseTime; + } + + /** + * Function for setting the exercise to use. + * This updates the user and target path and the + * duration of the exercise. + * + * @param exercise The exercise to use the paths for. + */ + public void useExercise(Exercise exercise) { + if ( this.recordingMovement ) + throw new IllegalStateException("Cannot change exercise while recording movement."); + + this.selfRotationVectorPaths = new Vector3f[2][(int) (exercise.exerciseTimeInSeconds * this.sampleRate)]; + this.targetRotationVectorPaths = new Vector3f[2][exercise.rightPath.getVectors().length]; + this.exerciseDurationInSeconds = exercise.exerciseTimeInSeconds; + this.secondsPassed = 0.0D; + this.lastTime = System.currentTimeMillis(); + } + + /** + * Function for setting whether the motion data + * should be recorded or not. + * + * @param recording Whether the motion data should be recorded. + * @param duration For how long the motion data should be recorded. + * This only has an effect if `recording` is true. + */ + public void setRecording(boolean recording, float duration) { + this.recordingMovement = recording; + this.recordingDurationInSeconds = duration; + if (recording) { + this.secondsPassed = 0.0D; + this.lastTime = System.currentTimeMillis(); + + } + } + + /** + * Function for checking if the exercise or recording has finished. + * This function will return true if the execution of the exercise has finished or + * if the recording has finished, depending on the state of the `recordingMovement` field. + * + * @return Whether the exercise or recording has finished. + */ + public boolean hasFinished() { + return this.recordingMovement ? + (this.secondsPassed >= this.recordingDurationInSeconds) : + (this.secondsPassed >= this.exerciseDurationInSeconds); + } + + /** + * Function for starting the listening process + * of the motion sensor. This function will create + * a new WebSocket server and start listening for + * incoming connections. + */ + public void startListening() { + // Create socket server + this.server = WebServer.createServer(); + + Log.i("MotionProcessor", "Listening for incoming connections."); + + // Check if the socket + if (server != null) { + // Update event handler to match our functionality. + server.setEventHandler(this::parsePacket); + this.secondsPassed = 0.0d; + this.lastTime = System.currentTimeMillis(); + } + } + + /** + * Function for stopping the listening process + * of the motion sensor. This function will stop + * the WebSocket server. + */ + public void stopListening() { + if (server != null) { + server.stop(); + server = null; + } + } + + /** + * Function for parsing arbitrary packet data. + * + * @param data The data to parse. + */ + public void parsePacket(@NotNull String data) { + + try { + + Log.i("MotionProcessor", "Received packet data: " + data); + + JsonElement json = JsonParser.parseString(data); + + if (!json.isJsonObject()) + return; + + JsonObject object = json.getAsJsonObject(); + + // Ensure all properties are present in the received JSON object + for (String s : REQUIRED_SENSOR_JSON_PROPERTIES) { + if (!object.has(s)) + return; + } + + // Parse the data + Vector3f rotation = new Vector3f(object.get("rotationX").getAsFloat(), object.get("rotationY").getAsFloat(), object.get("rotationZ").getAsFloat()); + int deviceId = object.get("deviceId").getAsInt(); + String type = object.get("type").getAsString(); + + parseRotationVector(rotation, deviceId); + } catch (Exception e) { + Log.i("MotionProcessor", "Failed to parse packet data."); + } + } + + /** + * Function for adding motion data to the processor. + * + * @param rotation The rotation vector of the motion data. + * @param deviceId The device ID of the motion data. + */ + public void parseRotationVector(Vector3f rotation, int deviceId) { + if (deviceId >= 0 && deviceId < selfRotationVectorPaths.length) { + + // Re-calculate time for index calculation + secondsPassed = (System.currentTimeMillis() - lastTime) / 1000.0d; + lastTime = System.currentTimeMillis(); + + // Supposed index of the current rotation vector in the `rotationVectorPaths` array + int selfIndex = (int) (secondsPassed * sampleRate); + + motionDataConsumer.accept(rotation, deviceId); + if (selfIndex >= selfRotationVectorPaths[deviceId].length || selfIndex < 0) + return; + + selfRotationVectorPaths[deviceId][selfIndex] = rotation; + + if ( this.recordingMovement && this.hasFinished()) { + // Do something with the recorded data. + this.recordingMovement = false; + // Convert recorded data from `selfRotationVectorPaths` to string, and + // publish to database, or do something else with it. + + String converted = convertRecordedDataToString(); + + // Do something with it + Log.i("MotionProcessor", "Converted data: "); + Log.i("MotionProcessor", converted); + } + } + } + + /** + * Function for converting the recorded data to a string. + * This function will convert the recorded data to a string + * that can be sent to a database or other storage. + * + * @return The converted string. + */ + private String convertRecordedDataToString() + { + // First, remove empty entries + StringBuilder pathBuilder = new StringBuilder(); + + int[] intBits = new int[3]; + char[] vectorChars = new char[12]; // 4 bytes per scalar, 12 chars per vector + + // Iterate over all devices. In the current instance, it's 2. + for ( int deviceId = 0; deviceId < selfRotationVectorPaths.length; deviceId++) { + for (Vector3f dataPoint : selfRotationVectorPaths[deviceId]) { + if (dataPoint != null) { + // Convert float to int bits for conversion to char + intBits[0] = Float.floatToIntBits(dataPoint.x); + intBits[1] = Float.floatToIntBits(dataPoint.y); + intBits[2] = Float.floatToIntBits(dataPoint.z); + + // Convert int bits to char, in Big Endian order. + // This is important for converting back to float later. + for (int i = 0; i < 3; i++) { + vectorChars[i * 4] = (char) (intBits[i] >> 24); + vectorChars[i * 4 + 1] = (char) (intBits[i] >> 16); + vectorChars[i * 4 + 2] = (char) (intBits[i] >> 8); + vectorChars[i * 4 + 3] = (char) intBits[i]; + } + + pathBuilder.append(vectorChars); + } + } + // Add a separator between devices + if ( deviceId < selfRotationVectorPaths.length - 1) + pathBuilder.append(ExerciseManager.PATH_DELIMITER); + } + return pathBuilder.toString(); + } + + /** + * Method for getting the current progress of the exercise. + * The return value will range between 0.0 and 1.0. + * + * @return The current progress of the exercise. + */ + public double getCurrentProgress() { + return secondsPassed / exerciseDurationInSeconds; + } + + /** + * Function for setting the motion data receiver. + * + * @param consumer The consumer to set. + */ + public void setInputHandler(IInputHandler consumer) { + if (consumer != null) + this.motionDataConsumer = consumer; + } + + /** + * Function for getting the error offsets of the user's path compared to the + * target path at a given point in time. + * + * @param sensorId The sensor ID to get the error offsets from. + * @param time The time to get the error offsets from. + * This value must be >= 0 && <= exerciseTime, otherwise + * the error will be 0 by default. + * @return A list of error offsets of the motion data compared to the reference path. + */ + public double getError(int sensorId, float time) { + + // Ensure the sensor ID is within the bounds of the array + if (sensorId < 0 || sensorId >= selfRotationVectorPaths.length) + return 0.0d; + + // Index of the current rotation vector + int targetIndex = (int) ((this.exerciseDurationInSeconds / this.targetRotationVectorPaths[sensorId].length) * time); + int selfIndex = (int) (this.selfRotationVectorPaths[sensorId].length / this.sampleRate * time); + + // Ensure the indexes are within the bounds of the array + if ( + targetIndex >= 0 && targetIndex <= this.targetRotationVectorPaths[sensorId].length - 1 && + selfIndex >= 0 && selfIndex <= this.selfRotationVectorPaths[sensorId].length - 1 && + this.selfRotationVectorPaths[sensorId][selfIndex] != null && + this.targetRotationVectorPaths[sensorId][targetIndex] != null + ) { + return this.selfRotationVectorPaths[sensorId][selfIndex].distance(this.targetRotationVectorPaths[sensorId][targetIndex]); + } + return 0.0d; + } + + /** + * Method for getting the average error of the motion data + * compared to the reference path. + * + * @param sensorId The sensor ID to get the error offsets from. + * @return The average error of the motion data compared to the reference path. + */ + public double getAverageError(int sensorId) { + double error = 0; + for (int i = 0; i < this.exerciseDurationInSeconds; i++) { + error += getError(sensorId, i); + } + return error / this.exerciseDurationInSeconds; + } + + public float secondsPassed() { + return (float) secondsPassed; + } +} diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/MotionData.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/MotionData.java deleted file mode 100644 index fae42fd..0000000 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/MotionData.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.example.fitbot.util.processing; - -import org.joml.Vector3f; - -import java.util.Objects; - -public class MotionData { - - // Data of the motion sensor - public Vector3f acceleration, rotation; - public int sensorId; - - // Delimiter for the data received from the motion sensor - private static final String DATA_DELIMITER = ";"; - - /** - * Constructor for the MotionData class. - * - * @param accelerationX The acceleration in the X axis in m/s^2. - * @param accelerationY The acceleration in the Y axis in m/s^2. - * @param accelerationZ The acceleration in the Z axis in m/s^2. - * @param rotationX The rotation in the X axis in degrees. - * @param rotationY The rotation in the Y axis in degrees. - * @param rotationZ The rotation in the Z axis in degrees. - * @param sensorId The sensor id. - */ - public MotionData(float accelerationX, float accelerationY, float accelerationZ, float rotationX, float rotationY, float rotationZ, int sensorId) { - this(new Vector3f(accelerationX, accelerationY, accelerationZ), new Vector3f(rotationX, rotationY, rotationZ), sensorId); - } - - /** - * Constructor for the MotionData class. - * - * @param acceleration The acceleration vector in m/s^2. - * @param rotation The rotation vector in degrees. - */ - public MotionData(Vector3f acceleration, Vector3f rotation, int sensorId) { - this.acceleration = acceleration; - this.rotation = rotation; - this.sensorId = sensorId; - } - - /** - * Function for decoding a string into a MotionData object. - * This string must contain the data of the motion sensor - * separated by the delimiter. (;) - * - * @param data The string containing the data of the motion sensor. - * @return An instance of MotionData. - */ - public static MotionData decode(String data) { - Objects.requireNonNull(data); // Ensure data is not null - - String[] parts = data.split(DATA_DELIMITER); - if (parts.length != 7) - return null; - - return new MotionData( - Float.parseFloat(parts[0]), - Float.parseFloat(parts[1]), - Float.parseFloat(parts[2]), - Float.parseFloat(parts[3]), - Float.parseFloat(parts[4]), - Float.parseFloat(parts[5]), - Integer.parseInt(parts[6]) - ); - } -} 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 deleted file mode 100644 index 875f731..0000000 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/MotionProcessor.java +++ /dev/null @@ -1,228 +0,0 @@ -package com.example.fitbot.util.processing; - -import android.util.Log; - -import com.example.fitbot.util.path.GesturePath; -import com.example.fitbot.util.server.WebServer; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; - -import org.jetbrains.annotations.NotNull; -import org.joml.Vector3f; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -public class MotionProcessor { - - public static final String DELIMITER = ";"; - - private final List relativeLeftPath = new ArrayList<>(); // Relative path of the left motion data - private final List relativeRightPath = new ArrayList<>(); // Relative path of the motion data - - private Vector3f ZERO = new Vector3f(0, 0, 0); - - private final float sampleRate = 10.0F; // samples/second - - private IMotionDataConsumer motionDataConsumer = (p1, p2, p3, p4, p5) -> { }; - private WebServer server; - - - public MotionProcessor() {} - - - /** - * Function for starting the listening process - * of the motion sensor. This function will create - * a new WebSocket server and start listening for - * incoming connections. - */ - public void startListening() { - // Create socket server - this.server = WebServer.createServer(); - - Log.i("MotionProcessor", "Listening for incoming connections."); - - // Check if the socket - if (server != null) { - // Update event handler to match our functionality. - server.setEventHandler(this::parsePacket); - } - } - - /** - * Function for stopping the listening process - * of the motion sensor. This function will stop - * the WebSocket server. - */ - public void stopListening() { - if (server != null) { - server.stop(); - } - } - - /** - * Function for parsing arbitrary packet data. - * - * @param data The data to parse. - */ - public void parsePacket(@NotNull String data) { - - try { - - Log.i("MotionProcessor", "Received packet data: " + data); - - JsonElement json = JsonParser.parseString(data); - - if (!json.isJsonObject()) - return; - - JsonObject object = json.getAsJsonObject(); - - String[] required = { - "rotationX", "rotationY", "rotationZ", - "accelerationX", "accelerationY", "accelerationZ", - "type", - "deviceId" - }; - - // Ensure all properties are present in the received JSON object - for (String s : required) { - if (!object.has(s)) - return; - } - - // Parse the data - Vector3f rotation = new Vector3f(object.get("rotationX").getAsFloat(), object.get("rotationY").getAsFloat(), object.get("rotationZ").getAsFloat()); - Vector3f acceleration = new Vector3f(object.get("accelerationX").getAsFloat(), object.get("accelerationY").getAsFloat(), object.get("accelerationZ").getAsFloat()); - int deviceId = object.get("deviceId").getAsInt(); - String type = object.get("type").getAsString(); - MotionData motionData = new MotionData(rotation, acceleration, deviceId); - - if (type.equals("calibrate")) { - ZERO = getRelativeVector(motionData); - return; - } - - addMotionData(motionData); - } catch (Exception e) { - // Don't do anything ... just ignore the exception - Log.i("MotionProcessor", "Failed to parse packet data."); - } - } - - /** - * Function for adding motion data to the processor. - * - * @param data The motion data to add. - */ - public void addMotionData(MotionData data) { - List target; - if (data.sensorId == 0) - target = relativeLeftPath; - else target = relativeRightPath; - Vector3f previous = target.isEmpty() ? ZERO : target.get(target.size() - 1); - Vector3f relativeVector = getRelativeVector(data).add(previous); - target.add(relativeVector); - motionDataConsumer.accept(relativeVector, data, target.size(), this.sampleRate, data.sensorId); - } - - /** - * Function for updating the relative path. - * - * @param relativeRightPath The new relative path. - */ - public void setRelativePaths(List relativeLeftPath, List relativeRightPath) { - this.relativeRightPath.clear(); - this.relativeLeftPath.clear(); - this.relativeLeftPath.addAll(relativeLeftPath); - this.relativeRightPath.addAll(relativeRightPath); - } - - /** - * Function for setting the motion data receiver. - * - * @param consumer The consumer to set. - */ - public void setMotionDataEventHandler(IMotionDataConsumer consumer) { - if (consumer != null) - this.motionDataConsumer = consumer; - } - - /** - * Function for getting the relative vector of the motion data. - * This function will calculate the relative position of the motion data - * based on its acceleration and rotation vectors. This has to be done since - * the acceleration vector is relative to its own rotation vector. - * - * @param motionData The motion data to calculate the relative vector for. - * @return The relative vector of the motion data. - */ - public Vector3f getRelativeVector(MotionData motionData) { - - // Rotate the acceleration vector back by the rotation vector to make it - // perpendicular to the gravity vector, then apply double integration to get the relative position. - // s = 1/2 * a * t^2 - return motionData.acceleration - .rotateX(-motionData.rotation.x) - .rotateY(-motionData.rotation.y) - .rotateZ(-motionData.rotation.z) - .div(2) - .mul(sampleRate * sampleRate); - } - - /** - * Function for getting the error offsets of the provided path and the - * received motion data. - * - * @param referencePath The reference path to compare the motion data to. - * @return A list of error offsets of the motion data compared to the reference path. - */ - public List getErrors(GesturePath referencePath) { - - List errors = new ArrayList<>(); - for (Vector3f vector : relativeRightPath) { - errors.add(referencePath.getError(vector)); - } - return errors; - } - - /** - * Function for getting the error of the motion data compared to the reference path. - * - * @param path The path to compare the motion data to. - * @param referencePoint The reference point to compare the motion data to. - * @return The error of the motion data compared to the reference path. - */ - public double getError(GesturePath path, Vector3f referencePoint) { - return path.getError(referencePoint); - } - - /** - * Function for calculating the average error of the motion data - * compared to the reference path. - * - * @param referencePath The reference path to compare the motion data to. - * @return The average error of the motion data compared to the reference path. - */ - public double getAverageError(GesturePath referencePath, int sensorId) { - double error = 0; - for (Double e : getErrors(referencePath)) { - error += e; - } - return error / Math.max(1, (sensorId == 0 ? relativeLeftPath : relativeRightPath).size()); - } - - /** - * Function for logging the statistics of the motion data. - * - * @param referencePath The reference path to compare the motion data to. - */ - public void logStatistics(GesturePath referencePath) { - Log.i("MotionProcessor", "Path length: " + relativeRightPath.size()); - Log.i("MotionProcessor", "Sample rate: " + sampleRate); - Log.i("MotionProcessor", "Calibration point: " + ZERO.toString()); - } -} 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 index 98739b4..67327cb 100644 --- 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 @@ -1,7 +1,5 @@ package com.example.fitbot.util.server; -import java.net.Socket; - /** * Interface for handling WebSocket events. */ 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 index 7fe001a..87dc331 100644 --- 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 @@ -10,19 +10,16 @@ import java.io.OutputStream; import java.net.InetSocketAddress; 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 IWebServerHandler eventHandler = (input) -> {}; // No-op. + protected IWebServerHandler eventHandler = (input) -> { + }; // No-op. private Thread thread; - private AtomicBoolean forceClose = new AtomicBoolean(false); + private final AtomicBoolean forceClose = new AtomicBoolean(false); /** * Constructor for creating a new WebSocket server. @@ -84,7 +81,7 @@ public class WebServer implements Runnable { String[] data = builder.toString().split("\n\n"); - if ( data.length > 1) { // Check if the data is valid. + if (data.length > 1) { // Check if the data is valid. this.eventHandler.onReceive(data[1]); } diff --git a/code/src/Fitbot/app/src/main/res/drawable/darkred_button_gradient.xml b/code/src/Fitbot/app/src/main/res/drawable/big_red_button_gradient.xml similarity index 63% rename from code/src/Fitbot/app/src/main/res/drawable/darkred_button_gradient.xml rename to code/src/Fitbot/app/src/main/res/drawable/big_red_button_gradient.xml index 0ade340..878f41e 100644 --- a/code/src/Fitbot/app/src/main/res/drawable/darkred_button_gradient.xml +++ b/code/src/Fitbot/app/src/main/res/drawable/big_red_button_gradient.xml @@ -2,11 +2,12 @@ + \ No newline at end of file diff --git a/code/src/Fitbot/app/src/main/res/drawable/border_background.xml b/code/src/Fitbot/app/src/main/res/drawable/border_background.xml new file mode 100644 index 0000000..7ea69e6 --- /dev/null +++ b/code/src/Fitbot/app/src/main/res/drawable/border_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/code/src/Fitbot/app/src/main/res/drawable/border_background_2.xml b/code/src/Fitbot/app/src/main/res/drawable/border_background_2.xml new file mode 100644 index 0000000..0fb955b --- /dev/null +++ b/code/src/Fitbot/app/src/main/res/drawable/border_background_2.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/code/src/Fitbot/app/src/main/res/drawable/border_background_3.xml b/code/src/Fitbot/app/src/main/res/drawable/border_background_3.xml new file mode 100644 index 0000000..4543b55 --- /dev/null +++ b/code/src/Fitbot/app/src/main/res/drawable/border_background_3.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/code/src/Fitbot/app/src/main/res/drawable/box_background.xml b/code/src/Fitbot/app/src/main/res/drawable/box_background.xml new file mode 100644 index 0000000..53f8ad7 --- /dev/null +++ b/code/src/Fitbot/app/src/main/res/drawable/box_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/code/src/Fitbot/app/src/main/res/drawable/fitbot_launcher_background.xml b/code/src/Fitbot/app/src/main/res/drawable/fitbot_launcher_background.xml new file mode 100644 index 0000000..ca3826a --- /dev/null +++ b/code/src/Fitbot/app/src/main/res/drawable/fitbot_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/code/src/Fitbot/app/src/main/res/drawable/ic_baseline_check_48.xml b/code/src/Fitbot/app/src/main/res/drawable/ic_baseline_check_48.xml new file mode 100644 index 0000000..c4f67b4 --- /dev/null +++ b/code/src/Fitbot/app/src/main/res/drawable/ic_baseline_check_48.xml @@ -0,0 +1,5 @@ + + + diff --git a/code/src/Fitbot/app/src/main/res/drawable/ic_baseline_close_48.xml b/code/src/Fitbot/app/src/main/res/drawable/ic_baseline_close_48.xml new file mode 100644 index 0000000..72f8573 --- /dev/null +++ b/code/src/Fitbot/app/src/main/res/drawable/ic_baseline_close_48.xml @@ -0,0 +1,5 @@ + + + diff --git a/code/src/Fitbot/app/src/main/res/drawable/ic_baseline_info_24.xml b/code/src/Fitbot/app/src/main/res/drawable/ic_baseline_info_24.xml new file mode 100644 index 0000000..17255b7 --- /dev/null +++ b/code/src/Fitbot/app/src/main/res/drawable/ic_baseline_info_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/code/src/Fitbot/app/src/main/res/drawable/ic_baseline_info_40.xml b/code/src/Fitbot/app/src/main/res/drawable/ic_baseline_info_40.xml new file mode 100644 index 0000000..df12462 --- /dev/null +++ b/code/src/Fitbot/app/src/main/res/drawable/ic_baseline_info_40.xml @@ -0,0 +1,5 @@ + + + diff --git a/code/src/Fitbot/app/src/main/res/drawable/ic_baseline_info_48.xml b/code/src/Fitbot/app/src/main/res/drawable/ic_baseline_info_48.xml new file mode 100644 index 0000000..15875d1 --- /dev/null +++ b/code/src/Fitbot/app/src/main/res/drawable/ic_baseline_info_48.xml @@ -0,0 +1,5 @@ + + + diff --git a/code/src/Fitbot/app/src/main/res/drawable/ic_baseline_skip_next_48.xml b/code/src/Fitbot/app/src/main/res/drawable/ic_baseline_skip_next_48.xml new file mode 100644 index 0000000..400f0a5 --- /dev/null +++ b/code/src/Fitbot/app/src/main/res/drawable/ic_baseline_skip_next_48.xml @@ -0,0 +1,5 @@ + + + diff --git a/code/src/Fitbot/app/src/main/res/drawable/ic_launcher_background.xml b/code/src/Fitbot/app/src/main/res/drawable/ic_launcher_background.xml index 3050a42..3a8cd35 100644 --- a/code/src/Fitbot/app/src/main/res/drawable/ic_launcher_background.xml +++ b/code/src/Fitbot/app/src/main/res/drawable/ic_launcher_background.xml @@ -1,170 +1,74 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + xmlns:android="http://schemas.android.com/apk/res/android"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/code/src/Fitbot/app/src/main/res/drawable/rectangle.xml b/code/src/Fitbot/app/src/main/res/drawable/rectangle.xml deleted file mode 100644 index 20ab824..0000000 --- a/code/src/Fitbot/app/src/main/res/drawable/rectangle.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/code/src/Fitbot/app/src/main/res/drawable/red_button_gradient.xml b/code/src/Fitbot/app/src/main/res/drawable/red_button_gradient.xml index 7190f99..a087a43 100644 --- a/code/src/Fitbot/app/src/main/res/drawable/red_button_gradient.xml +++ b/code/src/Fitbot/app/src/main/res/drawable/red_button_gradient.xml @@ -9,4 +9,5 @@ android:startColor="#990000" android:endColor="#FF0000" android:angle="90"/> + \ No newline at end of file diff --git a/code/src/Fitbot/app/src/main/res/layout/activity_end_screen.xml b/code/src/Fitbot/app/src/main/res/layout/activity_end_screen.xml index b07bcad..35aa21a 100644 --- a/code/src/Fitbot/app/src/main/res/layout/activity_end_screen.xml +++ b/code/src/Fitbot/app/src/main/res/layout/activity_end_screen.xml @@ -1,77 +1,113 @@ + android:background="@color/darkBlue" + tools:context=".ui.activities.HelpActivity"> - + app:layout_constraintTop_toTopOf="parent"> + + + + + + + + + + + + + + + + +