diff --git a/__tests__/helpers/daily-data-providers/combined-mindful-minutes.test.ts b/__tests__/helpers/daily-data-providers/combined-mindful-minutes.test.ts index 9677aa220..59682a169 100644 --- a/__tests__/helpers/daily-data-providers/combined-mindful-minutes.test.ts +++ b/__tests__/helpers/daily-data-providers/combined-mindful-minutes.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from '@jest/globals'; import { createEmptyCombinedDataCollectionSettings, createMockResult, sampleEndDate, sampleResult, sampleStartDate, setupCombinedDataCollectionSettings, setupCombinedFirstValueResult, setupDailyDataProvider } from '../../fixtures/daily-data-providers'; import combinedMindfulMinutes from '../../../src/helpers/daily-data-providers/combined-mindful-minutes'; import * as dailyDataResultFunctions from '../../../src/helpers/daily-data-providers/daily-data/daily-data-result'; -import { appleHealthMindfulMinutesDataProvider, googleFitMindfulMinutesDataProvider } from '../../../src/helpers/daily-data-providers'; +import { appleHealthMindfulMinutesDataProvider, googleFitMindfulMinutesDataProvider, healthConnectMindfulMinutesDataProvider } from '../../../src/helpers/daily-data-providers'; jest.mock('../../../src/helpers/daily-data-providers/apple-health-mindful-minutes', () => ({ __esModule: true, @@ -14,10 +14,16 @@ jest.mock('../../../src/helpers/daily-data-providers/google-fit-mindful-minutes' default: jest.fn() })); +jest.mock('../../../src/helpers/daily-data-providers/health-connect-mindful-minutes', () => ({ + __esModule: true, + default: jest.fn() +})); + describe('Daily Data Provider - Combined Mindful Minutes', () => { const appleHealthMindfulMinutesDataProviderMock = appleHealthMindfulMinutesDataProvider as jest.Mock; const googleFitMindfulMinutesDataProviderMock = googleFitMindfulMinutesDataProvider as jest.Mock; + const healthConnectMindfulMinutesDataProviderMock = healthConnectMindfulMinutesDataProvider as jest.Mock; const combinedFirstValueResultMock = jest.spyOn(dailyDataResultFunctions, 'combineResultsUsingFirstValue'); beforeEach(() => { @@ -27,13 +33,14 @@ describe('Daily Data Provider - Combined Mindful Minutes', () => { it('Should return an empty result when no providers are enabled.', async () => { const combinedSettings = createEmptyCombinedDataCollectionSettings(); - setupCombinedDataCollectionSettings(false, combinedSettings); + setupCombinedDataCollectionSettings(true, combinedSettings); const result = await combinedMindfulMinutes(sampleStartDate, sampleEndDate); expect(result).toEqual({}); expect(appleHealthMindfulMinutesDataProviderMock).not.toHaveBeenCalled(); expect(googleFitMindfulMinutesDataProviderMock).not.toHaveBeenCalled(); + expect(healthConnectMindfulMinutesDataProviderMock).not.toHaveBeenCalled(); expect(combinedFirstValueResultMock).not.toHaveBeenCalled(); }); @@ -41,14 +48,16 @@ describe('Daily Data Provider - Combined Mindful Minutes', () => { const combinedSettings = createEmptyCombinedDataCollectionSettings(); combinedSettings.settings.appleHealthEnabled = true; combinedSettings.settings.googleFitEnabled = true; + combinedSettings.settings.healthConnectEnabled = true; - setupCombinedDataCollectionSettings(false, combinedSettings); + setupCombinedDataCollectionSettings(true, combinedSettings); const result = await combinedMindfulMinutes(sampleStartDate, sampleEndDate); expect(result).toEqual({}); expect(appleHealthMindfulMinutesDataProviderMock).not.toHaveBeenCalled(); expect(googleFitMindfulMinutesDataProviderMock).not.toHaveBeenCalled(); + expect(healthConnectMindfulMinutesDataProviderMock).not.toHaveBeenCalled(); expect(combinedFirstValueResultMock).not.toHaveBeenCalled(); }); @@ -61,13 +70,14 @@ describe('Daily Data Provider - Combined Mindful Minutes', () => { const appleHealthResult = createMockResult(); - setupCombinedDataCollectionSettings(false, combinedSettings); + setupCombinedDataCollectionSettings(true, combinedSettings); setupDailyDataProvider(appleHealthMindfulMinutesDataProviderMock, sampleStartDate, sampleEndDate, appleHealthResult); const result = await combinedMindfulMinutes(sampleStartDate, sampleEndDate); expect(result).toBe(appleHealthResult); expect(googleFitMindfulMinutesDataProviderMock).not.toHaveBeenCalled(); + expect(healthConnectMindfulMinutesDataProviderMock).not.toHaveBeenCalled(); expect(combinedFirstValueResultMock).not.toHaveBeenCalled(); }); @@ -80,13 +90,34 @@ describe('Daily Data Provider - Combined Mindful Minutes', () => { const googleFitResult = createMockResult(); - setupCombinedDataCollectionSettings(false, combinedSettings); + setupCombinedDataCollectionSettings(true, combinedSettings); setupDailyDataProvider(googleFitMindfulMinutesDataProviderMock, sampleStartDate, sampleEndDate, googleFitResult); const result = await combinedMindfulMinutes(sampleStartDate, sampleEndDate); expect(result).toBe(googleFitResult); expect(appleHealthMindfulMinutesDataProviderMock).not.toHaveBeenCalled(); + expect(healthConnectMindfulMinutesDataProviderMock).not.toHaveBeenCalled(); + expect(combinedFirstValueResultMock).not.toHaveBeenCalled(); + }); + + it('Should return the Health Connect result when fully enabled.', async () => { + const combinedSettings = createEmptyCombinedDataCollectionSettings(); + combinedSettings.settings.healthConnectEnabled = true; + combinedSettings.deviceDataV2Types.push( + { namespace: 'HealthConnect', type: 'mindfulness-sessions', enabled: true } + ); + + const healthConnectResult = createMockResult(); + + setupCombinedDataCollectionSettings(true, combinedSettings); + setupDailyDataProvider(healthConnectMindfulMinutesDataProviderMock, sampleStartDate, sampleEndDate, healthConnectResult); + + const result = await combinedMindfulMinutes(sampleStartDate, sampleEndDate); + + expect(result).toBe(healthConnectResult); + expect(appleHealthMindfulMinutesDataProviderMock).not.toHaveBeenCalled(); + expect(googleFitMindfulMinutesDataProviderMock).not.toHaveBeenCalled(); expect(combinedFirstValueResultMock).not.toHaveBeenCalled(); }); @@ -98,14 +129,20 @@ describe('Daily Data Provider - Combined Mindful Minutes', () => { { namespace: 'AppleHealth', type: 'MindfulSession' }, { namespace: 'GoogleFit', type: 'ActivitySegment' } ); + combinedSettings.settings.healthConnectEnabled = true; + combinedSettings.deviceDataV2Types.push( + { namespace: 'HealthConnect', type: 'mindfulness-sessions', enabled: true } + ); const appleHealthResult = createMockResult(); const googleFitResult = createMockResult(); + const healthConnectResult = createMockResult(); - setupCombinedDataCollectionSettings(false, combinedSettings); + setupCombinedDataCollectionSettings(true, combinedSettings); setupDailyDataProvider(appleHealthMindfulMinutesDataProviderMock, sampleStartDate, sampleEndDate, appleHealthResult); setupDailyDataProvider(googleFitMindfulMinutesDataProviderMock, sampleStartDate, sampleEndDate, googleFitResult); - setupCombinedFirstValueResult(sampleStartDate, sampleEndDate, [appleHealthResult, googleFitResult], sampleResult); + setupDailyDataProvider(healthConnectMindfulMinutesDataProviderMock, sampleStartDate, sampleEndDate, healthConnectResult); + setupCombinedFirstValueResult(sampleStartDate, sampleEndDate, [appleHealthResult, googleFitResult, healthConnectResult], sampleResult); const result = await combinedMindfulMinutes(sampleStartDate, sampleEndDate); diff --git a/__tests__/helpers/daily-data-providers/health-connect-mindful-minutes.test.ts b/__tests__/helpers/daily-data-providers/health-connect-mindful-minutes.test.ts new file mode 100644 index 000000000..b181681f6 --- /dev/null +++ b/__tests__/helpers/daily-data-providers/health-connect-mindful-minutes.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from '@jest/globals'; +import { sampleDataPointsV2, sampleEndDate, sampleResult, sampleStartDate, sampleTimeRanges, setupDailyDataPointsV2, setupDailyTimeRanges, setupMinutesResult } from '../../fixtures/daily-data-providers'; +import healthConnectMindfulMinutes from '../../../src/helpers/daily-data-providers/health-connect-mindful-minutes'; + +describe('Daily Data Provider - Health Connect Mindful Minutes', () => { + it('Should query for daily data points and build a minutes result.', async () => { + setupDailyDataPointsV2('HealthConnect', 'mindfulness-sessions', sampleStartDate, sampleEndDate, undefined, undefined, sampleDataPointsV2); + setupDailyTimeRanges(sampleDataPointsV2, sampleTimeRanges); + setupMinutesResult(sampleStartDate, sampleEndDate, sampleTimeRanges, sampleResult); + + expect(await healthConnectMindfulMinutes(sampleStartDate, sampleEndDate)).toBe(sampleResult); + }); +}); diff --git a/rollup.config.mjs b/rollup.config.mjs index e93f37d91..bddcf9ebf 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -9,7 +9,7 @@ import terser from "@rollup/plugin-terser"; import peerDepsExternal from 'rollup-plugin-peer-deps-external'; import analyze from 'rollup-plugin-analyzer'; -const limitBytes = 8.5e6; +const limitBytes = 9.0e6; const onAnalysis = ({ bundleSize }) => { if (bundleSize < limitBytes) return; diff --git a/src/helpers/daily-data-providers/combined-mindful-minutes.ts b/src/helpers/daily-data-providers/combined-mindful-minutes.ts index 104c57a87..e9f43f33f 100644 --- a/src/helpers/daily-data-providers/combined-mindful-minutes.ts +++ b/src/helpers/daily-data-providers/combined-mindful-minutes.ts @@ -1,4 +1,4 @@ -import { appleHealthMindfulMinutesDataProvider, googleFitMindfulMinutesDataProvider } from '.'; +import { appleHealthMindfulMinutesDataProvider, googleFitMindfulMinutesDataProvider, healthConnectMindfulMinutesDataProvider } from '.'; import { DailyDataQueryResult } from '../query-daily-data'; import { getCombinedDataCollectionSettings } from './combined-data-collection-settings'; import { combineResultsUsingFirstValue } from './daily-data'; @@ -6,7 +6,7 @@ import { combineResultsUsingFirstValue } from './daily-data'; export default async function (startDate: Date, endDate: Date): Promise { const providers: Promise[] = []; - const { settings } = await getCombinedDataCollectionSettings(false); + const { settings, deviceDataV2Types } = await getCombinedDataCollectionSettings(true); if (settings.appleHealthEnabled && settings.queryableDeviceDataTypes.some(type => type.namespace === 'AppleHealth' && type.type === 'MindfulSession')) { providers.push(appleHealthMindfulMinutesDataProvider(startDate, endDate)); @@ -14,6 +14,9 @@ export default async function (startDate: Date, endDate: Date): Promise type.namespace === 'GoogleFit' && type.type === 'ActivitySegment')) { providers.push(googleFitMindfulMinutesDataProvider(startDate, endDate)); } + if (settings.healthConnectEnabled && deviceDataV2Types.some(type => type.namespace === 'HealthConnect' && type.type === 'mindfulness-sessions')) { + providers.push(healthConnectMindfulMinutesDataProvider(startDate, endDate)); + } if (providers.length === 0) return {}; if (providers.length === 1) return providers[0]; diff --git a/src/helpers/daily-data-providers/health-connect-mindful-minutes.ts b/src/helpers/daily-data-providers/health-connect-mindful-minutes.ts new file mode 100644 index 000000000..399ae08b1 --- /dev/null +++ b/src/helpers/daily-data-providers/health-connect-mindful-minutes.ts @@ -0,0 +1,9 @@ +import { DailyDataQueryResult } from '../query-daily-data'; +import { buildMinutesResultFromDailyTimeRanges, computeDailyTimeRanges } from '../time-range'; +import { queryForDailyDataPointsV2 } from './daily-data'; + +export default async function(startDate: Date, endDate: Date): Promise { + const dataPoints = await queryForDailyDataPointsV2('HealthConnect', 'mindfulness-sessions', startDate, endDate); + const dailyTimeRanges = computeDailyTimeRanges(dataPoints); + return buildMinutesResultFromDailyTimeRanges(startDate, endDate, dailyTimeRanges); +} \ No newline at end of file diff --git a/src/helpers/daily-data-providers/index.ts b/src/helpers/daily-data-providers/index.ts index e99324f3f..658ca9e42 100644 --- a/src/helpers/daily-data-providers/index.ts +++ b/src/helpers/daily-data-providers/index.ts @@ -88,6 +88,7 @@ export { default as healthConnectMinHeartRateDataProvider } from "./health-conne export { default as healthConnectActiveCaloriesBurnedDataProvider } from "./health-connect-active-calories-burned"; export { default as healthConnectTotalCaloriesBurnedDataProvider } from "./health-connect-total-calories-burned"; export { default as healthConnectTherapyMinutesDataProvider } from "./health-connect-therapy-minutes"; +export { default as healthConnectMindfulMinutesDataProvider } from "./health-connect-mindful-minutes"; export { default as ouraStepsDataProvider } from "./oura-daily-steps" export { default as ouraSleepMinutesDataProvider } from "./oura-total-sleep" export { default as ouraRestingHeartRateDataProvider } from "./oura-resting-heart-rate" diff --git a/src/helpers/daily-data-types.tsx b/src/helpers/daily-data-types.tsx index faa09336b..952264f23 100644 --- a/src/helpers/daily-data-types.tsx +++ b/src/helpers/daily-data-types.tsx @@ -93,6 +93,7 @@ export enum DailyDataType { HealthConnectActiveCaloriesBurned = "HealthConnectActiveCaloriesBurned", HealthConnectTotalCaloriesBurned = "HealthConnectTotalCaloriesBurned", HealthConnectTherapyMinutes = "HealthConnectTherapyMinutes", + HealthConnectMindfulMinutes = "HealthConnectMindfulMinutes", OuraSteps = "OuraSteps", OuraRestingHeartRate = "OuraRestingHeartRate", OuraSleepMinutes = "OuraSleepMinutes", diff --git a/src/helpers/daily-data-types/combined.tsx b/src/helpers/daily-data-types/combined.tsx index f5cb48f00..61f7aea32 100644 --- a/src/helpers/daily-data-types/combined.tsx +++ b/src/helpers/daily-data-types/combined.tsx @@ -43,7 +43,8 @@ const SLEEP_MINUTES_SOURCES = sources( const MINDFUL_MINUTES_SOURCES = sources( ["AppleHealth", "MindfulSession"], - ["GoogleFit", "ActivitySegment"] + ["GoogleFit", "ActivitySegment"], + ["HealthConnect", "mindfulness-sessions"] ); const THERAPY_MINUTES_SOURCES = sources( diff --git a/src/helpers/daily-data-types/health-connect.tsx b/src/helpers/daily-data-types/health-connect.tsx index b1bd3146d..87ece8805 100644 --- a/src/helpers/daily-data-types/health-connect.tsx +++ b/src/helpers/daily-data-types/health-connect.tsx @@ -5,6 +5,7 @@ import { healthConnectDistanceDataProvider, healthConnectLightSleepMinutesDataProvider, healthConnectMaxHeartRateDataProvider, + healthConnectMindfulMinutesDataProvider, healthConnectMinHeartRateDataProvider, healthConnectRemSleepMinutesDataProvider, healthConnectRestingHeartRateDataProvider, @@ -135,6 +136,15 @@ const healthConnectTypeDefinitions: DailyDataTypeDefinition[] = [ icon: , formatter: value => formatNumberForLocale(value), previewDataRange: [0, 120] + }, + { + type: DailyDataType.HealthConnectMindfulMinutes, + dataProvider: healthConnectMindfulMinutesDataProvider, + availabilityCheck: simpleAvailabilityCheck('HealthConnect', 'mindfulness-sessions'), + labelKey: 'mindful-minutes', + icon: , + formatter: value => formatNumberForLocale(value), + previewDataRange: [0, 120] } ]; healthConnectTypeDefinitions.forEach((def) => {