diff --git a/config.xml b/config.xml index eb723a609..22d52d7ed 100755 --- a/config.xml +++ b/config.xml @@ -1,5 +1,5 @@ - + RADAR Questionnaire An application that collects active data for research. RADAR-Base diff --git a/src/app/core/services/app-server/app-server.service.ts b/src/app/core/services/app-server/app-server.service.ts index 83ea1beeb..f242db564 100644 --- a/src/app/core/services/app-server/app-server.service.ts +++ b/src/app/core/services/app-server/app-server.service.ts @@ -1,4 +1,9 @@ -import { HttpClient, HttpHeaders } from '@angular/common/http' +import { + HttpClient, + HttpErrorResponse, + HttpHeaders, + HttpResponse +} from '@angular/common/http' import { Injectable } from '@angular/core' import * as moment from 'moment-timezone' import * as urljoin from 'url-join' @@ -9,12 +14,20 @@ import { } from '../../../../assets/data/defaultConfig' import { ConfigKeys } from '../../../shared/enums/config' import { StorageKeys } from '../../../shared/enums/storage' +import { + FcmNotificationDto, + FcmNotificationError +} from '../../../shared/models/app-server' +import { SingleNotification } from '../../../shared/models/notification-handler' +import { Task } from '../../../shared/models/task' import { RemoteConfigService } from '../config/remote-config.service' import { SubjectConfigService } from '../config/subject-config.service' import { LocalizationService } from '../misc/localization.service' import { LogService } from '../misc/log.service' import { StorageService } from '../storage/storage.service' import { TokenService } from '../token/token.service' +import { filter } from "rxjs/operators"; +import { Subscription } from "rxjs"; @Injectable() export class AppServerService { @@ -22,6 +35,12 @@ export class AppServerService { SUBJECT_PATH = 'users' PROJECT_PATH = 'projects' GITHUB_CONTENT_PATH = 'github/content' + QUESTIONNAIRE_SCHEDULE_PATH = 'questionnaire/schedule' + QUESTIONNAIRE_TASK = 'questionnaire/task' + QUESTIONNAIRE_STATE_EVENTS_PATH = 'state_events' + NOTIFICATIONS_PATH = 'messaging/notifications' + STATE_EVENTS_PATH = 'state_events' + private tokenSubscription: Subscription = null constructor( public storage: StorageService, @@ -35,7 +54,7 @@ export class AppServerService { init() { // NOTE: Initialising ensures project and subject exists in the app server - return Promise.all([this.updateAppServerURL()]) + return this.updateAppServerURL() .then(() => Promise.all([ this.subjectConfig.getParticipantLogin(), @@ -46,16 +65,25 @@ export class AppServerService { ]) ) .then(([subjectId, projectId, enrolmentDate, attributes, fcmToken]) => - this.addProjectIfMissing(projectId).then(() => - this.addSubjectIfMissing( - subjectId, - projectId, - enrolmentDate, - attributes, - fcmToken - ) - ) - ) + this.addProjectIfMissing(projectId) + .then(() => this.addSubjectIfMissing( + subjectId, + projectId, + enrolmentDate, + attributes, + fcmToken + ) + ).then(httpRes => { + if (this.tokenSubscription !== null) { + this.tokenSubscription.unsubscribe(); + } + this.tokenSubscription = this.storage.observe(StorageKeys.FCM_TOKEN) + .pipe(filter(t => t && t !== fcmToken)) + .subscribe(newFcmToken => + this.addSubjectIfMissing(subjectId, projectId, enrolmentDate, attributes, newFcmToken)) + return httpRes; + }) + ); } getHeaders() { @@ -210,6 +238,205 @@ export class AppServerService { }) } + getSchedule(): Promise { + return Promise.all([ + this.subjectConfig.getParticipantLogin(), + this.subjectConfig.getProjectName() + ]).then(([subjectId, projectId]) => { + return this.getHeaders().then(headers => + this.http + .get( + urljoin( + this.APP_SERVER_URL, + this.PROJECT_PATH, + projectId, + this.SUBJECT_PATH, + subjectId, + this.QUESTIONNAIRE_SCHEDULE_PATH + ), + { headers } + ) + .toPromise() + ) + }) + } + + getScheduleForDates(startTime: Date, endTime: Date): Promise { + return Promise.all([ + this.subjectConfig.getParticipantLogin(), + this.subjectConfig.getProjectName() + ]).then(([subjectId, projectId]) => { + return this.getHeaders().then(headers => + this.http + .get( + urljoin( + this.APP_SERVER_URL, + this.PROJECT_PATH, + projectId, + this.SUBJECT_PATH, + subjectId, + this.QUESTIONNAIRE_SCHEDULE_PATH + ), + { + headers, + params: { + startTime: startTime.toISOString(), + endTime: endTime.toISOString() + } + } + ) + .toPromise() + .catch(e => []) + ) + }) + } + + generateSchedule(): Promise { + return Promise.all([ + this.subjectConfig.getParticipantLogin(), + this.subjectConfig.getProjectName() + ]).then(([subjectId, projectId]) => { + return this.getHeaders().then(headers => + this.http + .post( + urljoin( + this.APP_SERVER_URL, + this.PROJECT_PATH, + projectId, + this.SUBJECT_PATH, + subjectId, + this.QUESTIONNAIRE_SCHEDULE_PATH + ), + { headers } + ) + .toPromise() + ) + }) + } + + pullAllPublishedNotifications(subject) { + return this.getHeaders().then(headers => + this.http + .get( + urljoin( + this.getAppServerURL(), + this.PROJECT_PATH, + subject.projectId, + this.SUBJECT_PATH, + subject.subjectId, + this.NOTIFICATIONS_PATH + ), + { headers } + ) + .toPromise() + ) + } + + deleteNotification(subject, notification: SingleNotification) { + return this.getHeaders().then(headers => + this.http + .delete( + urljoin( + this.getAppServerURL(), + this.PROJECT_PATH, + subject.projectId, + this.SUBJECT_PATH, + subject.subjectId, + this.NOTIFICATIONS_PATH, + notification.id.toString() + ), + { headers } + ) + .toPromise() + ) + } + + updateTaskState(taskId, state) { + return Promise.all([ + this.subjectConfig.getParticipantLogin(), + this.subjectConfig.getProjectName() + ]).then(([subjectId, projectId]) => { + return this.getHeaders().then(headers => + this.http + .post( + urljoin( + this.getAppServerURL(), + this.PROJECT_PATH, + projectId, + this.SUBJECT_PATH, + subjectId, + this.QUESTIONNAIRE_SCHEDULE_PATH, + taskId.toString(), + this.QUESTIONNAIRE_STATE_EVENTS_PATH + ), + { + taskId: taskId, + state: state, + time: new Date(), + associatedInfo: '' + }, + { headers } + ) + .toPromise() + ) + }) + } + + updateNotificationState(subject, notificationId, state) { + return this.getHeaders().then(headers => + this.http + .post( + urljoin( + this.getAppServerURL(), + this.PROJECT_PATH, + subject.projectId, + this.SUBJECT_PATH, + subject.subjectId, + this.NOTIFICATIONS_PATH, + notificationId.toString(), + this.STATE_EVENTS_PATH + ), + { notificationId: notificationId, state: state, time: new Date() }, + { headers } + ) + .toPromise() + ) + } + + public addNotification(notification, subjectId, projectId): Promise { + return this.getHeaders().then(headers => + this.http + .post( + urljoin( + this.getAppServerURL(), + this.PROJECT_PATH, + projectId, + this.SUBJECT_PATH, + subjectId, + this.NOTIFICATIONS_PATH + ), + notification.notificationDto, + { headers, observe: 'response' } + ) + .toPromise() + .then((res: HttpResponse) => { + this.logger.log('Successfully sent! Updating notification Id') + return res.body + }) + .catch((err: HttpErrorResponse) => { + this.logger.log('Http request returned an error: ' + err.message) + const data: FcmNotificationError = err.error + if (err.status == 409) { + this.logger.log( + 'Notification already exists, storing notification data..' + ) + return data.dto ? data.dto : notification.notification + } + return this.logger.error('Failed to send notification', err) + }) + ) + } + getFCMToken() { return this.storage.get(StorageKeys.FCM_TOKEN) } diff --git a/src/app/core/services/kafka/kafka.service.spec.ts b/src/app/core/services/kafka/kafka.service.spec.ts index ada9cf541..92a5eb80d 100644 --- a/src/app/core/services/kafka/kafka.service.spec.ts +++ b/src/app/core/services/kafka/kafka.service.spec.ts @@ -3,7 +3,7 @@ import { TestBed } from '@angular/core/testing' import { FirebaseAnalyticsServiceMock, - LogServiceMock, + LogServiceMock, RemoteConfigServiceMock, SchemaServiceMock, StorageServiceMock, TokenServiceMock @@ -14,6 +14,7 @@ import { TokenService } from '../token/token.service' import { AnalyticsService } from '../usage/analytics.service' import { KafkaService } from './kafka.service' import { SchemaService } from './schema.service' +import { RemoteConfigService } from "../config/remote-config.service"; describe('KafkaService', () => { let service @@ -31,7 +32,8 @@ describe('KafkaService', () => { { provide: AnalyticsService, useClass: FirebaseAnalyticsServiceMock - } + }, + { provide: RemoteConfigService, useClass: RemoteConfigServiceMock }, ] }) ) diff --git a/src/app/core/services/kafka/kafka.service.ts b/src/app/core/services/kafka/kafka.service.ts index a541a67cb..0f93c3bca 100755 --- a/src/app/core/services/kafka/kafka.service.ts +++ b/src/app/core/services/kafka/kafka.service.ts @@ -15,9 +15,13 @@ import { StorageService } from '../storage/storage.service' import { TokenService } from '../token/token.service' import { AnalyticsService } from '../usage/analytics.service' import { SchemaService } from './schema.service' +import { RemoteConfigService } from "../config/remote-config.service"; +import { ConfigKeys } from "../../../shared/enums/config"; @Injectable() export class KafkaService { + private static DEFAULT_TOPIC_CACHE_VALIDITY = 600_000 // 10 minutes + URI_topics: string = '/topics/' private readonly KAFKA_STORE = { @@ -27,6 +31,9 @@ export class KafkaService { private KAFKA_CLIENT_URL: string private BASE_URI: string private isCacheSending: boolean + private topics: string[] = null + private lastTopicFetch: number = 0 + private TOPIC_CACHE_VALIDITY = KafkaService.DEFAULT_TOPIC_CACHE_VALIDITY constructor( private storage: StorageService, @@ -34,22 +41,71 @@ export class KafkaService { private schema: SchemaService, private analytics: AnalyticsService, private logger: LogService, - private http: HttpClient + private http: HttpClient, + private remoteConfig: RemoteConfigService, ) { this.updateURI() + this.readTopicCacheValidity(); } init() { - return this.setCache({}) + return Promise.all([ + this.setCache({}), + this.updateTopicCacheValidity(), + this.fetchTopics(), + ]); } updateURI() { - this.token.getURI().then(uri => { + return this.token.getURI().then(uri => { this.BASE_URI = uri this.KAFKA_CLIENT_URL = uri + DefaultKafkaURI }) } + readTopicCacheValidity() { + return this.storage.get(StorageKeys.TOPIC_CACHE_TIMEOUT) + .then(timeout => { + if (typeof timeout === 'number') { + this.TOPIC_CACHE_VALIDITY = timeout + } + }); + } + + updateTopicCacheValidity() { + return this.remoteConfig.read() + .then(config => config.getOrDefault(ConfigKeys.TOPIC_CACHE_TIMEOUT, this.TOPIC_CACHE_VALIDITY.toString())) + .then(timeoutString => { + const timeout = parseInt(timeoutString) + if (!isNaN(timeout)) { + this.TOPIC_CACHE_VALIDITY = Math.max(0, timeout) + return this.storage.set(StorageKeys.TOPIC_CACHE_TIMEOUT, this.TOPIC_CACHE_VALIDITY) + } + }) + } + + private fetchTopics() { + return this.http.get(this.KAFKA_CLIENT_URL + this.URI_topics, {observe: 'body'}) + .toPromise() + .then((topics: string[]) => { + this.topics = topics + this.lastTopicFetch = Date.now() + return topics + }) + .catch(e => { + this.logger.error("Failed to fetch Kafka topics", e) + return this.topics + }); + } + + getTopics() { + if (this.topics !== null || this.lastTopicFetch + this.TOPIC_CACHE_VALIDITY >= Date.now()) { + return Promise.resolve(this.topics) + } else { + return this.fetchTopics(); + } + } + prepareKafkaObjectAndSend(type, payload, keepInCache?) { const value = this.schema.getKafkaObjectValue(type, payload) const keyPromise = this.schema.getKafkaObjectKey() @@ -83,14 +139,15 @@ export class KafkaService { return Promise.all([ this.getCache(), this.getKafkaHeaders(), - this.schema.getRadarSpecifications() + this.schema.getRadarSpecifications(), + this.getTopics(), ]) - .then(([cache, headers, specifications]) => { + .then(([cache, headers, specifications, topics]) => { const sendPromises = Object.entries(cache) .filter(([k]) => k) .map(([k, v]: any) => { return this.schema - .getKafkaTopic(specifications, v.name, v.avsc) + .getKafkaTopic(specifications, v.name, v.avsc, topics) .then(topic => this.sendToKafka(topic, k, v, headers)) .catch(e => this.logger.error('Failed to send data from cache to kafka', e) diff --git a/src/app/core/services/kafka/schema.service.ts b/src/app/core/services/kafka/schema.service.ts index b1529ffb8..1edc492bc 100644 --- a/src/app/core/services/kafka/schema.service.ts +++ b/src/app/core/services/kafka/schema.service.ts @@ -27,6 +27,7 @@ import { LogService } from '../misc/log.service' export class SchemaService { URI_schema: string = '/schema/subjects/' URI_version: string = '/versions/' + GENERAL_TOPIC: string = 'questionnaire_response' private schemas: { [key: string]: [Promise, Promise] } = {} @@ -153,17 +154,34 @@ export class SchemaService { }) } - getKafkaTopic(specifications: any[] | null, name, avsc): Promise { - try { - const type = name.toLowerCase() - const defaultTopic = `${avsc}_${name}` - if (specifications) { - const spec = specifications.find(t => t.type.toLowerCase() == type) - return Promise.resolve(spec && spec.topic ? spec.topic : defaultTopic) + getKafkaTopic(specifications: any[] | null, name, avsc, topics: string[] | null): Promise { + const type = name.toLowerCase() + + if (specifications) { + const spec = specifications.find(t => t.type.toLowerCase() == type) + if (spec && spec.topic && this.topicExists(spec.topic, topics)) { + return Promise.resolve(spec.topic) } + } + const questionnaireTopic = `${avsc}_${name}` + if (this.topicExists(questionnaireTopic, topics)) { + return Promise.resolve(questionnaireTopic) + } + const defaultTopic = this.GENERAL_TOPIC; + if (this.topicExists(defaultTopic, topics)) { return Promise.resolve(defaultTopic) - } catch (e) { - return Promise.reject('Failed to get kafka topic') + } + + return Promise.reject(`No suitable topic found on server for questionnaire ${name}`) + } + + private topicExists(topic: string, topics: string[] | null) { + if (!topics || topics.includes(topic)) { + return true + } else { + this.logger.error( + `Cannot send data to specification topic ${topic} because target server does not have it`, null) + return false } } diff --git a/src/app/core/services/notifications/fcm-notification.service.ts b/src/app/core/services/notifications/fcm-notification.service.ts index 9370a7772..c24efe5aa 100644 --- a/src/app/core/services/notifications/fcm-notification.service.ts +++ b/src/app/core/services/notifications/fcm-notification.service.ts @@ -16,6 +16,7 @@ import { SubjectConfigService } from '../config/subject-config.service' import { LogService } from '../misc/log.service' import { StorageService } from '../storage/storage.service' import { NotificationService } from './notification.service' +import { Subscription } from "rxjs"; declare var FirebasePlugin @@ -24,6 +25,7 @@ export abstract class FcmNotificationService extends NotificationService { FCM_TOKEN: string upstreamResends: number ttlMinutes = 10 + private tokenSubscription: Subscription constructor( public store: StorageService, @@ -34,6 +36,7 @@ export abstract class FcmNotificationService extends NotificationService { public remoteConfig: RemoteConfigService ) { super(store) + this.tokenSubscription = null this.platform.ready().then(() => { this.remoteConfig.subject().subscribe(cfg => { cfg @@ -50,21 +53,34 @@ export abstract class FcmNotificationService extends NotificationService { } init() { - this.firebase.setAutoInitEnabled(true) if (!this.platform.is('ios')) FirebasePlugin.setDeliveryMetricsExportToBigQuery(true) - FirebasePlugin.setSenderId( - FCMPluginProjectSenderId, - () => this.logger.log('[NOTIFICATION SERVICE] Set sender id success'), - error => { + return Promise.all([ + this.firebase.setAutoInitEnabled(true), + this.setSenderIdPromise(), + ]) + .then(() => this.firebase.getToken()) + .then(token => { + if (this.tokenSubscription === null) { + this.tokenSubscription = this.firebase + .onTokenRefresh() + .subscribe(t => this.onTokenRefresh(t)) + } + if (token) { + return this.onTokenRefresh(token); + } + }) + } + + setSenderIdPromise(): Promise { + return new Promise((resolve, reject) => + FirebasePlugin.setSenderId(FCMPluginProjectSenderId, resolve, reject)) + .then(() => this.logger.log('[NOTIFICATION SERVICE] Set sender id success')) + .catch(error => { this.logger.error('Failed to set sender ID', error) alert(error) - } - ) - this.firebase - .onTokenRefresh() - .subscribe(token => this.onTokenRefresh(token)) - this.firebase.getToken().then(token => this.onTokenRefresh(token)) + throw error + }) } publish( @@ -113,6 +129,10 @@ export abstract class FcmNotificationService extends NotificationService { } unregisterFromNotifications(): Promise { + if (this.tokenSubscription) { + this.tokenSubscription.unsubscribe(); + this.tokenSubscription = null; + } // NOTE: This will delete the current device token and stop receiving notifications return this.firebase .setAutoInitEnabled(false) @@ -122,8 +142,10 @@ export abstract class FcmNotificationService extends NotificationService { onTokenRefresh(token) { if (token) { this.FCM_TOKEN = token - this.setFCMToken(token) - this.logger.log('[NOTIFICATION SERVICE] Refresh token success') + return this.setFCMToken(token) + .then(() => this.logger.log('[NOTIFICATION SERVICE] Refresh token success')) + } else { + return Promise.resolve() } } diff --git a/src/app/core/services/notifications/fcm-rest-notification.service.ts b/src/app/core/services/notifications/fcm-rest-notification.service.ts index 55f471178..d64428cb0 100644 --- a/src/app/core/services/notifications/fcm-rest-notification.service.ts +++ b/src/app/core/services/notifications/fcm-rest-notification.service.ts @@ -40,7 +40,6 @@ export class FcmRestNotificationService extends FcmNotificationService { NOTIFICATIONS_PATH = 'messaging/notifications' SUBJECT_PATH = 'users' PROJECT_PATH = 'projects' - STATE_EVENTS_PATH = 'state_events' resumeListener: Subscription = new Subscription() @@ -55,7 +54,6 @@ export class FcmRestNotificationService extends FcmNotificationService { public remoteConfig: RemoteConfigService, public localization: LocalizationService, private appServerService: AppServerService, - private http: HttpClient, private webIntent: WebIntent ) { super(storage, config, firebase, platform, logger, remoteConfig) @@ -68,8 +66,8 @@ export class FcmRestNotificationService extends FcmNotificationService { } init() { - super.init() - return this.appServerService.init() + return super.init() + .then(() => this.appServerService.init()) } onAppOpen() { @@ -85,17 +83,19 @@ export class FcmRestNotificationService extends FcmNotificationService { tasks, messageId ) - return this.updateNotificationState( - subject, - notification.id, - NotificationMessagingState.DELIVERED - ).then(() => - this.updateNotificationState( + return this.appServerService + .updateNotificationState( subject, notification.id, - NotificationMessagingState.OPENED + NotificationMessagingState.DELIVERED + ) + .then(() => + this.appServerService.updateNotificationState( + subject, + notification.id, + NotificationMessagingState.OPENED + ) ) - ) }) }) } @@ -123,16 +123,14 @@ export class FcmRestNotificationService extends FcmNotificationService { this.logger.log('NOTIFICATIONS Scheduling FCM notifications') this.logger.log(fcmNotifications) return Promise.all( - fcmNotifications - .map(n => - this.sendNotification(n, subject.subjectId, subject.projectId) - ) - .concat([this.setLastNotificationUpdate(Date.now())]) + fcmNotifications.map(n => + this.sendNotification(n, subject.subjectId, subject.projectId) + ) ) }) } - publishTestNotification(subject): Promise { + publishTestNotification(subject): Promise { return this.sendNotification( this.format(this.notifications.createTestNotification(), subject), subject.subjectId, @@ -140,22 +138,21 @@ export class FcmRestNotificationService extends FcmNotificationService { ) } - pullAllPublishedNotifications(subject) { + sendNotification(notification, subjectId, projectId) { return this.appServerService - .getHeaders() - .then(headers => - this.http - .get( - this.getNotificationEndpoint(subject.projectId, subject.subjectId), - { headers } - ) - .toPromise() - ) + .addNotification(notification, subjectId, projectId) + .then((resultNotification: FcmNotificationDto) => { + this.setLastNotificationUpdate(Date.now()) + notification.notification.id = resultNotification.id + return (notification.notification.messageId = + resultNotification.fcmMessageId) + }) } cancelAllNotifications(subject): Promise { - return this.pullAllPublishedNotifications(subject).then( - (res: FcmNotifications) => { + return this.appServerService + .pullAllPublishedNotifications(subject) + .then((res: FcmNotifications) => { const now = Date.now() const notifications = res.notifications .map(n => ({ @@ -164,26 +161,13 @@ export class FcmRestNotificationService extends FcmNotificationService { })) .filter(n => n.timestamp > now) notifications.map(o => this.cancelSingleNotification(subject, o)) - } - ) + }) } cancelSingleNotification(subject, notification: SingleNotification) { if (notification.id) { return this.appServerService - .getHeaders() - .then(headers => - this.http - .delete( - this.getNotificationEndpoint( - subject.projectId, - subject.subjectId, - notification.id - ), - { headers } - ) - .toPromise() - ) + .deleteNotification(subject, notification) .then(() => { this.logger.log('Success cancelling notification ' + notification.id) return (notification.id = undefined) @@ -194,59 +178,6 @@ export class FcmRestNotificationService extends FcmNotificationService { } } - updateNotificationState(subject, notificationId, state) { - return this.appServerService - .getHeaders() - .then(headers => - this.http - .post( - urljoin( - this.getNotificationEndpoint( - subject.projectId, - subject.subjectId, - notificationId - ), - this.STATE_EVENTS_PATH - ), - { notificationId: notificationId, state: state, time: new Date() }, - { headers } - ) - .toPromise() - ) - } - - private sendNotification(notification, subjectId, projectId): Promise { - return this.appServerService.getHeaders().then(headers => - this.http - .post( - this.getNotificationEndpoint(projectId, subjectId), - notification.notificationDto, - { headers, observe: 'response' } - ) - .toPromise() - .then((res: HttpResponse) => { - this.logger.log('Successfully sent! Updating notification Id') - return res.body - }) - .catch((err: HttpErrorResponse) => { - this.logger.log('Http request returned an error: ' + err.message) - const data: FcmNotificationError = err.error - if (err.status == 409) { - this.logger.log( - 'Notification already exists, storing notification data..' - ) - return data.dto ? data.dto : notification.notification - } - return this.logger.error('Failed to send notification', err) - }) - .then((resultNotification: FcmNotificationDto) => { - notification.notification.id = resultNotification.id - return (notification.notification.messageId = - resultNotification.fcmMessageId) - }) - ) - } - private format(notification: SingleNotification, subject) { const taskInfo = notification.task return { @@ -267,16 +198,4 @@ export class FcmRestNotificationService extends FcmNotificationService { } } } - - getNotificationEndpoint(projectId, subjectId, notificationId?) { - return urljoin( - this.appServerService.getAppServerURL(), - this.PROJECT_PATH, - projectId, - this.SUBJECT_PATH, - subjectId, - this.NOTIFICATIONS_PATH, - notificationId ? notificationId.toString() : '' - ) - } } diff --git a/src/app/core/services/schedule/appserver-schedule.service.ts b/src/app/core/services/schedule/appserver-schedule.service.ts new file mode 100644 index 000000000..968bfcb7c --- /dev/null +++ b/src/app/core/services/schedule/appserver-schedule.service.ts @@ -0,0 +1,94 @@ +import {} from './notification.service' + +import { Injectable } from '@angular/core' +import * as moment from 'moment' + +import { Assessment, AssessmentType } from '../../../shared/models/assessment' +import { TaskState } from '../../../shared/models/protocol' +import { Task } from '../../../shared/models/task' +import { + advanceRepeat, + getMilliseconds, + setDateTimeToMidnight, + setDateTimeToMidnightEpoch +} from '../../../shared/utilities/time' +import { AppServerService } from '../app-server/app-server.service' +import { QuestionnaireService } from '../config/questionnaire.service' +import { LocalizationService } from '../misc/localization.service' +import { LogService } from '../misc/log.service' +import { StorageService } from '../storage/storage.service' +import { ScheduleGeneratorService } from './schedule-generator.service' +import { ScheduleService } from './schedule.service' + +@Injectable() +export class AppserverScheduleService extends ScheduleService { + constructor( + private store: StorageService, + logger: LogService, + private appServer: AppServerService, + private localization: LocalizationService, + private questionnaire: QuestionnaireService + ) { + super(store, logger) + } + + init() {} + + getTasksForDate(date: Date, type: AssessmentType) { + const startTime = setDateTimeToMidnight(date) + const endTime = moment(startTime).add(1, 'days').toDate() + return this.appServer + .getScheduleForDates(startTime, endTime) + .then(tasks => + Promise.all( + tasks.map(t => this.mapTaskDTO(t, AssessmentType.SCHEDULED)) + ).then(res => this.setTasks(AssessmentType.SCHEDULED, res)) + ) + } + + generateSchedule(referenceTimestamp, utcOffsetPrev) { + this.logger.log('Updating schedule..', referenceTimestamp) + return Promise.all([this.appServer.init(), this.getCompletedTasks()]).then( + ([, completedTasks]) => { + return this.appServer + .getSchedule() + .then(tasks => + Promise.all( + tasks.map(t => this.mapTaskDTO(t, AssessmentType.SCHEDULED)) + ) + ) + .then(res => this.setTasks(AssessmentType.SCHEDULED, res)) + } + ) + } + + updateTaskToComplete(updatedTask): Promise { + return this.appServer + .updateTaskState(updatedTask.id, TaskState.COMPLETED) + .then(() => super.updateTaskToReportedCompletion(updatedTask)) + .catch(() => super.updateTaskToComplete(updatedTask)) + } + + generateSingleAssessmentTask( + assessment: Assessment, + assessmentType, + referenceDate: number + ) { + return + } + + mapTaskDTO(task: Task, assesmentType: AssessmentType): Promise { + return this.questionnaire + .getAssessmentForTask(assesmentType, task) + .then(assessment => { + const newTask = Object.assign(task, { + reportedCompletion: !!task.completed, + nQuestions: assessment.questions.length, + warning: this.localization.chooseText(assessment.warn), + requiresInClinicCompletion: assessment.requiresInClinicCompletion, + notifications: [] + }) + return newTask + }) + } +} diff --git a/src/app/core/services/schedule/local-schedule.service.ts b/src/app/core/services/schedule/local-schedule.service.ts new file mode 100644 index 000000000..72285e1ce --- /dev/null +++ b/src/app/core/services/schedule/local-schedule.service.ts @@ -0,0 +1,79 @@ +import { Injectable } from '@angular/core' + +import { Assessment, AssessmentType } from '../../../shared/models/assessment' +import { Task } from '../../../shared/models/task' +import { compareTasks } from '../../../shared/utilities/compare-tasks' +import { + getMilliseconds, + setDateTimeToMidnightEpoch +} from '../../../shared/utilities/time' +import { LogService } from '../misc/log.service' +import { StorageService } from '../storage/storage.service' +import { ScheduleGeneratorService } from './schedule-generator.service' +import { ScheduleService } from './schedule.service' + +@Injectable() +export class LocalScheduleService extends ScheduleService { + constructor( + private store: StorageService, + logger: LogService, + private scheduleGenerator: ScheduleGeneratorService + ) { + super(store, logger) + } + + init() {} + + getTasksForDate(date: Date, type: AssessmentType) { + return this.getTasks(type).then(schedule => { + const startTime = setDateTimeToMidnightEpoch(date) + const endTime = startTime + getMilliseconds({ days: 1 }) + return schedule + ? schedule.filter(d => { + return ( + d.timestamp + d.completionWindow > startTime && + d.timestamp < endTime + ) + }) + : [] + }) + } + + generateSchedule(referenceTimestamp, utcOffsetPrev) { + this.logger.log('Updating schedule..', referenceTimestamp) + return this.getCompletedTasks() + .then(completedTasks => { + return this.scheduleGenerator.runScheduler( + referenceTimestamp, + completedTasks, + utcOffsetPrev + ) + }) + .then(res => + Promise.all([ + this.setTasks(AssessmentType.SCHEDULED, res.schedule), + this.setCompletedTasks(res.completed ? res.completed : []) + ]) + ) + } + + generateSingleAssessmentTask( + assessment: Assessment, + assessmentType, + referenceDate: number + ) { + return this.getTasks(assessmentType).then((tasks: Task[]) => { + const schedule = this.scheduleGenerator.buildTasksForSingleAssessment( + assessment, + tasks ? tasks.length : 0, + referenceDate, + assessmentType + ) + const newTasks = (tasks ? tasks.concat(schedule) : schedule).sort( + compareTasks + ) + this.changeDetectionEmitter.emit() + return this.setTasks(assessmentType, newTasks) + }) + } +} diff --git a/src/app/core/services/schedule/schedule-factory.service.ts b/src/app/core/services/schedule/schedule-factory.service.ts new file mode 100644 index 000000000..14f17ead5 --- /dev/null +++ b/src/app/core/services/schedule/schedule-factory.service.ts @@ -0,0 +1,76 @@ +import { Injectable } from '@angular/core' + +import { DefaultScheduleServiceType } from '../../../../assets/data/defaultConfig' +import { ConfigKeys } from '../../../shared/enums/config' +import { Assessment, AssessmentType } from '../../../shared/models/assessment' +import { SchedulerType } from '../../../shared/models/notification-handler' +import { RemoteConfigService } from '../config/remote-config.service' +import { LogService } from '../misc/log.service' +import { StorageService } from '../storage/storage.service' +import { AppserverScheduleService } from './appserver-schedule.service' +import { LocalScheduleService } from './local-schedule.service' +import { ScheduleService } from './schedule.service' + +@Injectable() +export class ScheduleFactoryService extends ScheduleService { + scheduleService: ScheduleService + + constructor( + public localScheduleService: LocalScheduleService, + public appServerScheduleSerice: AppserverScheduleService, + private remoteConfig: RemoteConfigService, + private store: StorageService, + logger: LogService + ) { + super(store, logger) + this.init() + } + + init() { + return this.remoteConfig + .forceFetch() + .then(config => + config.getOrDefault( + ConfigKeys.SCHEDULE_SERVICE_TYPE, + DefaultScheduleServiceType + ) + ) + .then(type => { + switch (type) { + case SchedulerType.LOCAL: + return (this.scheduleService = this.localScheduleService) + case SchedulerType.APPSERVER: + return (this.scheduleService = this.appServerScheduleSerice) + default: + throw new Error('No such scheduling service available') + } + }) + } + + generateSchedule(referenceTimestamp, utcOffsetPrev) { + return this.scheduleService.generateSchedule( + referenceTimestamp, + utcOffsetPrev + ) + } + + generateSingleAssessmentTask( + assessment: Assessment, + assessmentType, + referenceDate: number + ) { + return this.scheduleService.generateSingleAssessmentTask( + assessment, + assessmentType, + referenceDate + ) + } + + getTasksForDate(date: Date, type: AssessmentType) { + return this.scheduleService.getTasksForDate(date, type) + } + + updateTaskToComplete(updatedTask): Promise { + return this.scheduleService.updateTaskToComplete(updatedTask) + } +} diff --git a/src/app/core/services/schedule/schedule-generator.service.ts b/src/app/core/services/schedule/schedule-generator.service.ts index d68647b64..d23f3a254 100644 --- a/src/app/core/services/schedule/schedule-generator.service.ts +++ b/src/app/core/services/schedule/schedule-generator.service.ts @@ -17,6 +17,7 @@ import { Task } from '../../../shared/models/task' import { compareTasks } from '../../../shared/utilities/compare-tasks' import { advanceRepeat, + getMilliseconds, setDateTimeToMidnight, setDateTimeToMidnightEpoch, timeIntervalToMillis @@ -152,7 +153,7 @@ export class ScheduleGeneratorService { completionWindow ): Task { const task: Task = this.util.deepCopy(DefaultTask) - task.index = index + task.id = index task.timestamp = timestamp task.name = assessment.name task.type = assessment.type diff --git a/src/app/core/services/schedule/schedule.service.ts b/src/app/core/services/schedule/schedule.service.ts old mode 100755 new mode 100644 index efb18e83e..d96188a3a --- a/src/app/core/services/schedule/schedule.service.ts +++ b/src/app/core/services/schedule/schedule.service.ts @@ -10,12 +10,13 @@ import { getMilliseconds, setDateTimeToMidnightEpoch } from '../../../shared/utilities/time' +import { AppServerService } from '../app-server/app-server.service' import { LogService } from '../misc/log.service' import { StorageService } from '../storage/storage.service' import { ScheduleGeneratorService } from './schedule-generator.service' @Injectable() -export class ScheduleService { +export abstract class ScheduleService { private readonly SCHEDULE_STORE = { SCHEDULE_TASKS: StorageKeys.SCHEDULE_TASKS, SCHEDULE_TASKS_ON_DEMAND: StorageKeys.SCHEDULE_TASKS_ON_DEMAND, @@ -25,11 +26,22 @@ export class ScheduleService { changeDetectionEmitter: EventEmitter = new EventEmitter() constructor( - private storage: StorageService, - private schedule: ScheduleGeneratorService, - private logger: LogService + protected storage: StorageService, + protected logger: LogService ) {} + abstract init() + + abstract generateSchedule(referenceTimestamp, utcOffsetPrev) + + abstract generateSingleAssessmentTask( + assessment: Assessment, + assessmentType, + referenceDate: number + ) + + abstract getTasksForDate(date: Date, type: AssessmentType) + getTasks(type: AssessmentType): Promise { switch (type) { case AssessmentType.SCHEDULED: @@ -57,21 +69,6 @@ export class ScheduleService { } } - getTasksForDate(date: Date, type: AssessmentType) { - return this.getTasks(type).then(schedule => { - const startTime = setDateTimeToMidnightEpoch(date) - const endTime = startTime + getMilliseconds({ days: 1 }) - return schedule - ? schedule.filter(d => { - return ( - d.timestamp + d.completionWindow > startTime && - d.timestamp < endTime - ) - }) - : [] - }) - } - getScheduledTasks(): Promise { return this.storage.get(this.SCHEDULE_STORE.SCHEDULE_TASKS) } @@ -100,6 +97,16 @@ export class ScheduleService { }) } + getReportedIncompleteTasks(): Promise { + // These tasks have been completed but have not yet been reported as complete to the app server + return this.getTasks(AssessmentType.ALL).then(tasks => { + const now = new Date().getTime() + return tasks + .filter(d => d.completed && !d.reportedCompletion) + .slice(0, 100) + }) + } + setTasks(type: AssessmentType, tasks: Task[]): Promise { const uniqueTasks = [ ...new Map( @@ -139,49 +146,11 @@ export class ScheduleService { return this.storage.push(this.SCHEDULE_STORE.SCHEDULE_TASKS_COMPLETED, task) } - generateSchedule(referenceTimestamp, utcOffsetPrev) { - this.logger.log('Updating schedule..', referenceTimestamp) - return this.getCompletedTasks() - .then(completedTasks => { - return this.schedule.runScheduler( - referenceTimestamp, - completedTasks, - utcOffsetPrev - ) - }) - .then(res => - Promise.all([ - this.setTasks(AssessmentType.SCHEDULED, res.schedule), - this.setCompletedTasks(res.completed ? res.completed : []) - ]) - ) - } - - generateSingleAssessmentTask( - assessment: Assessment, - assessmentType, - referenceDate: number - ) { - return this.getTasks(assessmentType).then((tasks: Task[]) => { - const schedule = this.schedule.buildTasksForSingleAssessment( - assessment, - tasks ? tasks.length : 0, - referenceDate, - assessmentType - ) - const newTasks = (tasks ? tasks.concat(schedule) : schedule).sort( - compareTasks - ) - this.changeDetectionEmitter.emit() - return this.setTasks(assessmentType, newTasks) - }) - } - insertTask(task): Promise { const type = task.type return this.getTasks(type).then(tasks => { if (!tasks) return - const updatedTasks = tasks.map(d => (d.index === task.index ? task : d)) + const updatedTasks = tasks.map(d => (d.id === task.id ? task : d)) return this.setTasks(type, updatedTasks) }) } diff --git a/src/app/core/services/storage/storage.service.ts b/src/app/core/services/storage/storage.service.ts index 4fb37454e..33f6656a3 100755 --- a/src/app/core/services/storage/storage.service.ts +++ b/src/app/core/services/storage/storage.service.ts @@ -1,18 +1,21 @@ import { Injectable } from '@angular/core' import { Storage } from '@ionic/storage' -import { throwError as observableThrowError } from 'rxjs' +import { Observable, Subject, throwError as observableThrowError } from 'rxjs' import { StorageKeys } from '../../../shared/enums/storage' import { LogService } from '../misc/log.service' +import { filter, startWith, switchMap } from "rxjs/operators"; @Injectable() export class StorageService { global: { [key: string]: any } = {} + private readonly keyUpdates: Subject constructor(private storage: Storage, private logger: LogService) { this.prepare().then(() => this.logger.log('Global configuration', this.global) ) + this.keyUpdates = new Subject(); } getStorageState() { @@ -23,15 +26,23 @@ export class StorageService { const k = key.toString() this.global[k] = value return this.storage.set(k, value) + .then(res => { + this.keyUpdates.next(key); + return res; + }); } push(key: StorageKeys, value: any): Promise { if (this.global[key.toString()]) this.global[key.toString()].push(value) else this.global[key.toString()] = [value] return this.storage.set(key.toString(), this.global[key.toString()]) + .then(res => { + this.keyUpdates.next(key); + return res; + }); } - get(key: StorageKeys) { + get(key: StorageKeys): Promise { const k = key.toString() const local = this.global[k] if (local !== undefined) { @@ -44,12 +55,21 @@ export class StorageService { } } + observe(key: StorageKeys): Observable { + return this.keyUpdates.pipe( + startWith(key), + filter(k => k === key || k === null), + switchMap(k => this.get(k)), + ); + } + remove(key: StorageKeys) { const k = key.toString() return this.storage .remove(k) .then(res => { this.global[k] = null + this.keyUpdates.next(key); return res }) .catch(error => this.handleError(error)) @@ -72,6 +92,7 @@ export class StorageService { clear() { this.global = {} return this.storage.clear() + .then(() => this.keyUpdates.next(null)); } private handleError(error: any) { diff --git a/src/app/pages/home/containers/home-page.component.ts b/src/app/pages/home/containers/home-page.component.ts index 0445e943e..2fbe6c5f0 100755 --- a/src/app/pages/home/containers/home-page.component.ts +++ b/src/app/pages/home/containers/home-page.component.ts @@ -94,7 +94,7 @@ export class HomePageComponent implements OnDestroy { } init() { - this.sortedTasks = this.tasksService.getSortedTasksOfToday() + this.sortedTasks = this.tasksService.getValidTasksMap() this.tasks = this.tasksService.getTasksOfToday() this.currentDate = this.tasksService.getCurrentDateMidnight() this.tasksProgress = this.tasksService.getTaskProgress() diff --git a/src/app/pages/home/services/tasks.service.ts b/src/app/pages/home/services/tasks.service.ts index 5e99d6b24..c44535e0a 100644 --- a/src/app/pages/home/services/tasks.service.ts +++ b/src/app/pages/home/services/tasks.service.ts @@ -61,16 +61,19 @@ export class TasksService { ) } - getSortedTasksOfToday(): Promise> { - return this.getTasksOfToday().then(tasks => { - const sortedTasks = new Map() - tasks.forEach(t => { - const midnight = setDateTimeToMidnightEpoch(new Date(t.timestamp)) - if (sortedTasks.has(midnight)) sortedTasks.get(midnight).push(t) - else sortedTasks.set(midnight, [t]) + getValidTasksMap(): Promise> { + // This groups the tasks valid for today into a Map, where the key is midnight epoch and the value is an array of tasks + return this.getTasksOfToday() + .then(t => t.sort((a, b) => a.timestamp - b.timestamp)) + .then(tasks => { + const sortedTasks = new Map() + tasks.forEach(t => { + const midnight = setDateTimeToMidnightEpoch(new Date(t.timestamp)) + if (sortedTasks.has(midnight)) sortedTasks.get(midnight).push(t) + else sortedTasks.set(midnight, [t]) + }) + return sortedTasks }) - return sortedTasks - }) } getTaskProgress(): Promise { diff --git a/src/app/pages/pages.module.ts b/src/app/pages/pages.module.ts index d1a840f7d..8bb9ecf92 100644 --- a/src/app/pages/pages.module.ts +++ b/src/app/pages/pages.module.ts @@ -19,6 +19,9 @@ import { MessageHandlerService } from '../core/services/notifications/message-ha import { NotificationFactoryService } from '../core/services/notifications/notification-factory.service' import { NotificationGeneratorService } from '../core/services/notifications/notification-generator.service' import { NotificationService } from '../core/services/notifications/notification.service' +import { AppserverScheduleService } from '../core/services/schedule/appserver-schedule.service' +import { LocalScheduleService } from '../core/services/schedule/local-schedule.service' +import { ScheduleFactoryService } from '../core/services/schedule/schedule-factory.service' import { ScheduleGeneratorService } from '../core/services/schedule/schedule-generator.service' import { ScheduleService } from '../core/services/schedule/schedule.service' import { StorageService } from '../core/services/storage/storage.service' @@ -61,7 +64,9 @@ import { SplashModule } from './splash/splash.module' TokenService, KafkaService, LocalizationService, - ScheduleService, + { provide: ScheduleService, useClass: ScheduleFactoryService }, + LocalScheduleService, + AppserverScheduleService, ScheduleGeneratorService, StorageService, TranslatePipe, diff --git a/src/app/pages/questions/services/finish-task.service.ts b/src/app/pages/questions/services/finish-task.service.ts index 70fc00c96..a80cf5be5 100644 --- a/src/app/pages/questions/services/finish-task.service.ts +++ b/src/app/pages/questions/services/finish-task.service.ts @@ -21,8 +21,9 @@ export class FinishTaskService { updateTaskToComplete(task): Promise { return Promise.all([ - this.schedule.updateTaskToComplete(task), - this.schedule.updateTaskToReportedCompletion(task), + this.schedule + .updateTaskToComplete(task) + .then(res => this.schedule.updateTaskToReportedCompletion(task)), task.type == AssessmentType.SCHEDULED ? this.schedule.addToCompletedTasks(task) : Promise.resolve() diff --git a/src/app/pages/questions/services/questions.service.ts b/src/app/pages/questions/services/questions.service.ts index f67c6e256..fbd1e1e84 100644 --- a/src/app/pages/questions/services/questions.service.ts +++ b/src/app/pages/questions/services/questions.service.ts @@ -258,16 +258,6 @@ export class QuestionsService { return this.finish.createClinicalFollowUpTask(assessment) } - getHiddenQuestions(): Promise { - return this.remoteConfig - .read() - .then(config => - config.getOrDefault(ConfigKeys.QUESTIONS_HIDDEN, DefaultQuestionsHidden) - ) - .then(res => JSON.parse(res)) - .catch(e => DefaultQuestionsHidden) - } - stringToArray(array, delimiter) { return array.split(delimiter).map(s => s.trim()) } diff --git a/src/app/pages/splash/containers/splash-page.component.ts b/src/app/pages/splash/containers/splash-page.component.ts index 8389cf562..0163a3440 100644 --- a/src/app/pages/splash/containers/splash-page.component.ts +++ b/src/app/pages/splash/containers/splash-page.component.ts @@ -53,7 +53,9 @@ export class SplashPageComponent { this.status = this.localization.translateKey( LocKeys.SPLASH_STATUS_SENDING_LOGS ) - return this.splashService.sendMissedQuestionnaireLogs() + return this.splashService + .sendMissedQuestionnaireLogs() + .then(() => this.splashService.sendReportedIncompleteTasks()) }) .catch(e => this.showFetchConfigFail(e)) .then(() => this.navCtrl.setRoot(HomePageComponent)) diff --git a/src/app/pages/splash/services/splash.service.ts b/src/app/pages/splash/services/splash.service.ts index a0e43bf77..266d7c34c 100644 --- a/src/app/pages/splash/services/splash.service.ts +++ b/src/app/pages/splash/services/splash.service.ts @@ -67,4 +67,12 @@ export class SplashService { ) ) } + + sendReportedIncompleteTasks() { + return this.schedule + .getReportedIncompleteTasks() + .then(tasks => + Promise.all(tasks.map(task => this.schedule.updateTaskToComplete(task))) + ) + } } diff --git a/src/app/shared/enums/config.ts b/src/app/shared/enums/config.ts index f56e30af2..b0001b23a 100644 --- a/src/app/shared/enums/config.ts +++ b/src/app/shared/enums/config.ts @@ -13,6 +13,7 @@ export class ConfigKeys { static NOTIFICATION_MESSAGING_TYPE = new ConfigKeys( 'notification_messaging_type' ) + static SCHEDULE_SERVICE_TYPE = new ConfigKeys('schedule_service_type') static APP_SERVER_URL = new ConfigKeys('app_server_url') static ON_DEMAND_ASSESSMENT_LABEL = new ConfigKeys( 'on_demand_assessment_label' @@ -35,6 +36,10 @@ export class ConfigKeys { static GITHUB_FETCH_STRATEGY = new ConfigKeys('github_fetch_strategy') + static TOPIC_CACHE_TIMEOUT = new ConfigKeys( + 'topic_cache_timeout' + ) + constructor(public value: string) {} toString() { diff --git a/src/app/shared/enums/storage.ts b/src/app/shared/enums/storage.ts index 17d5cdf7c..347ac4c66 100755 --- a/src/app/shared/enums/storage.ts +++ b/src/app/shared/enums/storage.ts @@ -42,6 +42,10 @@ export class StorageKeys { 'REMOTE_CONFIG_CACHE_TIMEOUT' ) + static TOPIC_CACHE_TIMEOUT = new StorageKeys( + 'TOPIC_CACHE_TIMEOUT' + ) + static FCM_TOKEN = new StorageKeys('FCM_TOKEN') static NOTIFICATION_MESSAGING_TYPE = new StorageKeys( 'NOTIFICATION_MESSAGING_TYPE' diff --git a/src/app/shared/models/notification-handler.ts b/src/app/shared/models/notification-handler.ts index ad901bfe2..ec1f22f25 100644 --- a/src/app/shared/models/notification-handler.ts +++ b/src/app/shared/models/notification-handler.ts @@ -39,6 +39,11 @@ export enum NotificationMessagingType { FCM_REST = 'FCM_REST' } +export enum SchedulerType { + LOCAL = 'LOCAL', + APPSERVER = 'APPSERVER' +} + export enum NotificationMessagingState { DELIVERED = 'DELIVERED', OPENED = 'OPENED' diff --git a/src/app/shared/models/protocol.ts b/src/app/shared/models/protocol.ts index 98479d7cd..0dc0d3ccb 100755 --- a/src/app/shared/models/protocol.ts +++ b/src/app/shared/models/protocol.ts @@ -29,6 +29,14 @@ export enum ReferenceTimestampFormat { NOW = 'now' } +export enum TaskState { + ADDED = 'ADDED', + UPDATED = 'UPDATED', + CANCELLED = 'CANCELLED', + SCHEDULED = 'SCHEDULED', + COMPLETED = 'COMPLETED' +} + export interface ProtocolMetaData { protocol: string url?: string diff --git a/src/app/shared/models/task.ts b/src/app/shared/models/task.ts index 561ae6c5f..beac7390e 100755 --- a/src/app/shared/models/task.ts +++ b/src/app/shared/models/task.ts @@ -2,7 +2,7 @@ import { AssessmentType } from './assessment' import { SingleNotification } from './notification-handler' export interface Task { - index: number + id: number completed: boolean reportedCompletion: boolean timestamp: number @@ -19,6 +19,7 @@ export interface Task { isDemo: boolean order: number isLastTask?: boolean + status?: string } export interface TasksProgress { diff --git a/src/assets/data/defaultConfig.ts b/src/assets/data/defaultConfig.ts index bcf67b33b..3d7118355 100755 --- a/src/assets/data/defaultConfig.ts +++ b/src/assets/data/defaultConfig.ts @@ -16,7 +16,7 @@ import { Localisations } from './localisations' export const DefaultPlatformInstance = 'RADAR-CNS' // *Default app version -export const DefaultAppVersion = '2.4.0-alpha' +export const DefaultAppVersion = '2.6.0-alpha' // *Default Android package name export const DefaultPackageName = 'org.phidatalab.radar_armt' @@ -89,7 +89,7 @@ export const DefaultESMCompletionWindow = 600000 // *Default sample task export const DefaultTask: Task = { - index: 0, + id: 0, type: AssessmentType.SCHEDULED, completed: false, reportedCompletion: false, @@ -118,6 +118,9 @@ export const DefaultScheduleVersion = '0.3.10' // *Default max number of completion logs to send on app start export const DefaultNumberOfCompletionLogsToSend = 10 +// *Default schedule service type (either 'LOCAL' or 'APPSERVER') +export const DefaultScheduleServiceType: string = 'LOCAL' + // DEFAULT NOTIFICATION SETUP // *Default notification type (either 'FCM_XMPP', 'FCM_REST' or 'LOCAL' notifications)