diff --git a/src/screens/testScreen/TestScreen.tsx b/src/screens/testScreen/TestScreen.tsx index c6317be94..114c1fc35 100644 --- a/src/screens/testScreen/TestScreen.tsx +++ b/src/screens/testScreen/TestScreen.tsx @@ -115,7 +115,7 @@ const Content = () => { }, [setOutbreakStatus]); const onCheckForOutbreak = useCallback(async () => { - checkForOutbreaks(); + checkForOutbreaks(true); }, [checkForOutbreaks]); const goToCheckInHistory = useCallback(() => navigation.navigate('CheckInHistoryScreen'), [navigation]); diff --git a/src/services/ExposureNotificationService/ExposureNotificationService.ts b/src/services/ExposureNotificationService/ExposureNotificationService.ts index 6508678e8..61b58ff24 100644 --- a/src/services/ExposureNotificationService/ExposureNotificationService.ts +++ b/src/services/ExposureNotificationService/ExposureNotificationService.ts @@ -23,11 +23,12 @@ import {captureException, captureMessage} from 'shared/log'; import {log} from 'shared/logging/config'; import {DeviceEventEmitter, Platform} from 'react-native'; import {ContagiousDateInfo, ContagiousDateType} from 'shared/DataSharing'; -import {EN_API_VERSION} from 'env'; +import {EN_API_VERSION, QR_ENABLED} from 'env'; import {EventTypeMetric, FilteredMetricsService} from 'services/MetricsService/FilteredMetricsService'; import {checkNotifications} from 'react-native-permissions'; import {Status} from 'screens/home/components/NotificationPermissionStatus'; import {PollNotifications} from 'services/PollNotificationService'; +import {OutbreakService} from 'shared/OutbreakProvider'; import {BackendInterface, SubmissionKeySet} from '../BackendService'; import {PERIODIC_TASK_INTERVAL_IN_MINUTES} from '../BackgroundSchedulerService'; @@ -263,6 +264,10 @@ export class ExposureNotificationService { await this.updateExposureStatus(); await this.processNotification(); + if (QR_ENABLED) { + OutbreakService.sharedInstance(this.i18n).checkForOutbreaks(); + } + const filteredMetricsService = FilteredMetricsService.sharedInstance(); await filteredMetricsService.addEvent({type: EventTypeMetric.BackgroundCheck}); diff --git a/src/shared/OutbreakProvider.tsx b/src/shared/OutbreakProvider.tsx index 750cd3ae0..19589b1ac 100644 --- a/src/shared/OutbreakProvider.tsx +++ b/src/shared/OutbreakProvider.tsx @@ -1,22 +1,46 @@ +import {TEST_MODE} from 'env'; import AsyncStorage from '@react-native-community/async-storage'; import React, {useContext, useEffect, useMemo, useState} from 'react'; import {Key} from 'services/StorageService'; import PushNotification from 'bridge/PushNotification'; import {useI18nRef, I18n} from 'locale'; +import PQueue from 'p-queue'; + +// eslint-disable-next-line @shopify/strict-component-boundaries +import {DefaultSecureKeyValueStore, SecureKeyValueStore} from '../services/MetricsService/SecureKeyValueStorage'; import {Observable} from './Observable'; import {CheckInData, getNewOutbreakStatus, getOutbreakEvents, initialOutbreakStatus, OutbreakStatus} from './qr'; import {createCancellableCallbackPromise} from './cancellablePromise'; +import {getCurrentDate, minutesBetween} from './date-fns'; + +const OutbreaksLastCheckedStorageKey = 'A436ED42-707E-11EB-9439-0242AC130002'; + +const MIN_OUTBREAKS_CHECK_MINUTES = TEST_MODE ? 15 : 240; + +export class OutbreakService implements OutbreakService { + private static instance: OutbreakService; + + static sharedInstance(i18n: I18n): OutbreakService { + if (!this.instance) { + this.instance = new this(i18n); + } + return this.instance; + } -class OutbreakService implements OutbreakService { outbreakStatus: Observable; checkInHistory: Observable; i18n: I18n; + secureKeyValueStore: SecureKeyValueStore; + + private serialPromiseQueue: PQueue; constructor(i18n: I18n) { this.outbreakStatus = new Observable(initialOutbreakStatus); this.checkInHistory = new Observable([]); this.i18n = i18n; + this.secureKeyValueStore = new DefaultSecureKeyValueStore(); + this.serialPromiseQueue = new PQueue({concurrency: 1}); } setOutbreakStatus = async (value: OutbreakStatus) => { @@ -49,11 +73,28 @@ class OutbreakService implements OutbreakService { this.checkInHistory.set(JSON.parse(checkInHistory)); }; - checkForOutbreaks = async () => { + checkForOutbreaks = async (forceCheck?: boolean) => { + return this.serialPromiseQueue.add(() => { + return this.getOutbreaksLastCheckedDateTime().then(async outbreaksLastCheckedDateTime => { + if (forceCheck === false && outbreaksLastCheckedDateTime) { + const today = getCurrentDate(); + const minutesSinceLastOutbreaksCheck = minutesBetween(outbreaksLastCheckedDateTime, today); + if (minutesSinceLastOutbreaksCheck > MIN_OUTBREAKS_CHECK_MINUTES) { + await this.getOutbreaksFromServer(); + } + } else { + await this.getOutbreaksFromServer(); + } + }); + }); + }; + + getOutbreaksFromServer = async () => { const outbreakEvents = await getOutbreakEvents(); const newOutbreakStatusType = getNewOutbreakStatus(this.checkInHistory.get(), outbreakEvents); this.setOutbreakStatus(newOutbreakStatusType); this.processOutbreakNotification(newOutbreakStatusType); + this.markOutbreaksLastCheckedDateTime(getCurrentDate()); }; processOutbreakNotification = (status: OutbreakStatus) => { @@ -65,6 +106,16 @@ class OutbreakService implements OutbreakService { }); } }; + + private getOutbreaksLastCheckedDateTime(): Promise { + return this.secureKeyValueStore + .retrieve(OutbreaksLastCheckedStorageKey) + .then(value => (value ? new Date(Number(value)) : null)); + } + + private markOutbreaksLastCheckedDateTime(date: Date): Promise { + return this.secureKeyValueStore.save(OutbreaksLastCheckedStorageKey, `${date.getTime()}`); + } } export const createOutbreakService = async (i18n: I18n) => {