Skip to content

Commit

Permalink
Merge pull request #432 from cph-cachet/419-upgrade-to-carp_serializa…
Browse files Browse the repository at this point in the history
…ble-v-20

Upgrade to carp serializable v 20
  • Loading branch information
bardram authored Oct 14, 2024
2 parents 40ae20f + bb7581a commit 5d086fa
Show file tree
Hide file tree
Showing 145 changed files with 1,484 additions and 1,172 deletions.
58 changes: 26 additions & 32 deletions apps/carp_mobile_sensing_app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,43 +26,29 @@ Since the [`SensingBLoC`](https://github.com/cph-cachet/carp.sensing-flutter/blo

````dart
class SensingBLoC {
/// The [Sensing] layer used in the app.
Sensing get sensing => Sensing();
/// What kind of deployment are we running? Default is local.
DeploymentMode deploymentMode => ...
/// The URI of the CAWS server to use depending on the current [deploymentMode].
String get uri => ...
/// The study id for the currently running deployment.
/// Returns the study id cached locally on the phone (if available).
/// Returns `null` if no study is deployed (yet).
String? get studyId => ...
/// The study deployment id for the currently running deployment.
/// Returns the deployment id cached locally on the phone (if available).
/// The study for the currently running study deployment.
/// The study is cached locally on the phone.
/// Returns `null` if no study is deployed (yet).
String? get studyDeploymentId => ...
/// The device role name for the currently running deployment.
/// Returns the role name cached locally on the phone (if available).
/// Returns `null` if no study is deployed (yet).
String? get deviceRoleName => ...
SmartphoneStudy? get study => ...
/// Use the cached study deployment?
bool get useCachedStudyDeployment => _useCached;
bool get useCachedStudyDeployment => ...
/// Should sensing be automatically resumed on app startup?
bool get resumeSensingOnStartup => _resumeSensingOnStartup;
bool get resumeSensingOnStartup => ...
/// The [SmartphoneDeployment] deployed on this phone.
SmartphoneDeployment? get deployment => bloc.sensing.controller?.deployment;
SmartphoneDeployment? get deployment => sensing.controller?.deployment;
/// What kind of deployment are we running. Default is local.
DeploymentMode deploymentMode = DeploymentMode.local;
/// The preferred format of the data to be uploaded according to
/// [NameSpace]. Default using the [NameSpace.CARP].
String dataFormat = NameSpace.CARP;
StudyDeploymentModel? _model;
/// Get the view model for this study [deployment].
Expand All @@ -84,15 +70,13 @@ class SensingBLoC {
/// Initialize the BLoC.
Future<void> initialize({
DeploymentMode deploymentMode = DeploymentMode.local,
String? deploymentId,
String dataFormat = NameSpace.CARP,
bool useCachedStudyDeployment = true,
bool resumeSensingOnStartup = false,
}) async {
await Settings().init();
Settings().debugLevel = DebugLevel.debug;
this.deploymentMode = deploymentMode;
if (deploymentId != null) studyDeploymentId = deploymentId;
this.dataFormat = dataFormat;
_resumeSensingOnStartup = resumeSensingOnStartup;
_useCached = useCachedStudyDeployment;
Expand All @@ -119,7 +103,7 @@ final bloc = SensingBLoC();

The BLoC basically plays three roles:

* it holds core business data like `studyId`, `deploymentId`, `deviceRoleName`, and the `deployment` configuration
* it holds core business data like the `study` and `deployment` configuration
* it can create view models such as the `StudyDeploymentViewModel` and the list of `ProbeViewModel`s and `DeviceViewModel`s
* it provide a set of life cycle methods for sensing like `initialize`, `connectToDevice` and `start`.

Expand All @@ -132,7 +116,7 @@ Configuration of sensing is done in the [`Sensing`](https://github.com/cph-cache
This class also illustrates how the app can be run both in a "local" deployment mode and in different "CAWS" modes. Depending on the deployment mode (local or using CAWS), deployment is initialized using the [`SmartphoneDeploymentService`](https://pub.dev/documentation/carp_mobile_sensing/latest/runtime/SmartphoneDeploymentService-class.html) or the [`CarpDeploymentService`](https://pub.dev/documentation/carp_webservices/latest/carp_services/CarpDeploymentService-class.html), respectively.

In the case a local deployment is used, a protocol is fetched from the `LocalStudyProtocolManager`, which is then added to the local `SmartphoneDeploymentService`.
In the case a CAWS deployment is used, the study deployment configuration will be fetched from the `CarpDeploymentService` based on the `studyDeploymentId` fetched from an invitation (this invitation is fetched as part of the `init` method of the main `App` class).
In the case a CAWS deployment is used, the study deployment configuration will be fetched from the `CarpDeploymentService` based on the `study`, which again is part of an `invitation` (this invitation is fetched as part of the `init` method of the main `App` class).

Once, the right deployment service is configured, the `SmartPhoneClientManager` singleton is configured and the study is added (based on the deployment id and the role name of the phone) and deployed.
When deployed, the runtime (`SmartphoneDeploymentController`) is configured and sampling can now be started or stopped. This part of `Sensing` is shown below:
Expand All @@ -142,12 +126,11 @@ When deployed, the runtime (`SmartphoneDeploymentController`) is configured and
// (local or CAWS), add the study, and deploy it.
await SmartPhoneClientManager().configure(
deploymentService: deploymentService,
askForPermissions: true,
);
study = await SmartPhoneClientManager().addStudy(
bloc.studyDeploymentId!,
bloc.deviceRoleName!,
);
await controller?.tryDeployment();
study = await SmartPhoneClientManager().addStudy(bloc.study!);
await controller?.tryDeployment(useCached: bloc.useCachedStudyDeployment);
await controller?.configure();
```

Expand Down Expand Up @@ -243,3 +226,14 @@ StreamBuilder<Measurement>(
_StudyControllerLine('${viewModel.samplingSize}',
heading: 'Sample Size')),
`````

## Technical Notes

The CARP Mobile Sensing Demo app makes use of many of the CAMS Sampling Packages. Each of these have their own requirements to work, which entails modification on how to configure and build the app on iOS and Android. You should pay special attention to the requirements described in the README of each sampling package. This often entails editing and modifying:

* the `info.plist` on iOS
* the `AndroidManifest.xml` file on Android
* the `MainActivity.kt` or `MainActivity.java` on Android
* the different `build.gradle` and `settings.gradle` files on Android

This example app also illustrates how these files should be configured.
58 changes: 23 additions & 35 deletions apps/carp_mobile_sensing_app/android/app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,52 +1,40 @@
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}

def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}

def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
plugins {
id "com.android.application"
id "kotlin-android"
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id "dev.flutter.flutter-gradle-plugin"
}

def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

android {
compileSdkVersion 34
namespace = "dk.cachet.carp_mobile_sensing_app"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}

defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "dk.cachet.carp_mobile_sensing_app"
minSdkVersion 28
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
applicationId = "dk.cachet.carp_mobile_sensing_app"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 28
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}

buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
signingConfig = signingConfigs.debug
}
}
}
Expand All @@ -57,5 +45,5 @@ dependencies {
}

