Skip to content

Commit

Permalink
Merge pull request hpi-studyu#520 from hpi-studyu/feat/multimodal-rev…
Browse files Browse the repository at this point in the history
…ised

Feat/multimodal revised
  • Loading branch information
johannesvedder authored Apr 17, 2024
2 parents 984b58b + a13754b commit 93dc03c
Show file tree
Hide file tree
Showing 79 changed files with 25,112 additions and 21,306 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/e2e_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ jobs:
run: flutter config --enable-web
- name: Initialize Supabase
run: |
dart pub global activate melos
melos bootstrap
dart pub get
docker network create studyu_network || true
cp docker/supabase/.env.example docker/supabase/.env
cp docker/proxy/.env.example docker/proxy/.env
Expand Down
5 changes: 3 additions & 2 deletions app/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ android {
defaultConfig {
applicationId "health.studyu.app"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion flutter.minSdkVersion
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration
// camera_android requires at least sdk 21
minSdkVersion Math.max(flutter.minSdkVersion, 21)
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
Expand Down
17 changes: 17 additions & 0 deletions app/lib/l10n/app_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,23 @@

"could_not_save_results": "Ergebnisse konnten nicht gespeichert werden.",

"take_a_photo": "Mache ein Foto",
"start_recording": "Starte eine Aufnahme",
"stop_recording": "Stoppe die Aufnahme",
"error_recording": "Fehler bei der Aufnahme",
"photo_captured": "Foto aufgenommen",
"audio_recorded": "Audio aufgenommen",
"data_captured": "Daten aufgenommen",
"multimodal_not_supported": "Multimodale Aufgaben werden in der Web-Version zur Zeit nicht unterstützt. Bitte verwenden Sie die App Version für iOS oder Android.",
"camera_access_denied": "Zugriff auf Kamera verweigert",
"no_camera_available": "Keine Kamera verfügbar",
"microphone_access_denied": "Zugriff auf Mikrofon verweigert",
"camera_error": "Kamerafehler",
"recording_error": "Aufnahme fehlgeschlagen",
"storing_photo": "Foto wird gespeichert",
"storing_audio": "Audio wird gespeichert",
"upload_error": "Die Datei konnte nicht hochgeladen werden",

"language": "Sprache",
"en": "Englisch",
"de": "Deutsch",
Expand Down
17 changes: 16 additions & 1 deletion app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,22 @@

"could_not_save_results": "Could not save results",

"take_a_photo": "Take a photo",
"start_recording": "Start recording",
"stop_recording": "Stop recording",
"error_recording": "Error occurred during recording",
"photo_captured": "Photo captured",
"audio_recorded": "Audio recorded",
"multimodal_not_supported": "Multimodal Trials are currently not supported to run in a web browser. Please use the StudyU App for Android or iOS.",
"camera_access_denied": "Camera access denied",
"no_camera_available": "No camera available",
"microphone_access_denied": "Microphone access denied",
"camera_error": "Camera error",
"recording_error": "Recording error",
"storing_photo": "The photo is being stored",
"storing_audio": "The audio file is being stored",
"upload_error": "The file could not be uploaded",

"language": "Language",
"en": "English",
"de": "German",
Expand Down Expand Up @@ -154,7 +170,6 @@
"report_outcome_inconclusive": "The results are inconclusive. There does not seem to be a statistically significant difference between the interventions.",
"report_outcome_neither": "Both interventions seem to have a negative effect on the outcome for you.",
"report_outcome_one": "The intervention {intervention} seems to improve the outcome for you.",

"report_axis_phase": "Phase",

"study_not_started": "Your study has not started yet. Please check back tomorrow!",
Expand Down
3 changes: 1 addition & 2 deletions app/lib/screens/app_onboarding/loading_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -253,8 +253,7 @@ class _LoadingScreenState extends State<LoadingScreen> {
}*/

/*Future<bool> migrateParticipantToNewDB(String selectedStudyObjectId) async {
final prefs = await SharedPreferences.getInstance();
if (prefs.containsKey(userEmailKey) && prefs.containsKey(userPasswordKey)) {
if (await SecureStorage.containsKey(userEmailKey) && await SecureStorage.containsKey(userPasswordKey)) {
try {
// create new account
if (await anonymousSignUp()) {
Expand Down
213 changes: 213 additions & 0 deletions app/lib/screens/study/multimodal/capture_picture_screen.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import 'dart:async';
import 'dart:io';

import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:studyu_app/util/temporary_storage_handler.dart';

class CapturePictureScreen extends StatefulWidget {
final String userId;
final String studyId;

const CapturePictureScreen({super.key, required this.userId, required this.studyId});

@override
State<CapturePictureScreen> createState() => _CapturePictureScreenState();
}

class _CapturePictureScreenState extends State<CapturePictureScreen> with WidgetsBindingObserver {
CameraController? _cameraController;
List<CameraDescription>? _cameras;
int _jumpToNextCameraTaps = 0;
bool _isTakingPicture = false;

@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
await _initializeCameraController();
});
}

@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_cameraController?.dispose();
super.dispose();
}

@override
void didChangeAppLifecycleState(AppLifecycleState state) async {
final CameraController? cameraController = _cameraController;

// App state changed before we got the chance to initialize.
if (cameraController == null || cameraController.value.isInitialized) {
return;
}

if (state == AppLifecycleState.inactive) {
await cameraController.dispose();
} else if (state == AppLifecycleState.resumed) {
await _initializeCameraController();
}
}

Future<void> _initializeCameraController() async {
try {
final oldCameraController = _cameraController;
final cameras = _cameras ?? await _getAvailableCameras();
_cameras = cameras;
if (cameras.isEmpty) {
throw CameraException("NoCameraAvailable", "No cameras are available");
}
if (oldCameraController != null) {
await oldCameraController.dispose();
}
final cameraIndex = _jumpToNextCameraTaps % cameras.length;
final newCameraController = CameraController(
cameras[cameraIndex],
ResolutionPreset.max,
enableAudio: false,
imageFormatGroup: ImageFormatGroup.jpeg,
);
await newCameraController.initialize();
setState(() {
_cameraController = newCameraController;
});
} catch (e) {
if (!mounted) return;
String errorText = AppLocalizations.of(context)!.camera_error;
if (e is CameraException) {
switch (e.code) {
case 'CameraAccessDenied':
case 'CameraAccessDeniedWithoutPrompt':
case 'CameraAccessRestricted':
errorText = AppLocalizations.of(context)!.camera_access_denied;
break;
case 'NoCameraAvailable':
errorText = AppLocalizations.of(context)!.no_camera_available;
break;
}
}

Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(errorText),
),
);
}
}

Future<List<CameraDescription>> _getAvailableCameras() async {
return (await availableCameras())
.where((CameraDescription aCameraDescription) =>
aCameraDescription.lensDirection == CameraLensDirection.back ||
aCameraDescription.lensDirection == CameraLensDirection.front)
.toList();
}

Future<void> _jumpToNextCamera() async {
_jumpToNextCameraTaps++;
await _initializeCameraController();
}

Future<void> _tryCapturePicture() async {
final cameraController = _cameraController;
if (cameraController == null || !cameraController.value.isInitialized || cameraController.value.isTakingPicture) {
return;
}

setState(() {
_isTakingPicture = true;
});

XFile image;
try {
image = await cameraController.takePicture();
} on Exception catch (e) {
debugPrint("Failed to take picture: $e");
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.camera_error),
),
);
setState(() {
_isTakingPicture = false;
});
return;
}

// Move the image to the staging directory
final imageFile = File(image.path);
final storage = TemporaryStorageHandler(widget.studyId, widget.userId);
final stagingImageFile = await storage.getStagingImage();
await imageFile.rename(stagingImageFile.localFilePath);

if (!mounted) return;
Navigator.pop(context, stagingImageFile);
}

@override
Widget build(BuildContext context) {
final cameraController = _cameraController;
return Scaffold(
appBar: AppBar(title: Text(AppLocalizations.of(context)!.take_a_photo)),
body: cameraController == null
? const Center(child: CircularProgressIndicator())
: Stack(
children: [
CameraPreview(cameraController),
_isTakingPicture
? Container(
color: Colors.black.withOpacity(0.5),
alignment: Alignment.center,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).dialogBackgroundColor,
borderRadius: BorderRadius.circular(10),
),
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(AppLocalizations.of(context)!.take_a_photo),
],
),
),
)
: const SizedBox.shrink(),
],
),
floatingActionButton: Wrap(
direction: Axis.horizontal,
children: [
Container(
margin: const EdgeInsets.all(10),
child: FloatingActionButton(
heroTag: "captureImage",
onPressed: cameraController != null && !_isTakingPicture
? () async {
await _tryCapturePicture();
}
: null,
child: const Icon(Icons.camera_alt),
),
),
Container(
margin: const EdgeInsets.all(10),
child: FloatingActionButton(
heroTag: "jumpToNextCamera",
onPressed: cameraController != null && !_isTakingPicture ? () async => await _jumpToNextCamera() : null,
child: const Icon(Icons.autorenew),
),
)
],
));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:studyu_app/screens/study/tasks/task_screen.dart';
import 'package:studyu_app/util/misc.dart';
import 'package:studyu_app/util/study_subject_extension.dart';
import 'package:studyu_core/core.dart';

class CheckmarkTaskWidget extends StatefulWidget {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:studyu_app/screens/study/tasks/task_screen.dart';
import 'package:studyu_app/util/misc.dart';
import 'package:studyu_app/util/study_subject_extension.dart';
import 'package:studyu_app/util/temporary_storage_handler.dart';
import 'package:studyu_core/core.dart';
import 'package:studyu_app/widgets/questionnaire/questionnaire_widget.dart';

Expand Down Expand Up @@ -38,6 +40,12 @@ class _QuestionnaireTaskWidgetState extends State<QuestionnaireTaskWidget> {
Navigator.pop(context, true);
}

@override
void dispose() {
super.dispose();
TemporaryStorageHandler.deleteAllStagingFiles();
}

@override
Widget build(BuildContext context) {
return Column(
Expand Down
9 changes: 3 additions & 6 deletions app/lib/util/app_analytics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:sentry_logging/sentry_logging.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:studyu_app/app.dart';
import 'package:studyu_app/models/app_state.dart';
import 'package:studyu_core/core.dart';
Expand All @@ -28,8 +27,7 @@ class AppAnalytics /*extends Analytics*/ {

static Future<void> init() async {
if (_userEnabled == null) {
final prefs = await SharedPreferences.getInstance();
_userEnabled = prefs.get(keyAnalyticsUserEnable) as bool?;
_userEnabled = await SecureStorage.readBool(keyAnalyticsUserEnable);
// analytics is enabled by default
_userEnabled ??= true;
}
Expand Down Expand Up @@ -69,8 +67,7 @@ class AppAnalytics /*extends Analytics*/ {
}

static void setEnabled(bool newEnabled) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(keyAnalyticsUserEnable, newEnabled);
await SecureStorage.write(keyAnalyticsUserEnable, newEnabled.toString());
if (!newEnabled) {
// a restart of the app will be necessary to enable sentry again
Sentry.close();
Expand All @@ -84,7 +81,7 @@ class AppAnalytics /*extends Analytics*/ {
final basicContext = {
'selectedStudyObjectId': await getActiveSubjectId(),
'isPreview': state.isPreview,
'sharedPrefsEmail': await getFakeUserEmail(),
'storedEmail': await getFakeUserEmail(),
};
scope.setContexts('basicState', basicContext);
},
Expand Down
Loading

0 comments on commit 93dc03c

Please sign in to comment.