flutter {
source '../..'
source = "../.."
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
<!-- The following permissions are used in the Connectivity Package -->
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>


<!-- Legacy Bluetooth permissions, which is needed on devices with API 30 (Android Q) or older. -->
<uses-permission
android:name="android.permission.BLUETOOTH"
Expand Down Expand Up @@ -132,6 +131,7 @@
<!-- Configuration of AppAuth - see also flutter_appauth plugin -->
<activity
android:name="net.openid.appauth.RedirectUriReceiverActivity"
android:theme="@style/Theme.AppCompat.NoActionBar"
android:exported="true"
tools:node="replace">
<intent-filter>
Expand Down Expand Up @@ -177,6 +177,18 @@
</intent-filter>
</activity>

<!-- Activity to show Permissions screen for Health Connect -->
<activity-alias
android:name="ViewPermissionUsageActivity"
android:exported="true"
android:targetActivity=".MainActivity"
android:permission="android.permission.START_VIEW_PERMISSION_USAGE">
<intent-filter>
<action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
<category android:name="android.intent.category.HEALTH_PERMISSIONS" />
</intent-filter>
</activity-alias>

<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dk.cachet.carp_mobile_sensing_app;

import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.android.FlutterFragmentActivity;

public class MainActivity extends FlutterActivity {
// Need to extend FlutterFragmentActivity to work with the Health Connect API
public class MainActivity extends FlutterFragmentActivity {
}
21 changes: 3 additions & 18 deletions apps/carp_mobile_sensing_app/android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,33 +1,18 @@
buildscript {
ext.kotlin_version = '1.9.0'
repositories {
google()
mavenCentral()
}

dependencies {
classpath 'com.android.tools.build:gradle:7.3.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

allprojects {
repositories {
google()
mavenCentral()
flatDir{
dirs "$rootDir/libs"
}

}
} }
}

rootProject.buildDir = '../build'
rootProject.buildDir = "../build"
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
project.evaluationDependsOn(":app")
}

tasks.register("clean", Delete) {
Expand Down
31 changes: 23 additions & 8 deletions apps/carp_mobile_sensing_app/android/settings.gradle
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
include ':app'
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()

def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")

assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}

def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false
// id "org.jetbrains.kotlin.android" version "1.7.10" apply false
id "org.jetbrains.kotlin.android" version "2.0.0" apply false
}

include ":app"
6 changes: 2 additions & 4 deletions apps/carp_mobile_sensing_app/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
library mobile_sensing_app;

import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart' hide TimeOfDay;
import 'package:flutter/services.dart';
Expand Down Expand Up @@ -53,8 +52,7 @@ void main() async {

// Initialize the bloc, setting the deployment mode.
await bloc.initialize(
deploymentMode: DeploymentMode.local,
// deploymentId: testDeploymentId,
deploymentMode: DeploymentMode.dev,
useCachedStudyDeployment: false,
resumeSensingOnStartup: false,
);
Expand Down
12 changes: 6 additions & 6 deletions apps/carp_mobile_sensing_app/lib/src/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ class LoadingPage extends StatelessWidget {
await CarpBackend().initialize();
await CarpBackend().authenticate();

// Check if there is a local deployment id.
// If not, get a deployment id based on an invitation.
if (bloc.studyDeploymentId == null) {
// Check if there is a local study.
// If not, get a study deployment based on an invitation.
if (bloc.study == null) {
await CarpBackend().getStudyInvitation(context);
}

// Make sure that CarpService knows the study and deployment ids
CarpService().app.studyId = bloc.studyId;
CarpService().app.studyDeploymentId = bloc.studyDeploymentId;
// Make sure that CarpService knows the study deployment.
// This is useful when an app (like this one only handles one study at a time
CarpService().study = bloc.study;
}

await bloc.sensing.initialize();
Expand Down
22 changes: 9 additions & 13 deletions apps/carp_mobile_sensing_app/lib/src/blocs/carp_backend.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,7 @@ class CarpBackend {
);

/// The CAWS app configuration.
late final CarpApp _app = CarpApp(
name: "CAWS @ DTU",
uri: uri,
studyId: bloc.studyId,
studyDeploymentId: bloc.studyDeploymentId,
);
late final CarpApp _app = CarpApp(name: "CAWS @ DTU", uri: uri);

CarpApp get app => _app;

Expand Down Expand Up @@ -92,12 +87,13 @@ class CarpBackend {
await CarpParticipationService().getStudyInvitation(context);
debug('CAWS Study Invitation: $invitation');

bloc.studyId = invitation?.studyId;
bloc.studyDeploymentId = invitation?.studyDeploymentId;
bloc.deviceRoleName = invitation?.assignedDevices?.first.device.roleName;
info('Invitation received - '
'study id: ${bloc.studyId}, '
'deployment id: ${bloc.studyDeploymentId}, '
'role name: ${bloc.deviceRoleName}');
if (invitation != null) {
bloc.study = SmartphoneStudy.fromInvitation(invitation);

info('Invitation received - '
'study id: ${invitation.studyId}, '
'deployment id: ${invitation.studyDeploymentId}, '
'role name: ${invitation.deviceRoleName}');
}
}
}
Loading

0 comments on commit 5d086fa

Please sign in to comment.