summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-12-01 04:31:36 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-12-01 04:31:36 +0000
commit1cefc66b91f828f77d70b6c29d8e5c5d577155f4 (patch)
treee304a2cca0dd19a5538a93d4223d6677633b8c20
parent54eb260ab5ea2153110efb94cf73ad8b59e18eb3 (diff)
parent884b6debe1c18825a015de95ed8769c7d6f6aa92 (diff)
downloadHealthFitness-android14-mainline-cellbroadcast-release.tar.gz
Snap for 11164065 from 884b6debe1c18825a015de95ed8769c7d6f6aa92 to mainline-cellbroadcast-releaseaml_cbr_341410010android14-mainline-cellbroadcast-release
Change-Id: I77033e3224c769738434502131fc1fcc72b6f6bc
-rw-r--r--TEST_MAPPING7
-rw-r--r--apk/res/layout/migration_in_progress_screen.xml7
-rw-r--r--apk/res/navigation/data_nav_graph.xml23
-rw-r--r--apk/res/values-af/strings.xml5
-rw-r--r--apk/res/values-am/strings.xml5
-rw-r--r--apk/res/values-ar/strings.xml5
-rw-r--r--apk/res/values-as/strings.xml5
-rw-r--r--apk/res/values-az/strings.xml5
-rw-r--r--apk/res/values-b+sr+Latn/strings.xml5
-rw-r--r--apk/res/values-be/strings.xml5
-rw-r--r--apk/res/values-bg/strings.xml5
-rw-r--r--apk/res/values-bn/strings.xml5
-rw-r--r--apk/res/values-bs/strings.xml5
-rw-r--r--apk/res/values-ca/strings.xml5
-rw-r--r--apk/res/values-cs/strings.xml5
-rw-r--r--apk/res/values-da/strings.xml5
-rw-r--r--apk/res/values-de/strings.xml5
-rw-r--r--apk/res/values-el/strings.xml5
-rw-r--r--apk/res/values-en-rAU/strings.xml5
-rw-r--r--apk/res/values-en-rCA/strings.xml2
-rw-r--r--apk/res/values-en-rGB/strings.xml5
-rw-r--r--apk/res/values-en-rIN/strings.xml5
-rw-r--r--apk/res/values-en-rXC/strings.xml2
-rw-r--r--apk/res/values-es-rUS/strings.xml5
-rw-r--r--apk/res/values-es/strings.xml5
-rw-r--r--apk/res/values-et/strings.xml5
-rw-r--r--apk/res/values-eu/strings.xml5
-rw-r--r--apk/res/values-fa/strings.xml9
-rw-r--r--apk/res/values-fi/strings.xml5
-rw-r--r--apk/res/values-fr-rCA/strings.xml5
-rw-r--r--apk/res/values-fr/strings.xml5
-rw-r--r--apk/res/values-gl/strings.xml5
-rw-r--r--apk/res/values-gu/strings.xml5
-rw-r--r--apk/res/values-hi/strings.xml5
-rw-r--r--apk/res/values-hr/strings.xml5
-rw-r--r--apk/res/values-hu/strings.xml5
-rw-r--r--apk/res/values-hy/strings.xml5
-rw-r--r--apk/res/values-in/strings.xml5
-rw-r--r--apk/res/values-is/strings.xml5
-rw-r--r--apk/res/values-it/strings.xml7
-rw-r--r--apk/res/values-iw/strings.xml5
-rw-r--r--apk/res/values-ja/strings.xml5
-rw-r--r--apk/res/values-ka/strings.xml5
-rw-r--r--apk/res/values-kk/strings.xml5
-rw-r--r--apk/res/values-km/strings.xml5
-rw-r--r--apk/res/values-kn/strings.xml7
-rw-r--r--apk/res/values-ko/strings.xml5
-rw-r--r--apk/res/values-ky/strings.xml5
-rw-r--r--apk/res/values-lo/strings.xml5
-rw-r--r--apk/res/values-lt/strings.xml5
-rw-r--r--apk/res/values-lv/strings.xml5
-rw-r--r--apk/res/values-mk/strings.xml5
-rw-r--r--apk/res/values-ml/strings.xml5
-rw-r--r--apk/res/values-mn/strings.xml5
-rw-r--r--apk/res/values-mr/strings.xml5
-rw-r--r--apk/res/values-ms/strings.xml5
-rw-r--r--apk/res/values-my/strings.xml5
-rw-r--r--apk/res/values-nb/strings.xml5
-rw-r--r--apk/res/values-ne/strings.xml5
-rw-r--r--apk/res/values-nl/strings.xml5
-rw-r--r--apk/res/values-or/strings.xml5
-rw-r--r--apk/res/values-pa/strings.xml5
-rw-r--r--apk/res/values-pl/strings.xml5
-rw-r--r--apk/res/values-pt-rPT/strings.xml5
-rw-r--r--apk/res/values-pt/strings.xml5
-rw-r--r--apk/res/values-ro/strings.xml5
-rw-r--r--apk/res/values-ru/strings.xml5
-rw-r--r--apk/res/values-si/strings.xml5
-rw-r--r--apk/res/values-sk/strings.xml5
-rw-r--r--apk/res/values-sl/strings.xml5
-rw-r--r--apk/res/values-sq/strings.xml5
-rw-r--r--apk/res/values-sr/strings.xml5
-rw-r--r--apk/res/values-sv/strings.xml5
-rw-r--r--apk/res/values-sw/strings.xml5
-rw-r--r--apk/res/values-ta/strings.xml5
-rw-r--r--apk/res/values-te/strings.xml5
-rw-r--r--apk/res/values-th/strings.xml5
-rw-r--r--apk/res/values-tl/strings.xml5
-rw-r--r--apk/res/values-tr/strings.xml5
-rw-r--r--apk/res/values-uk/strings.xml5
-rw-r--r--apk/res/values-ur/strings.xml5
-rw-r--r--apk/res/values-uz/strings.xml5
-rw-r--r--apk/res/values-vi/strings.xml5
-rw-r--r--apk/res/values-zh-rCN/strings.xml5
-rw-r--r--apk/res/values-zh-rHK/strings.xml5
-rw-r--r--apk/res/values-zh-rTW/strings.xml5
-rw-r--r--apk/res/values-zu/strings.xml5
-rw-r--r--apk/res/values/strings.xml2
-rw-r--r--apk/res/xml/data_sources_and_priority_screen.xml6
-rw-r--r--apk/res/xml/empty_preference_screen.xml4
-rw-r--r--apk/src/com/android/healthconnect/controller/MainActivity.kt29
-rw-r--r--apk/src/com/android/healthconnect/controller/data/DataManagementActivity.kt26
-rw-r--r--apk/src/com/android/healthconnect/controller/data/access/AccessViewModel.kt2
-rw-r--r--apk/src/com/android/healthconnect/controller/data/access/LoadAccessUseCase.kt16
-rw-r--r--apk/src/com/android/healthconnect/controller/data/access/LoadPermissionTypeContributorAppsUseCase.kt8
-rw-r--r--apk/src/com/android/healthconnect/controller/data/entries/api/LoadDataEntriesUseCase.kt13
-rw-r--r--apk/src/com/android/healthconnect/controller/data/entries/api/LoadEntriesHelper.kt17
-rw-r--r--apk/src/com/android/healthconnect/controller/data/entries/api/LoadSleepDataUseCase.kt42
-rw-r--r--apk/src/com/android/healthconnect/controller/datasources/AddAnAppFragment.kt29
-rw-r--r--apk/src/com/android/healthconnect/controller/datasources/DataSourcesFragment.kt45
-rw-r--r--apk/src/com/android/healthconnect/controller/datasources/DataSourcesViewModel.kt2
-rw-r--r--apk/src/com/android/healthconnect/controller/datasources/api/LoadLastDateWithPriorityDataUseCase.kt145
-rw-r--r--apk/src/com/android/healthconnect/controller/datasources/api/LoadMostRecentAggregationsUseCase.kt309
-rw-r--r--apk/src/com/android/healthconnect/controller/datasources/api/LoadPriorityEntriesUseCase.kt80
-rw-r--r--apk/src/com/android/healthconnect/controller/datasources/api/SleepSessionHelper.kt207
-rw-r--r--apk/src/com/android/healthconnect/controller/datasources/api/UpdatePriorityListUseCase.kt20
-rw-r--r--apk/src/com/android/healthconnect/controller/datasources/appsources/AppSourcesAdapter.kt22
-rw-r--r--apk/src/com/android/healthconnect/controller/datasources/appsources/AppSourcesPreference.kt13
-rw-r--r--apk/src/com/android/healthconnect/controller/managedata/ManageDataFragment.kt19
-rw-r--r--apk/src/com/android/healthconnect/controller/migration/AppUpdateRequiredFragment.kt13
-rw-r--r--apk/src/com/android/healthconnect/controller/migration/MigrationNavigationFragment.kt35
-rw-r--r--apk/src/com/android/healthconnect/controller/migration/MigrationPausedFragment.kt15
-rw-r--r--apk/src/com/android/healthconnect/controller/migration/ModuleUpdateRequiredFragment.kt11
-rw-r--r--apk/src/com/android/healthconnect/controller/onboarding/OnboardingActivity.kt8
-rw-r--r--apk/src/com/android/healthconnect/controller/onboarding/OnboardingActivityContract.kt25
-rw-r--r--apk/src/com/android/healthconnect/controller/permissions/api/GetGrantedHealthPermissionsUseCase.kt10
-rw-r--r--apk/src/com/android/healthconnect/controller/permissions/api/LoadAccessDateUseCase.kt2
-rw-r--r--apk/src/com/android/healthconnect/controller/permissions/request/PermissionsActivity.kt57
-rw-r--r--apk/src/com/android/healthconnect/controller/permissions/shared/SettingsActivity.kt36
-rw-r--r--apk/src/com/android/healthconnect/controller/permissiontypes/HealthPermissionTypesFragment.kt41
-rw-r--r--apk/src/com/android/healthconnect/controller/service/UseCaseModule.kt88
-rw-r--r--apk/src/com/android/healthconnect/controller/shared/preference/AggregationDataCard.kt15
-rw-r--r--apk/src/com/android/healthconnect/controller/shared/preference/CardContainerPreference.kt161
-rw-r--r--apk/src/com/android/healthconnect/controller/utils/AppStoreUtil.kt72
-rw-r--r--apk/src/com/android/healthconnect/controller/utils/AppStoreUtils.kt83
-rw-r--r--apk/src/com/android/healthconnect/controller/utils/NavigationUtils.kt17
-rw-r--r--apk/src/com/android/healthconnect/controller/utils/TimeExtensions.kt25
-rw-r--r--apk/src/com/android/healthconnect/controller/utils/logging/HealthConnectLogger.kt75
-rw-r--r--apk/tests/Android.bp2
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/MainActivityTest.kt4
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/autodelete/api/LoadAutoDeleteUseCaseTest.kt89
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/autodelete/api/UpdateAutoDeleteUseCaseTest.kt126
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/data/DataManagementActivityTest.kt145
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/data/access/AccessViewModelTest.kt74
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/data/access/LoadAccessUseCaseTest.kt90
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/data/access/LoadPermissionTypeContributorAppsUseCaseTest.kt113
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/data/entries/api/LoadDataAggregationsUseCaseTest.kt2
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/data/entries/api/LoadDataEntriesUseCaseTest.kt147
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/data/entries/api/LoadEntriesHelperUseCaseTest.kt (renamed from apk/tests/src/com/android/healthconnect/controller/tests/data/entries/api/LoadSleepDataUseCaseTest.kt)168
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/datasources/DataSourcesFragmentTest.kt36
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/datasources/api/LoadLastDateWithPriorityDataUseCaseTest.kt517
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/datasources/api/LoadMostRecentAggregationsUseCaseTest.kt1198
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/datasources/api/LoadPriorityEntriesUseCaseTest.kt375
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/datasources/api/SleepSessionHelperTest.kt676
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/deletion/DeletionFragmentTest.kt321
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/deletion/DeletionParametersTest.kt116
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/managedata/ManageDataFragmentTest.kt18
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/migration/AppUpdateRequiredFragmentTest.kt113
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/migration/MigrationInProgressFragmentTest.kt36
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/migration/MigrationNavigationFragmentTest.kt217
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/migration/MigrationPausedFragmentTest.kt96
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/migration/ModuleUpdateRequiredFragmentTest.kt117
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/onboarding/OnboardingScreenTest.kt4
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/permissions/api/GetGrantedHealthPermissionsUseCaseTest.kt24
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/permissions/api/GetHealthPermissionsFlagsUseCaseTest.kt13
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/permissions/api/HealthPermissionManagerImplTest.kt107
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/permissions/api/LoadAccessDateUseCaseTest.kt69
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/HealthPermissionTypesFragmentTest.kt397
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/api/FilterPermissionTypesUseCaseTest.kt (renamed from apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/FilterPermissionTypesUseCaseTest.kt)8
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/api/LoadContributingAppsUseCaseTest.kt138
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/api/LoadPermissionTypesUseCaseTest.kt129
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/api/LoadPriorityListUseCaseTest.kt81
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/api/UpdatePriorityListUseCaseTest.kt76
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/prioritylist/PriorityListAdapterTest.kt63
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/selectabledeletion/DeletionTypeTest.kt101
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/utils/ApiExtensions.kt22
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/utils/AppStoreUtilTest.kt44
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/utils/LocalDateTimeFormatterTest.kt165
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/utils/TestConstants.kt73
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/utils/TestTimeSource.kt20
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/utils/TimeExtensionsTest.kt35
-rw-r--r--apk/tests/src/com/android/healthconnect/controller/tests/utils/di/FakeComponents.kt193
-rw-r--r--framework/java/android/health/connect/AggregateRecordsResponse.java16
-rw-r--r--framework/java/android/health/connect/TimeRangeFilterHelper.java12
-rw-r--r--framework/java/android/health/connect/aidl/AggregateDataResponseParcel.java73
-rw-r--r--framework/java/android/health/connect/aidl/DeletedLogsParcel.java88
-rw-r--r--framework/java/android/health/connect/aidl/InsertRecordsResponseParcel.java10
-rw-r--r--framework/java/android/health/connect/changelog/ChangeLogsResponse.java18
-rw-r--r--framework/java/android/health/connect/datatypes/CyclingPedalingCadenceRecord.java5
-rw-r--r--framework/java/android/health/connect/datatypes/DataOrigin.java3
-rw-r--r--framework/java/android/health/connect/datatypes/Device.java2
-rw-r--r--framework/java/android/health/connect/datatypes/HeartRateRecord.java5
-rw-r--r--framework/java/android/health/connect/datatypes/InstantRecord.java3
-rw-r--r--framework/java/android/health/connect/datatypes/IntervalRecord.java3
-rw-r--r--framework/java/android/health/connect/datatypes/MenstruationPeriodRecord.java3
-rw-r--r--framework/java/android/health/connect/datatypes/Metadata.java2
-rw-r--r--framework/java/android/health/connect/datatypes/PowerRecord.java3
-rw-r--r--framework/java/android/health/connect/datatypes/Record.java3
-rw-r--r--framework/java/android/health/connect/datatypes/SpeedRecord.java5
-rw-r--r--framework/java/android/health/connect/datatypes/StepsCadenceRecord.java5
-rw-r--r--framework/java/android/health/connect/datatypes/TotalCaloriesBurnedRecord.java3
-rw-r--r--framework/java/android/health/connect/datatypes/WheelchairPushesRecord.java3
-rw-r--r--framework/java/android/health/connect/internal/ParcelUtils.java9
-rw-r--r--service/java/com/android/server/healthconnect/HealthConnectDeviceConfigManager.java28
-rw-r--r--service/java/com/android/server/healthconnect/HealthConnectServiceImpl.java47
-rw-r--r--service/java/com/android/server/healthconnect/permission/DataPermissionEnforcer.java20
-rw-r--r--service/java/com/android/server/healthconnect/storage/datatypehelpers/AccessLogsHelper.java3
-rw-r--r--service/java/com/android/server/healthconnect/storage/datatypehelpers/HealthDataCategoryPriorityHelper.java13
-rw-r--r--service/java/com/android/server/healthconnect/storage/datatypehelpers/aggregation/PriorityRecordsAggregator.java7
-rw-r--r--tests/cts/hostsidetests/healthconnect/HealthConnectTestHelper/src/android/healthconnect/cts/testhelper/DailyLogsTests.java44
-rw-r--r--tests/cts/hostsidetests/healthconnect/HealthConnectTestHelper/src/android/healthconnect/cts/testhelper/HealthConnectServiceLogsTests.java3
-rw-r--r--tests/cts/hostsidetests/healthconnect/HealthConnectTestHelper/src/android/healthconnect/cts/testhelper/HealthConnectTestHelper.java46
-rw-r--r--tests/cts/hostsidetests/healthconnect/HealthConnectTestHelper/src/android/healthconnect/cts/testhelper/TestHelperUtils.java27
-rw-r--r--tests/cts/hostsidetests/healthconnect/device/src/android/healthconnect/cts/device/HealthConnectDeviceTest.java270
-rw-r--r--tests/cts/hostsidetests/healthconnect/host/src/android/healthconnect/cts/dailyjob/DailyDeleteAccessLogTest.java90
-rw-r--r--tests/cts/hostsidetests/healthconnect/host/src/android/healthconnect/cts/logging/HealthConnectDailyLogsStatsTests.java36
-rw-r--r--tests/cts/hostsidetests/healthconnect/host/src/android/healthconnect/cts/logging/HealthConnectServiceStatsTests.java2
-rw-r--r--tests/cts/hostsidetests/healthconnect/host/src/android/healthconnect/cts/logging/HealthConnectUiLogsTests.kt2
-rw-r--r--tests/cts/hostsidetests/healthconnect/host/src/android/healthconnect/cts/logging/HostSideTestsUtils.java44
-rw-r--r--tests/cts/hostsidetests/healthconnect/host/src/util/HostSideTestUtil.java130
-rw-r--r--tests/cts/hostsidetests/healthconnect/libs/HealthConnectTestLib/src/android/healthconnect/cts/lib/MultiAppTestUtils.java20
-rw-r--r--tests/cts/src/android/healthconnect/cts/HealthConnectManagerTest.java110
-rw-r--r--tests/cts/src/android/healthconnect/cts/SharedMemoryTest.java174
-rw-r--r--tests/cts/src/android/healthconnect/cts/StepsRecordTest.java67
-rw-r--r--tests/cts/src/android/healthconnect/cts/WeightRecordTest.java64
-rw-r--r--tests/cts/src/android/healthconnect/cts/ratelimiter/RateLimiterTest.java13
-rw-r--r--tests/cts/utils/HealthConnectTestUtils/src/android/healthconnect/cts/utils/TestUtils.java54
-rw-r--r--tests/integrationtests/src/android/healthconnect/tests/backgroundread/BackgroundReadTest.java7
-rw-r--r--tests/unittests/src/android/healthconnect/RateLimiterTest.java90
-rw-r--r--tests/unittests/src/com/android/server/healthconnect/TestUtils.java19
-rw-r--r--tests/unittests/src/com/android/server/healthconnect/permission/FirstGrantTimeUnitTest.java4
-rw-r--r--tests/unittests/src/com/android/server/healthconnect/storage/datatypehelpers/HealthDataCategoryPriorityHelperTest.java46
222 files changed, 8251 insertions, 2365 deletions
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 521bc661..2ed54e68 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -40,7 +40,12 @@
"name": "HealthFitnessIntegrationTests"
},
{
- "name": "HealthFitnessUnitTests"
+ "name": "HealthFitnessUnitTests",
+ "options": [
+ {
+ "exclude-filter": "org.junit.Ignore"
+ }
+ ]
},
{
"name": "HealthConnectBackupRestoreUnitTests"
diff --git a/apk/res/layout/migration_in_progress_screen.xml b/apk/res/layout/migration_in_progress_screen.xml
index 4f52f065..23b8e2a9 100644
--- a/apk/res/layout/migration_in_progress_screen.xml
+++ b/apk/res/layout/migration_in_progress_screen.xml
@@ -55,13 +55,6 @@
android:indeterminate="true"
/>
- <TextView
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/migration_in_progress_screen_integration_dont_close"
- android:textAppearance="?attr/textAppearanceSubheader2"
- />
-
</LinearLayout>
</ScrollView>
diff --git a/apk/res/navigation/data_nav_graph.xml b/apk/res/navigation/data_nav_graph.xml
index a12ddcf3..df80c113 100644
--- a/apk/res/navigation/data_nav_graph.xml
+++ b/apk/res/navigation/data_nav_graph.xml
@@ -71,6 +71,9 @@
<action
android:id="@+id/action_healthPermissionTypes_to_unitsFragment"
app:destination="@+id/unitFragment" />
+ <action
+ android:id="@+id/action_healthPermissionTypes_to_dataSourcesAndPriority"
+ app:destination="@+id/dataSourcesFragment"/>
</fragment>
<fragment
@@ -110,6 +113,26 @@
app:argType="string" />
</fragment>
+ <fragment
+ android:id="@+id/dataSourcesFragment"
+ android:name="com.android.healthconnect.controller.datasources.DataSourcesFragment"
+ android:label="@string/data_sources_and_priority_title">
+ <action
+ android:id="@+id/action_dataSourcesFragment_to_addAnAppFragment"
+ app:destination="@id/addAnAppFragment"/>
+ </fragment>
+
+ <fragment
+ android:id="@+id/addAnAppFragment"
+ android:name="com.android.healthconnect.controller.datasources.AddAnAppFragment"
+ android:label="@string/data_sources_add_app">
+ <action
+ android:id="@+id/action_addAnAppFragment_to_dataSourcesFragment"
+ app:popUpTo="@id/dataSourcesFragment"
+ app:popUpToInclusive="true"
+ app:destination="@id/dataSourcesFragment"/>
+ </fragment>
+
<activity
android:id="@+id/manageAppPermissions"
app:action="android.health.connect.action.MANAGE_HEALTH_PERMISSIONS">
diff --git a/apk/res/values-af/strings.xml b/apk/res/values-af/strings.xml
index fb602d2e..8bc248e3 100644
--- a/apk/res/values-af/strings.xml
+++ b/apk/res/values-af/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Geen"</string>
<string name="entry_details_title" msgid="590184849040247850">"Invoerbesonderhede"</string>
<string name="backup_title" msgid="211503191266235085">"Rugsteun"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Databronne en -prioriteit"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Databronne en prioriteit"</string>
<string name="set_units_title" msgid="2657822539603758029">"Stel eenhede"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Onlangse toegang"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Geen apps het onlangs toegang tot Health Connect gekry nie"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Hoe bronne en prioritisering werk"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Voeg ’n app by"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Wysig appbronne"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Toestelverstek"</string>
<string name="app_data_title" msgid="6499967982291000837">"Appdata"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Data vanaf apps met toegang tot Health Connect sal hier gewys word"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Dag"</string>
diff --git a/apk/res/values-am/strings.xml b/apk/res/values-am/strings.xml
index f39dbd2a..87335ba3 100644
--- a/apk/res/values-am/strings.xml
+++ b/apk/res/values-am/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"ምንም"</string>
<string name="entry_details_title" msgid="590184849040247850">"የግቤት ዝርዝሮች"</string>
<string name="backup_title" msgid="211503191266235085">"ምትኬ"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"የውሂብ ምንጮች እና ቅድሚያ"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"የውሂብ ምንጮች እና ቅድሚያ"</string>
<string name="set_units_title" msgid="2657822539603758029">"ምደባዎችን አቀናብር"</string>
<string name="recent_access_header" msgid="7623497371790225888">"የቅርብ ጊዜ መዳረሻ"</string>
<string name="no_recent_access" msgid="4724297929902441784">"በቅርብ ጊዜ ምንም መተግበሪያዎች የጤና አገናኝን አልደረሱበትም"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"ምንጮች እና ቅድሚያ አሰጣጥ እንዴት እንደሚሰሩ"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"መተግበሪያ ያክሉ"</string>
<string name="edit_data_sources" msgid="79641360876849547">"የመተግበሪያ ምንጮችን ያርትዑ"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"የመሣሪያ ነባሪ"</string>
<string name="app_data_title" msgid="6499967982291000837">"የመተግበሪያ ውሂብ"</string>
<string name="no_data_footer" msgid="4777297654713673100">"ወደ የጤና አገናኝ መዳረሻ ካላቸው መተግበሪያዎች ያለ ውሂብ እዚህ ይታያል"</string>
<string name="date_picker_day" msgid="3076687507968958991">"ቀን"</string>
diff --git a/apk/res/values-ar/strings.xml b/apk/res/values-ar/strings.xml
index e544c1b9..5b27ae5a 100644
--- a/apk/res/values-ar/strings.xml
+++ b/apk/res/values-ar/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"ما مِن تطبيقات"</string>
<string name="entry_details_title" msgid="590184849040247850">"تفاصيل الإدخال"</string>
<string name="backup_title" msgid="211503191266235085">"الاحتفاظ بنسخة احتياطية"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"أولوية مصادر البيانات"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"مصادر البيانات وأولوية التطبيقات"</string>
<string name="set_units_title" msgid="2657822539603758029">"ضبط الوحدات"</string>
<string name="recent_access_header" msgid="7623497371790225888">"التطبيقات التي وصلت مؤخرًا إلى البيانات الصحية"</string>
<string name="no_recent_access" msgid="4724297929902441784">"لم تستخدم أي تطبيقات Health Connect مؤخرًا."</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"آلية عمل أولوية المصادر"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"إضافة تطبيق"</string>
<string name="edit_data_sources" msgid="79641360876849547">"تعديل مصادر التطبيقات"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"التطبيق التلقائي في الجهاز"</string>
<string name="app_data_title" msgid="6499967982291000837">"بيانات التطبيق"</string>
<string name="no_data_footer" msgid="4777297654713673100">"ستظهر هنا البيانات الواردة من التطبيقات التي يمكنها الوصول إلى Health Connect."</string>
<string name="date_picker_day" msgid="3076687507968958991">"يوم"</string>
diff --git a/apk/res/values-as/strings.xml b/apk/res/values-as/strings.xml
index 2e816b5e..1ba2a040 100644
--- a/apk/res/values-as/strings.xml
+++ b/apk/res/values-as/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"নাই"</string>
<string name="entry_details_title" msgid="590184849040247850">"প্ৰৱিষ্টিৰ সবিশেষ"</string>
<string name="backup_title" msgid="211503191266235085">"বেকআপ"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"ডেটাৰ উৎস আৰু অগ্ৰাধিকাৰ"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"ডেটাৰ উৎস আৰু অগ্ৰাধিকাৰ"</string>
<string name="set_units_title" msgid="2657822539603758029">"একক ছেট কৰক"</string>
<string name="recent_access_header" msgid="7623497371790225888">"শেহতীয়া এক্সেছ"</string>
<string name="no_recent_access" msgid="4724297929902441784">"শেহতীয়াকৈ কোনো এপে Health Connect এক্সেছ কৰা নাই"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"ডেটাৰ উৎস আৰু অগ্ৰাধিকাৰে কেনেকৈ কাম কৰে"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"এটা এপ্‌ যোগ দিয়ক"</string>
<string name="edit_data_sources" msgid="79641360876849547">"এপৰ উৎস সম্পাদনা কৰক"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"ডিভাইচৰ ডিফ’ল্ট"</string>
<string name="app_data_title" msgid="6499967982291000837">"এপৰ ডেটা"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Health Connectৰ এক্সেছ থকা এপ্‌সমূহৰ ডেটা ইয়াত দেখুওৱা হ’ব"</string>
<string name="date_picker_day" msgid="3076687507968958991">"দিন"</string>
diff --git a/apk/res/values-az/strings.xml b/apk/res/values-az/strings.xml
index 04be0b97..c486ccb3 100644
--- a/apk/res/values-az/strings.xml
+++ b/apk/res/values-az/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Heç biri"</string>
<string name="entry_details_title" msgid="590184849040247850">"Giriş detalları"</string>
<string name="backup_title" msgid="211503191266235085">"Yedəkləyin"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Data mənbələri və prioritet"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Data mənbələri və prioritet"</string>
<string name="set_units_title" msgid="2657822539603758029">"Vahid təyini"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Son giriş"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Bu yaxınlarda heç bir tətbiq Health Connect-ə giriş etməyib"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Mənbə və prioritetləşdirmə haqqında"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Tətbiq əlavə edin"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Tətbiq mənbələrini redaktə edin"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Cihaz defoltu"</string>
<string name="app_data_title" msgid="6499967982291000837">"Tətbiq datası"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Health Connect-ə girişi olan tətbiqlərin datası burada göstəriləcək"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Gün"</string>
diff --git a/apk/res/values-b+sr+Latn/strings.xml b/apk/res/values-b+sr+Latn/strings.xml
index 2a6878e6..af57f2ab 100644
--- a/apk/res/values-b+sr+Latn/strings.xml
+++ b/apk/res/values-b+sr+Latn/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Ništa"</string>
<string name="entry_details_title" msgid="590184849040247850">"Detalji unosa"</string>
<string name="backup_title" msgid="211503191266235085">"Rezervna kopija"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Izvori podataka i prioritet"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Izvori podataka i prioritet"</string>
<string name="set_units_title" msgid="2657822539603758029">"Podesi jedinice"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Nedavni pristup"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Nijedna aplikacija nije nedavno pristupila Povezivanju zdravlja"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Kako izvori i određivanje prioriteta rade"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Dodaj aplikaciju"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Izmeni izvore aplikacija"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Podrazumevano za uređaj"</string>
<string name="app_data_title" msgid="6499967982291000837">"Podaci aplikacija"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Podaci iz aplikacija sa pristupom Povezivanju zdravlja prikazaće se ovde"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Dan"</string>
diff --git a/apk/res/values-be/strings.xml b/apk/res/values-be/strings.xml
index 2dea3236..1d29a218 100644
--- a/apk/res/values-be/strings.xml
+++ b/apk/res/values-be/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Няма"</string>
<string name="entry_details_title" msgid="590184849040247850">"Звесткі пра ўвод"</string>
<string name="backup_title" msgid="211503191266235085">"Рэзервовае капіраванне"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Крыніцы даных і прыярытэт"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Крыніцы даных і прыярытэт"</string>
<string name="set_units_title" msgid="2657822539603758029">"Задаць адзінкі вымярэння"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Апошні доступ да даных геалакацыі"</string>
<string name="no_recent_access" msgid="4724297929902441784">"У апошні час праграмы не атрымлівалі доступу да Здароўя і спорту"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Як працуюць крыніцы даных і прыярытэт"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Дадаць праграму"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Змяніць спіс праграм – крыніц даных"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Стандартная праграма прылады"</string>
<string name="app_data_title" msgid="6499967982291000837">"Даныя праграмы"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Тут будуць паказвацца даныя з праграм, у якіх ёсць доступ да \"Здароўя і спорта\""</string>
<string name="date_picker_day" msgid="3076687507968958991">"Дзень"</string>
diff --git a/apk/res/values-bg/strings.xml b/apk/res/values-bg/strings.xml
index 6b188984..37a8a9aa 100644
--- a/apk/res/values-bg/strings.xml
+++ b/apk/res/values-bg/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Няма"</string>
<string name="entry_details_title" msgid="590184849040247850">"Подробности за записа"</string>
<string name="backup_title" msgid="211503191266235085">"Създаване на резервни копия"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Източници на данни и приоритет"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Източници на данни и приоритет"</string>
<string name="set_units_title" msgid="2657822539603758029">"Задаване на мерни единици"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Скорошен достъп"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Няма приложения, наскоро осъществили достъп до Health Connect"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Как работят източниците и задаването на приоритет"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Добавяне на приложение"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Редактиране на източниците на приложения"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Стандартно за устройството"</string>
<string name="app_data_title" msgid="6499967982291000837">"Данни от приложението"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Тук ще се показват данните от приложенията, които имат достъп до Health Connect"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Ден"</string>
diff --git a/apk/res/values-bn/strings.xml b/apk/res/values-bn/strings.xml
index 001d1c35..fd5dd865 100644
--- a/apk/res/values-bn/strings.xml
+++ b/apk/res/values-bn/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"কোনওটিই নয়"</string>
<string name="entry_details_title" msgid="590184849040247850">"এন্ট্রি সংক্রান্ত বিবরণ"</string>
<string name="backup_title" msgid="211503191266235085">"ব্যাক-আপ নিন"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"ডেটা সোর্স এবং প্রায়োরিটি"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"ডেটা সোর্স ও প্রায়োরিটি"</string>
<string name="set_units_title" msgid="2657822539603758029">"ইউনিট সেট করুন"</string>
<string name="recent_access_header" msgid="7623497371790225888">"সাম্প্রতিক অ্যাক্সেস"</string>
<string name="no_recent_access" msgid="4724297929902441784">"সম্প্রতি কোনও অ্যাপ Health Connect অ্যাক্সেস করেনি"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"সোর্স এবং প্রায়োরিটি কীভাবে কাজ করে"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"অ্যাপ যোগ করুন"</string>
<string name="edit_data_sources" msgid="79641360876849547">"অ্যাপের সোর্স এডিট করুন"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"ডিভাইসের ডিফল্ট অ্যাপ"</string>
<string name="app_data_title" msgid="6499967982291000837">"অ্যাপ ডেটা"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Health Connect-এর অ্যাক্সেস সহ অ্যাপ থেকে পাওয়া ডেটা এখানে দেখানো হবে"</string>
<string name="date_picker_day" msgid="3076687507968958991">"দিনের"</string>
diff --git a/apk/res/values-bs/strings.xml b/apk/res/values-bs/strings.xml
index 523270a5..d0bffe4b 100644
--- a/apk/res/values-bs/strings.xml
+++ b/apk/res/values-bs/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Ništa"</string>
<string name="entry_details_title" msgid="590184849040247850">"Detalji unosa"</string>
<string name="backup_title" msgid="211503191266235085">"Sigurnosna kopija"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Izvori podataka i prioritet"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Izvori podataka i prioritet"</string>
<string name="set_units_title" msgid="2657822539603758029">"Postavite jedinice"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Nedavni pristup"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Nijedna aplikacija nije nedavno pristupila Health Connectu"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Kako funkcioniraju izvori i dodjeljivanje prioriteta"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Dodaj aplikaciju"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Uredi izvore aplikacija"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Zadana postavka uređaja"</string>
<string name="app_data_title" msgid="6499967982291000837">"Podaci aplikacije"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Ovdje će se prikazivati podaci iz aplikacija s pristupom Health Connectu"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Dan"</string>
diff --git a/apk/res/values-ca/strings.xml b/apk/res/values-ca/strings.xml
index b9d99b95..916b6cfe 100644
--- a/apk/res/values-ca/strings.xml
+++ b/apk/res/values-ca/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Cap"</string>
<string name="entry_details_title" msgid="590184849040247850">"Detalls de l\'entrada"</string>
<string name="backup_title" msgid="211503191266235085">"Còpia de seguretat"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Fonts de dades i prioritat"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Fonts de dades i prioritat"</string>
<string name="set_units_title" msgid="2657822539603758029">"Defineix les unitats"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Accés recent"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Cap aplicació ha accedit a Salut connectada recentment"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Com funcionen les fonts i la priorització"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Afegeix una aplicació"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Edita les fonts d\'aplicacions"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Opció predeterminada del dispositiu"</string>
<string name="app_data_title" msgid="6499967982291000837">"Dades de l\'aplicació"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Les dades de les aplicacions amb accés a Salut connectada es mostraran aquí"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Dia"</string>
diff --git a/apk/res/values-cs/strings.xml b/apk/res/values-cs/strings.xml
index bb6cf7f7..670001a2 100644
--- a/apk/res/values-cs/strings.xml
+++ b/apk/res/values-cs/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Žádné"</string>
<string name="entry_details_title" msgid="590184849040247850">"Podrobnosti o záznamu"</string>
<string name="backup_title" msgid="211503191266235085">"Záloha"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Zdroje dat a priorita"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Zdroje dat a priorita"</string>
<string name="set_units_title" msgid="2657822539603758029">"Nastavit jednotky"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Nedávný přístup"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Žádné aplikace ke službě Health Connect v poslední době nepřistupovaly"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Jak fungují zdroje a určování priorit"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Přidat aplikaci"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Upravit zdroje aplikací"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Výchozí nastavení zařízení"</string>
<string name="app_data_title" msgid="6499967982291000837">"Data aplikace"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Tady se budou zobrazovat data z aplikací s přístupem na platformu Health Connect"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Den"</string>
diff --git a/apk/res/values-da/strings.xml b/apk/res/values-da/strings.xml
index 98c58b41..8536a6bd 100644
--- a/apk/res/values-da/strings.xml
+++ b/apk/res/values-da/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Ingen"</string>
<string name="entry_details_title" msgid="590184849040247850">"Oplysninger om dataposten"</string>
<string name="backup_title" msgid="211503191266235085">"Sikkerhedskopiering"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Datakilder og -prioritet"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Datakilder og prioritet"</string>
<string name="set_units_title" msgid="2657822539603758029">"Angiv enheder"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Seneste adgang"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Ingen apps har for nylig tilgået Health Connect"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Sådan fungerer kilder og prioritering"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Tilføj en app"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Rediger appkilder"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Enhedens standardapp"</string>
<string name="app_data_title" msgid="6499967982291000837">"Appdata"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Data fra apps med adgang til Health Connect vises her"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Dag"</string>
diff --git a/apk/res/values-de/strings.xml b/apk/res/values-de/strings.xml
index 73c875db..5a89f789 100644
--- a/apk/res/values-de/strings.xml
+++ b/apk/res/values-de/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Keine"</string>
<string name="entry_details_title" msgid="590184849040247850">"Eintragsdetails"</string>
<string name="backup_title" msgid="211503191266235085">"Back-up machen"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Datenquellen und Priorität"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Datenquellen und Priorität"</string>
<string name="set_units_title" msgid="2657822539603758029">"Einheiten festlegen"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Letzte Zugriffe"</string>
<string name="no_recent_access" msgid="4724297929902441784">"In letzter Zeit hat keine App auf Health Connect zugegriffen"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"So funktionieren Quellen und Priorisierung"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"App hinzufügen"</string>
<string name="edit_data_sources" msgid="79641360876849547">"App-Quellen bearbeiten"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Standard-App bei diesem Gerät"</string>
<string name="app_data_title" msgid="6499967982291000837">"App-Daten"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Daten aus Apps mit Zugriff auf Health Connect werden hier angezeigt"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Tag"</string>
diff --git a/apk/res/values-el/strings.xml b/apk/res/values-el/strings.xml
index ae441f5d..805697be 100644
--- a/apk/res/values-el/strings.xml
+++ b/apk/res/values-el/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Καμία"</string>
<string name="entry_details_title" msgid="590184849040247850">"Λεπτομέρειες καταχώρισης"</string>
<string name="backup_title" msgid="211503191266235085">"Δημιουργία αντιγράφων ασφαλείας"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Πηγές δεδομένων και προτεραιότητα"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Πηγές δεδομένων και προτεραιότητα"</string>
<string name="set_units_title" msgid="2657822539603758029">"Ορισμός μονάδων"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Πρόσφατη πρόσβαση"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Καμία εφαρμογή δεν απέκτησε πρόσφατα πρόσβαση στο Health Connect"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Πώς λειτουργούν οι πηγές και η προτεραιότητα"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Προσθήκη εφαρμογής"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Επεξεργασία πηγών εφαρμογών"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Προεπιλογή συσκευής"</string>
<string name="app_data_title" msgid="6499967982291000837">"Δεδομένα εφαρμογών"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Τα δεδομένα από εφαρμογές με πρόσβαση στο Health Connect θα εμφανίζονται εδώ"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Ημέρα"</string>
diff --git a/apk/res/values-en-rAU/strings.xml b/apk/res/values-en-rAU/strings.xml
index e835a41e..326bd150 100644
--- a/apk/res/values-en-rAU/strings.xml
+++ b/apk/res/values-en-rAU/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"None"</string>
<string name="entry_details_title" msgid="590184849040247850">"Entry details"</string>
<string name="backup_title" msgid="211503191266235085">"Backup"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Data sources and priority"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Data sources and priority"</string>
<string name="set_units_title" msgid="2657822539603758029">"Set units"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Recent access"</string>
<string name="no_recent_access" msgid="4724297929902441784">"No apps recently accessed Health Connect"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"How sources and prioritisation work"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Add an app"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Edit app sources"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Device default"</string>
<string name="app_data_title" msgid="6499967982291000837">"App data"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Data from apps with access to Health Connect will show here"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Day"</string>
diff --git a/apk/res/values-en-rCA/strings.xml b/apk/res/values-en-rCA/strings.xml
index c707d17e..7b20f194 100644
--- a/apk/res/values-en-rCA/strings.xml
+++ b/apk/res/values-en-rCA/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"None"</string>
<string name="entry_details_title" msgid="590184849040247850">"Entry details"</string>
<string name="backup_title" msgid="211503191266235085">"Backup"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Data sources &amp; priority"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Data sources and priority"</string>
<string name="set_units_title" msgid="2657822539603758029">"Set units"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Recent access"</string>
<string name="no_recent_access" msgid="4724297929902441784">"No apps recently accessed Health Connect"</string>
diff --git a/apk/res/values-en-rGB/strings.xml b/apk/res/values-en-rGB/strings.xml
index e835a41e..326bd150 100644
--- a/apk/res/values-en-rGB/strings.xml
+++ b/apk/res/values-en-rGB/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"None"</string>
<string name="entry_details_title" msgid="590184849040247850">"Entry details"</string>
<string name="backup_title" msgid="211503191266235085">"Backup"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Data sources and priority"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Data sources and priority"</string>
<string name="set_units_title" msgid="2657822539603758029">"Set units"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Recent access"</string>
<string name="no_recent_access" msgid="4724297929902441784">"No apps recently accessed Health Connect"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"How sources and prioritisation work"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Add an app"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Edit app sources"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Device default"</string>
<string name="app_data_title" msgid="6499967982291000837">"App data"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Data from apps with access to Health Connect will show here"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Day"</string>
diff --git a/apk/res/values-en-rIN/strings.xml b/apk/res/values-en-rIN/strings.xml
index e835a41e..326bd150 100644
--- a/apk/res/values-en-rIN/strings.xml
+++ b/apk/res/values-en-rIN/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"None"</string>
<string name="entry_details_title" msgid="590184849040247850">"Entry details"</string>
<string name="backup_title" msgid="211503191266235085">"Backup"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Data sources and priority"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Data sources and priority"</string>
<string name="set_units_title" msgid="2657822539603758029">"Set units"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Recent access"</string>
<string name="no_recent_access" msgid="4724297929902441784">"No apps recently accessed Health Connect"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"How sources and prioritisation work"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Add an app"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Edit app sources"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Device default"</string>
<string name="app_data_title" msgid="6499967982291000837">"App data"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Data from apps with access to Health Connect will show here"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Day"</string>
diff --git a/apk/res/values-en-rXC/strings.xml b/apk/res/values-en-rXC/strings.xml
index 1e2d87be..8dedcede 100644
--- a/apk/res/values-en-rXC/strings.xml
+++ b/apk/res/values-en-rXC/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‎‏‏‎‏‏‎‏‏‏‎‏‏‎‏‎‎‎‏‎‎‎‎‏‎‎‏‎‎‏‏‎‎‏‎‎‎‎‏‎‏‎‎‏‏‎‎‏‎‏‏‎‏‏‏‏‏‎‎‏‏‎‎‎None‎‏‎‎‏‎"</string>
<string name="entry_details_title" msgid="590184849040247850">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‎‎‏‎‎‎‎‎‏‏‎‎‎‎‏‏‎‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‎‎‏‏‎‏‏‎‏‏‏‎‎‏‎‎‎‏‏‎‎‎‎‏‎‏‎‏‎‎Entry details‎‏‎‎‏‎"</string>
<string name="backup_title" msgid="211503191266235085">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‎‏‎‏‎‏‏‏‎‏‏‏‏‎‏‏‎‏‎‎‏‎‎‎‎‏‎‎‎‎‎‏‎‏‎‎‎‎‏‎‎‏‎‎‏‎‎‎‏‎‎‏‎‏‏‎‎‏‏‎‏‎Backup‎‏‎‎‏‎"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‎‏‏‎‏‎‏‏‎‏‏‏‎‏‏‎‏‏‎‎‎‎‏‏‏‎‎‎‎‎‎‏‏‎‏‎‎‎‎‏‎‏‏‎‏‎‎‏‎‏‎‎‏‎‎‎‏‏‏‏‎‏‏‎Data sources &amp; priority‎‏‎‎‏‎"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‎‏‎‎‎‎‎‏‏‎‎‎‎‎‏‎‎‏‏‎‎‎‏‏‎‏‏‏‎‎‏‎‎‏‎‎‎‎‏‎‎‏‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‎‎‏‏‏‎‎Data sources and priority‎‏‎‎‏‎"</string>
<string name="set_units_title" msgid="2657822539603758029">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‎‏‎‎‏‎‎‏‏‏‎‎‎‏‎‎‏‏‏‏‎‏‏‏‎‎‎‎‏‎‎‏‎‎‏‏‏‎‏‎‎‎‏‏‎‏‏‏‏‎‎‏‎‏‏‏‏‎‎‏‏‎‏‎Set units‎‏‎‎‏‎"</string>
<string name="recent_access_header" msgid="7623497371790225888">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‏‏‏‎‏‎‎‏‏‏‎‎‏‏‎‎‎‎‎‏‏‎‏‎‎‏‏‏‎‏‏‏‏‏‏‏‎‏‏‏‏‎‏‎‏‏‏‎‎‎‏‏‎‎‎‏‏‏‏‎‎‎‎‎‎Recent access‎‏‎‎‏‎"</string>
<string name="no_recent_access" msgid="4724297929902441784">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‏‏‎‎‎‎‎‏‏‎‎‏‎‎‎‎‎‎‎‏‎‎‏‏‏‏‏‎‏‏‏‏‏‏‎‏‎‏‎‎‏‏‏‏‎‎‏‎‏‏‎‎‎‏‎‏‎‎‏‏‏‎‎‎‎No apps recently accessed Health Connect‎‏‎‎‏‎"</string>
diff --git a/apk/res/values-es-rUS/strings.xml b/apk/res/values-es-rUS/strings.xml
index 05eeff43..041c8f8e 100644
--- a/apk/res/values-es-rUS/strings.xml
+++ b/apk/res/values-es-rUS/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Ninguna"</string>
<string name="entry_details_title" msgid="590184849040247850">"Información de la entrada"</string>
<string name="backup_title" msgid="211503191266235085">"Copia de seguridad"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Prioridad y fuentes de datos"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Prioridad y fuentes de datos"</string>
<string name="set_units_title" msgid="2657822539603758029">"Establecer unidades"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Acceso reciente"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Ninguna app accedió recientemente a Health Connect"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Cómo funcionan la priorización y las fuentes"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Agrega una app"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Editar fuentes de app"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Opción predeterminada del dispositivo"</string>
<string name="app_data_title" msgid="6499967982291000837">"Datos de app"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Aquí se mostrará la información de apps con acceso a Health Connect"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Día"</string>
diff --git a/apk/res/values-es/strings.xml b/apk/res/values-es/strings.xml
index 84217b44..48fdd563 100644
--- a/apk/res/values-es/strings.xml
+++ b/apk/res/values-es/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Ninguna"</string>
<string name="entry_details_title" msgid="590184849040247850">"Detalles de la entrada"</string>
<string name="backup_title" msgid="211503191266235085">"Copia de seguridad"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Fuentes de datos y prioridad"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Fuentes de datos y prioridad"</string>
<string name="set_units_title" msgid="2657822539603758029">"Configurar unidades"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Acceso reciente"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Ninguna aplicación ha accedido a Salud conectada recientemente"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Cómo funcionan las fuentes y la priorización"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Añadir una aplicación"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Editar fuentes de la aplicación"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Predeterminado por el dispositivo"</string>
<string name="app_data_title" msgid="6499967982291000837">"Datos de aplicaciones"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Los datos de las aplicaciones con acceso a Salud conectada se mostrarán aquí"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Día"</string>
diff --git a/apk/res/values-et/strings.xml b/apk/res/values-et/strings.xml
index 4854ec80..5f31814e 100644
--- a/apk/res/values-et/strings.xml
+++ b/apk/res/values-et/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Pole"</string>
<string name="entry_details_title" msgid="590184849040247850">"Kirje üksikasjad"</string>
<string name="backup_title" msgid="211503191266235085">"Varundamine"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Andmeallikad ja prioriteetsus"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Andmeallikad ja prioriteet"</string>
<string name="set_units_title" msgid="2657822539603758029">"Ühikute määramine"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Hiljutine juurdepääs"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Rakendusele Health Connect pole hiljuti ükski rakendus juurde pääsenud"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Kuidas toimivad allikad ja prioriteetsus?"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Rakenduse lisamine"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Rakendusallikate muutmine"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Seadme vaikeseade"</string>
<string name="app_data_title" msgid="6499967982291000837">"Rakenduse andmed"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Nende rakenduste andmed, millel on juurdepääs teenusele Health Connect, kuvatakse siin"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Päev"</string>
diff --git a/apk/res/values-eu/strings.xml b/apk/res/values-eu/strings.xml
index 51eea9bb..dc793eee 100644
--- a/apk/res/values-eu/strings.xml
+++ b/apk/res/values-eu/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Batek ere ez"</string>
<string name="entry_details_title" msgid="590184849040247850">"Sarreraren xehetasunak"</string>
<string name="backup_title" msgid="211503191266235085">"Babeskopiak"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Datu-iturburuak eta lehentasuna"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Datu-iturburuak eta lehentasuna"</string>
<string name="set_units_title" msgid="2657822539603758029">"Ezarritako unitateak"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Datuak atzitu dituzten azkenak"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Ez dago azkenaldian Health Connect atzitu duen aplikaziorik"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Nola funtzionatzen dute iturburuek eta lehenespenak?"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Gehitu aplikazio bat"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Editatu aplikazioen iturburuak"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Gailuko aplikazio lehenetsia"</string>
<string name="app_data_title" msgid="6499967982291000837">"Aplikazioko datuak"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Health Connect erabil dezaketen aplikazioetako datuak agertuko dira hemen"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Egunekoak"</string>
diff --git a/apk/res/values-fa/strings.xml b/apk/res/values-fa/strings.xml
index 4306fc5e..df57cadc 100644
--- a/apk/res/values-fa/strings.xml
+++ b/apk/res/values-fa/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"هیچ‌کدام"</string>
<string name="entry_details_title" msgid="590184849040247850">"جزئیات ورود"</string>
<string name="backup_title" msgid="211503191266235085">"پشتیبان‌گیری"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"منابع داده و اولویت"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"منابع داده و اولویت"</string>
<string name="set_units_title" msgid="2657822539603758029">"تنظیم واحدها"</string>
<string name="recent_access_header" msgid="7623497371790225888">"دسترسی اخیر"</string>
<string name="no_recent_access" msgid="4724297929902441784">"هیچ برنامه‌ای اخیراً به Health Connect دسترسی نداشته است"</string>
@@ -99,8 +99,8 @@
<string name="distance_lowercase_label" msgid="2287154001209381379">"مسافت"</string>
<string name="distance_read_content_description" msgid="8787235642020285789">"خواندن داده مسافت"</string>
<string name="distance_write_content_description" msgid="494549494589487562">"نوشتن داده مسافت"</string>
- <string name="elevation_gained_uppercase_label" msgid="7708101940695442377">"ارتفاع صعودکرده"</string>
- <string name="elevation_gained_lowercase_label" msgid="7532517182346738562">"ارتفاع صعودکرده"</string>
+ <string name="elevation_gained_uppercase_label" msgid="7708101940695442377">"ارتفاع صعودشده"</string>
+ <string name="elevation_gained_lowercase_label" msgid="7532517182346738562">"ارتفاع صعودشده"</string>
<string name="elevation_gained_read_content_description" msgid="6018756385903843355">"خواندن داده ارتفاع صعودشده"</string>
<string name="elevation_gained_write_content_description" msgid="6790199544670231367">"نوشتن داده ارتفاع صعودشده"</string>
<string name="floors_climbed_uppercase_label" msgid="3754372357767832441">"طبقات پیموده‌شده"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"نحوه عملکرد منابع و اولویت‌بندی"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"افزودن برنامه"</string>
<string name="edit_data_sources" msgid="79641360876849547">"ویرایش منابع برنامه"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"پیش‌فرض دستگاه"</string>
<string name="app_data_title" msgid="6499967982291000837">"داده‌های برنامه"</string>
<string name="no_data_footer" msgid="4777297654713673100">"داده‌ها از برنامه‌هایی که به Health Connect دسترسی دارند اینجا نشان داده خواهد شد"</string>
<string name="date_picker_day" msgid="3076687507968958991">"روز"</string>
diff --git a/apk/res/values-fi/strings.xml b/apk/res/values-fi/strings.xml
index c70ce178..db4d336e 100644
--- a/apk/res/values-fi/strings.xml
+++ b/apk/res/values-fi/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"–"</string>
<string name="entry_details_title" msgid="590184849040247850">"Tiedot"</string>
<string name="backup_title" msgid="211503191266235085">"Varmuuskopiointi"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Datalähteet ja prioriteetti"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Datalähteet ja prioriteetit"</string>
<string name="set_units_title" msgid="2657822539603758029">"Aseta yksiköt"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Viimeaikainen käyttö"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Sovellukset eivät ole käyttäneet Health Connectia äskettäin"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Miten lähteet ja priorisointi toimivat"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Sovelluksen lisääminen"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Muokkaa sovelluslähteitä"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Laitteen oletusasetus"</string>
<string name="app_data_title" msgid="6499967982291000837">"Sovellusdata"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Täällä näkyy data sovelluksilta, joilla on pääsy Health Connectiin"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Päivä"</string>
diff --git a/apk/res/values-fr-rCA/strings.xml b/apk/res/values-fr-rCA/strings.xml
index 5d49d2d3..3b694e0f 100644
--- a/apk/res/values-fr-rCA/strings.xml
+++ b/apk/res/values-fr-rCA/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Aucune"</string>
<string name="entry_details_title" msgid="590184849040247850">"Détails de l\'entrée"</string>
<string name="backup_title" msgid="211503191266235085">"Sauvegarde"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Sources de données et priorité"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Source de données et priorité"</string>
<string name="set_units_title" msgid="2657822539603758029">"Configurer les unités"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Accès récents"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Aucune application n\'a accédé à Connexion santé dernièrement"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Fonctionnement des sources et de la hiérarchisation"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Ajouter une application"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Modifier les sources d\'applications"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Valeur par défaut de l\'appareil"</string>
<string name="app_data_title" msgid="6499967982291000837">"Données de l\'application"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Les données des applications qui ont accès à Connexion santé s\'afficheront ici"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Jour"</string>
diff --git a/apk/res/values-fr/strings.xml b/apk/res/values-fr/strings.xml
index e713f984..992ce9e9 100644
--- a/apk/res/values-fr/strings.xml
+++ b/apk/res/values-fr/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Aucune"</string>
<string name="entry_details_title" msgid="590184849040247850">"Détails de l\'entrée"</string>
<string name="backup_title" msgid="211503191266235085">"Sauvegarde"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Priorité des sources de données"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Sources de données et priorité"</string>
<string name="set_units_title" msgid="2657822539603758029">"Définir des unités"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Accès récent"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Aucune appli n\'a accédé à Santé Connect récemment"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Fonctionnement de la priorisation des sources"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Ajouter une application"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Modifier les sources d\'application"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Paramètre par défaut"</string>
<string name="app_data_title" msgid="6499967982291000837">"Données d\'application"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Les données issues des applications ayant accès à Santé Connect s\'afficheront ici."</string>
<string name="date_picker_day" msgid="3076687507968958991">"Jour"</string>
diff --git a/apk/res/values-gl/strings.xml b/apk/res/values-gl/strings.xml
index 61196add..6f59eae8 100644
--- a/apk/res/values-gl/strings.xml
+++ b/apk/res/values-gl/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Ningunha"</string>
<string name="entry_details_title" msgid="590184849040247850">"Detalles da entrada"</string>
<string name="backup_title" msgid="211503191266235085">"Copia de seguranza"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Fontes de datos e prioridade"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Prioridade e fontes de datos"</string>
<string name="set_units_title" msgid="2657822539603758029">"Configurar unidades"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Acceso recente"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Ningunha aplicación accedeu a Saúde conectada recentemente"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Funcionamento das fontes e da priorización"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Engadir unha aplicación"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Editar fontes de aplicacións"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Predeterminada do dispositivo"</string>
<string name="app_data_title" msgid="6499967982291000837">"Datos da aplicación"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Mostraranse aquí os datos das aplicacións con acceso a Saúde conectada"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Día"</string>
diff --git a/apk/res/values-gu/strings.xml b/apk/res/values-gu/strings.xml
index e4de9dfc..62501752 100644
--- a/apk/res/values-gu/strings.xml
+++ b/apk/res/values-gu/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"એકપણ નહીં"</string>
<string name="entry_details_title" msgid="590184849040247850">"એન્ટ્રીની વિગતો"</string>
<string name="backup_title" msgid="211503191266235085">"બૅકઅપ"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"ડેટા સૉર્સ &amp; પ્રાધાન્યતા"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"ડેટા સૉર્સ અને પ્રાધાન્યતા"</string>
<string name="set_units_title" msgid="2657822539603758029">"એકમો સેટ કરો"</string>
<string name="recent_access_header" msgid="7623497371790225888">"તાજેતરનો ઍક્સેસ"</string>
<string name="no_recent_access" msgid="4724297929902441784">"કોઈ ઍપ દ્વારા તાજેતરમાં Health Connectનો ઍક્સેસ કરવામાં આવ્યો નથી"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"સૉર્સના &amp; પ્રાધાન્યતા આપવાના સેટિંગની કાર્ય કરવાની રીત"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"કોઈ ઍપ ઉમેરો"</string>
<string name="edit_data_sources" msgid="79641360876849547">"ઍપના સૉર્સમાં ફેરફાર કરો"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"ડિવાઇસ પર ડિફૉલ્ટ"</string>
<string name="app_data_title" msgid="6499967982291000837">"ઍપનો ડેટા"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Health Connectનો ઍક્સેસ ધરાવતી ઍપનો ડેટા અહીં દેખાશે"</string>
<string name="date_picker_day" msgid="3076687507968958991">"દિવસ"</string>
diff --git a/apk/res/values-hi/strings.xml b/apk/res/values-hi/strings.xml
index 2e3df20b..4bbbf134 100644
--- a/apk/res/values-hi/strings.xml
+++ b/apk/res/values-hi/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"कोई नहीं"</string>
<string name="entry_details_title" msgid="590184849040247850">"एंट्री के बारे में जानकारी"</string>
<string name="backup_title" msgid="211503191266235085">"बैकअप लें"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"डेटा सोर्स और प्राथमिकता की सेटिंग"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"डेटा सोर्स और प्राथमिकता"</string>
<string name="set_units_title" msgid="2657822539603758029">"इकाइयां सेट करें"</string>
<string name="recent_access_header" msgid="7623497371790225888">"हाल ही में, डेटा ऐक्सेस करने वाले ऐप्लिकेशन"</string>
<string name="no_recent_access" msgid="4724297929902441784">"हाल ही में, किसी भी ऐप्लिकेशन ने Health Connect के डेटा का इस्तेमाल नहीं किया है"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"सोर्स और प्राथमिकता की सेटिंग कैसे काम करती है"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"ऐप्लिकेशन जोड़ें"</string>
<string name="edit_data_sources" msgid="79641360876849547">"ऐप्लिकेशन के सोर्स बदलें"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"डिवाइस की डिफ़ॉल्ट सेटिंग"</string>
<string name="app_data_title" msgid="6499967982291000837">"ऐप्लिकेशन का डेटा"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Health Connect का ऐक्सेस रखने वाले ऐप्लिकेशन का डेटा यहां दिखेगा"</string>
<string name="date_picker_day" msgid="3076687507968958991">"दिन"</string>
diff --git a/apk/res/values-hr/strings.xml b/apk/res/values-hr/strings.xml
index 9743acfb..06263bbb 100644
--- a/apk/res/values-hr/strings.xml
+++ b/apk/res/values-hr/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Ništa"</string>
<string name="entry_details_title" msgid="590184849040247850">"Pojedinosti o unosu"</string>
<string name="backup_title" msgid="211503191266235085">"Sigurnosna kopija"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Izvori podataka i prioritet"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Izvori podataka i prioritet"</string>
<string name="set_units_title" msgid="2657822539603758029">"Postavi jedinice"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Nedavni pristup"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Nijedna aplikacija nije pristupila Health Connectu u posljednje vrijeme"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Kako funkcioniraju izvori i prioritizacija"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Dodaj aplikaciju"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Uređivanje izvora aplikacija"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Zadana postavka uređaja"</string>
<string name="app_data_title" msgid="6499967982291000837">"Podaci aplikacije"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Ovdje će se prikazati podaci iz aplikacija s pristupom Health Connectu"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Dan"</string>
diff --git a/apk/res/values-hu/strings.xml b/apk/res/values-hu/strings.xml
index 39ead841..779b4bfb 100644
--- a/apk/res/values-hu/strings.xml
+++ b/apk/res/values-hu/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Nincs"</string>
<string name="entry_details_title" msgid="590184849040247850">"Bejegyzés részletei"</string>
<string name="backup_title" msgid="211503191266235085">"Biztonsági mentés"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Adatforrások és prioritás"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Adatforrások és prioritás"</string>
<string name="set_units_title" msgid="2657822539603758029">"Egységek beállítása"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Legutóbbi hozzáférés"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Egyetlen alkalmazás sem fért hozzá a Health Connect szolgáltatáshoz a közelmúltban"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Hogyan működnek a források és a priorizálás?"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Alkalmazás hozzáadása"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Alkalmazásforrások szerkesztése"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Alapértelmezett"</string>
<string name="app_data_title" msgid="6499967982291000837">"Alkalmazásadatok"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Itt jelennek meg az adatok azokból az alkalmazásokból, amelyeknek hozzáférése van a Health Connecthez"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Nap"</string>
diff --git a/apk/res/values-hy/strings.xml b/apk/res/values-hy/strings.xml
index 36f53456..430268f8 100644
--- a/apk/res/values-hy/strings.xml
+++ b/apk/res/values-hy/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Ընտրված տարրեր չկան"</string>
<string name="entry_details_title" msgid="590184849040247850">"Գրառման մանրամասները"</string>
<string name="backup_title" msgid="211503191266235085">"Պահուստավորում"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Տվյալների աղբյուրներ և առաջնահերթություն"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Տվյալների աղբյուրներ և առաջնահերթություն"</string>
<string name="set_units_title" msgid="2657822539603758029">"Կարգավորել չափման միավորները"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Վերջին օգտագործումը"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Վերջերս ոչ մի հավելված չի օգտագործել Health Connect-ը"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Ինչպես են աղբյուրներն ու առաջնահերթությունն աշխատում"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Ավելացնել հավելված"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Հավելվածների աղբյուրների փոփոխում"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Կանխադրված տարբերակ"</string>
<string name="app_data_title" msgid="6499967982291000837">"Հավելվածի տվյալներ"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Health Connect-ին հասանելիություն ունեցող հավելվածների տվյալները կցուցադրվեն այստեղ"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Օր"</string>
diff --git a/apk/res/values-in/strings.xml b/apk/res/values-in/strings.xml
index 542362b9..62faa905 100644
--- a/apk/res/values-in/strings.xml
+++ b/apk/res/values-in/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Tidak ada"</string>
<string name="entry_details_title" msgid="590184849040247850">"Detail entri"</string>
<string name="backup_title" msgid="211503191266235085">"Pencadangan"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Prioritas &amp; sumber data"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Prioritas dan sumber data"</string>
<string name="set_units_title" msgid="2657822539603758029">"Tetapkan unit"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Akses terbaru"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Tidak ada aplikasi yang baru-baru ini mengakses Health Connect"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Cara kerja sumber &amp; prioritas"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Tambahkan aplikasi"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Edit sumber aplikasi"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Default perangkat"</string>
<string name="app_data_title" msgid="6499967982291000837">"Data aplikasi"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Data dari aplikasi yang memiliki akses ke Health Connect akan muncul di sini"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Hari"</string>
diff --git a/apk/res/values-is/strings.xml b/apk/res/values-is/strings.xml
index 9a2f7bf6..5b9bc2e3 100644
--- a/apk/res/values-is/strings.xml
+++ b/apk/res/values-is/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Ekkert"</string>
<string name="entry_details_title" msgid="590184849040247850">"Upplýsingar um færslu"</string>
<string name="backup_title" msgid="211503191266235085">"Öryggisafrit"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Gagnauppsprettur og forgangur"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Gagnauppsprettur og forgangur"</string>
<string name="set_units_title" msgid="2657822539603758029">"Velja mælieiningar"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Nýlegur aðgangur"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Engin forrit hafa fengið aðgang að Heilsutengingu nýlega"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Virkni uppruna og forgangs"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Bæta við forriti"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Bæta við eða fjarlægja forrit sem veita upplýsingar"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Sjálfgefin stilling tækis"</string>
<string name="app_data_title" msgid="6499967982291000837">"Forritsgögn"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Gögn frá forritum með aðgang að Heilsutengingu birtast hér"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Dagur"</string>
diff --git a/apk/res/values-it/strings.xml b/apk/res/values-it/strings.xml
index 278ce7b1..f3037bb9 100644
--- a/apk/res/values-it/strings.xml
+++ b/apk/res/values-it/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Nessuna"</string>
<string name="entry_details_title" msgid="590184849040247850">"Dettagli voce"</string>
<string name="backup_title" msgid="211503191266235085">"Backup"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Origini di app e priorità"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Origini dati e priorità"</string>
<string name="set_units_title" msgid="2657822539603758029">"Imposta unità"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Accesso recente"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Nessuna app ha usato di recente Connessione Salute"</string>
@@ -788,10 +788,9 @@
<string name="data_sources_empty_state" msgid="1899652759274805556">"Nessuna origine di app"</string>
<string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Dopo aver concesso all\'app le autorizzazioni di scrittura di dati <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>, le origini verranno visualizzate qui."</string>
<string name="data_sources_help_link" msgid="7740264923634947915">"Come funzionano le origini e la prioritizzazione"</string>
- <string name="data_sources_add_app" msgid="319926596123692514">"Aggiungere un\'app"</string>
+ <string name="data_sources_add_app" msgid="319926596123692514">"Aggiungi un\'app"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Modifica origini di app"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Opzione predefinita"</string>
<string name="app_data_title" msgid="6499967982291000837">"Dati dell\'app"</string>
<string name="no_data_footer" msgid="4777297654713673100">"I dati delle app con accesso a Connessione Salute appariranno qui"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Giorno"</string>
diff --git a/apk/res/values-iw/strings.xml b/apk/res/values-iw/strings.xml
index 98b12a25..98ba16dc 100644
--- a/apk/res/values-iw/strings.xml
+++ b/apk/res/values-iw/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"ללא"</string>
<string name="entry_details_title" msgid="590184849040247850">"פרטי כניסה"</string>
<string name="backup_title" msgid="211503191266235085">"גיבוי"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"מקורות הנתונים ועדיפות"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"מקורות הנתונים ועדיפות"</string>
<string name="set_units_title" msgid="2657822539603758029">"הגדרת יחידות"</string>
<string name="recent_access_header" msgid="7623497371790225888">"אפליקציות שניגשו לנתונים לאחרונה"</string>
<string name="no_recent_access" msgid="4724297929902441784">"אף אפליקציה לא ניגשה לאחרונה ל-Health Connect"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"איך מקורות הנתונים והעדיפות פועלים"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"הוספת אפליקציה"</string>
<string name="edit_data_sources" msgid="79641360876849547">"עריכת המקורות של האפליקציות"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"ברירת המחדל של המכשיר"</string>
<string name="app_data_title" msgid="6499967982291000837">"נתוני האפליקציה"</string>
<string name="no_data_footer" msgid="4777297654713673100">"כאן יוצגו נתונים מאפליקציות שיש להן גישה ל-Health Connect"</string>
<string name="date_picker_day" msgid="3076687507968958991">"יום"</string>
diff --git a/apk/res/values-ja/strings.xml b/apk/res/values-ja/strings.xml
index 9d2aa6ad..632d21cc 100644
--- a/apk/res/values-ja/strings.xml
+++ b/apk/res/values-ja/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"なし"</string>
<string name="entry_details_title" msgid="590184849040247850">"エントリの詳細"</string>
<string name="backup_title" msgid="211503191266235085">"バックアップ"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"データソースと優先度"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"データソースと優先度"</string>
<string name="set_units_title" msgid="2657822539603758029">"ユニットを設定"</string>
<string name="recent_access_header" msgid="7623497371790225888">"最近のアクセス"</string>
<string name="no_recent_access" msgid="4724297929902441784">"ヘルスコネクトに最近アクセスしたアプリはありません"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"ソースと優先度の仕組み"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"アプリを追加"</string>
<string name="edit_data_sources" msgid="79641360876849547">"アプリのソースを編集する"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"デバイスのデフォルト"</string>
<string name="app_data_title" msgid="6499967982291000837">"アプリデータ"</string>
<string name="no_data_footer" msgid="4777297654713673100">"ヘルスコネクトにアクセスできるアプリのデータがここに表示されます"</string>
<string name="date_picker_day" msgid="3076687507968958991">"日"</string>
diff --git a/apk/res/values-ka/strings.xml b/apk/res/values-ka/strings.xml
index 81e50c90..a0f6aa8b 100644
--- a/apk/res/values-ka/strings.xml
+++ b/apk/res/values-ka/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"არცერთი"</string>
<string name="entry_details_title" msgid="590184849040247850">"შესვლის დეტალები"</string>
<string name="backup_title" msgid="211503191266235085">"სარეზერვო ასლის შექმნა"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"მონაცემთა წყაროები და პრიორიტეტი"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"მონაცემთა წყაროები და პრიორიტეტები"</string>
<string name="set_units_title" msgid="2657822539603758029">"ერთეულების დაყენება"</string>
<string name="recent_access_header" msgid="7623497371790225888">"ბოლო წვდომა"</string>
<string name="no_recent_access" msgid="4724297929902441784">"არცერთ აპს არ ჰქონდა წვდომა Health Connect-თან"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"როგორ მუშაობს წყაროები და პრიორიტეტები"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"დაამატეთ აპი"</string>
<string name="edit_data_sources" msgid="79641360876849547">"აპების წყაროების რედაქტირება"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"მოწყობილობის ნაგულისხმევი"</string>
<string name="app_data_title" msgid="6499967982291000837">"აპის მონაცემები"</string>
<string name="no_data_footer" msgid="4777297654713673100">"მონაცემები იმ აპებიდან, რომლებსაც Health Connect-ზე აქვს წვდომა, აქ გამოჩნდება"</string>
<string name="date_picker_day" msgid="3076687507968958991">"დღე"</string>
diff --git a/apk/res/values-kk/strings.xml b/apk/res/values-kk/strings.xml
index fadaa574..e23ad055 100644
--- a/apk/res/values-kk/strings.xml
+++ b/apk/res/values-kk/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Ешқандай"</string>
<string name="entry_details_title" msgid="590184849040247850">"Енгізілген дерек мәліметтері"</string>
<string name="backup_title" msgid="211503191266235085">"Сақтық көшірме жасау"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Дереккөздер және басымдық"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Дереккөздер және басымдық"</string>
<string name="set_units_title" msgid="2657822539603758029">"Бірліктер орнату"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Соңғы пайдаланғандар"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Жақында ешбір қолданба Health Connect-ті пайдаланбады"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Дереккөздер мен басымдық туралы ақпарат"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Қолданба қосу"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Қолданба дереккөздерін өзгерту"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Құрылғының әдепкі қолданбасы"</string>
<string name="app_data_title" msgid="6499967982291000837">"Қолданба деректері"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Health Connect-ке кіру рұқсаты бар қолданбалардан алынған дерек осы жерде көрсетіледі."</string>
<string name="date_picker_day" msgid="3076687507968958991">"Күн"</string>
diff --git a/apk/res/values-km/strings.xml b/apk/res/values-km/strings.xml
index 4f325da2..d1663f4c 100644
--- a/apk/res/values-km/strings.xml
+++ b/apk/res/values-km/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"គ្មាន"</string>
<string name="entry_details_title" msgid="590184849040247850">"ព័ត៌មានលម្អិតអំពីធាតុ"</string>
<string name="backup_title" msgid="211503191266235085">"បម្រុង​ទុក"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"ប្រភពទិន្នន័យ និងអាទិភាព"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"ប្រភពទិន្នន័យ និងអាទិភាព"</string>
<string name="set_units_title" msgid="2657822539603758029">"កំណត់ឯកតា"</string>
<string name="recent_access_header" msgid="7623497371790225888">"ការចូលប្រើថ្មីៗ"</string>
<string name="no_recent_access" msgid="4724297929902441784">"មិនមានកម្មវិធីណាមួយបានចូលប្រើ Health Connect ថ្មីៗនេះទេ"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"របៀបដែល​ប្រភព និងការកំណត់អាទិភាព​ដំណើរការ"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"បញ្ចូល​កម្មវិធី"</string>
<string name="edit_data_sources" msgid="79641360876849547">"កែប្រភពកម្មវិធី"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"លំនាំដើម​របស់ឧបករណ៍"</string>
<string name="app_data_title" msgid="6499967982291000837">"ទិន្នន័យកម្មវិធី"</string>
<string name="no_data_footer" msgid="4777297654713673100">"ទិន្នន័យពីកម្មវិធីដែលមានសិទ្ធិចូលប្រើ Health Connect នឹងបង្ហាញនៅទីនេះ"</string>
<string name="date_picker_day" msgid="3076687507968958991">"ថ្ងៃ"</string>
diff --git a/apk/res/values-kn/strings.xml b/apk/res/values-kn/strings.xml
index faf64259..1fd17cda 100644
--- a/apk/res/values-kn/strings.xml
+++ b/apk/res/values-kn/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"ಯಾವುದೂ ಅಲ್ಲ"</string>
<string name="entry_details_title" msgid="590184849040247850">"ಪ್ರವೇಶದ ವಿವರಗಳು"</string>
<string name="backup_title" msgid="211503191266235085">"ಬ್ಯಾಕಪ್"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"ಡೇಟಾ ಮೂಲಗಳು ಮತ್ತು ಆದ್ಯತೆ"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"ಡೇಟಾ ಮೂಲಗಳು ಮತ್ತು ಆದ್ಯತೆ"</string>
<string name="set_units_title" msgid="2657822539603758029">"ಯೂನಿಟ್‌ಗಳನ್ನು ಸೆಟ್ ಮಾಡಿ"</string>
<string name="recent_access_header" msgid="7623497371790225888">"ಇತ್ತೀಚಿನ ಆ್ಯಕ್ಸೆಸ್"</string>
<string name="no_recent_access" msgid="4724297929902441784">"ಯಾವುದೇ ಆ್ಯಪ್‌ಗಳು ಇತ್ತೀಚೆಗೆ Health Connect ಅನ್ನು ಆ್ಯಕ್ಸೆಸ್ ಮಾಡಿಲ್ಲ"</string>
@@ -704,7 +704,7 @@
<string name="deletion_started_done_button" msgid="1232018689825054257">"ಮುಗಿದಿದೆ"</string>
<string name="priority_dialog_title" msgid="7360654442596118085">"ಆ್ಯಪ್ ಆದ್ಯತೆಯನ್ನು ಸೆಟ್ ಮಾಡಿ"</string>
<string name="priority_dialog_message" msgid="6971250365335018184">"ಒಂದಕ್ಕಿಂತ ಹೆಚ್ಚಿನ ಆ್ಯಪ್ <xliff:g id="DATA_TYPE">%s</xliff:g> ಡೇಟಾವನ್ನು ಸೇರಿಸಿದರೆ, ಈ ಪಟ್ಟಿಯ ಮೇಲ್ಭಾಗದಲ್ಲಿರುವ ಆ್ಯಪ್‌ಗೆ Health Connect ಆದ್ಯತೆ ನೀಡುತ್ತದೆ. ಆ್ಯಪ್‌ಗಳನ್ನು ಮರುಕ್ರಮಗೊಳಿಸಲು ಅವುಗಳನ್ನು ಡ್ರ್ಯಾಗ್ ಮಾಡಿ."</string>
- <string name="priority_dialog_positive_button" msgid="2503570694373675092">"ಉಳಿಸಿ"</string>
+ <string name="priority_dialog_positive_button" msgid="2503570694373675092">"ಸೇವ್ ಮಾಡಿ"</string>
<string name="action_drag_label_move_up" msgid="4221641798253080966">"ಮೇಲಕ್ಕೆ ಸರಿಸಿ"</string>
<string name="action_drag_label_move_down" msgid="3448000958912947588">"ಕೆಳಕ್ಕೆ ಸರಿಸಿ"</string>
<string name="action_drag_label_move_top" msgid="5114033774108663548">"ಮೇಲಕ್ಕೆ ಸರಿಸಿ"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"ಮೂಲಗಳು ಮತ್ತು ಆದ್ಯತೆ ನೀಡುವಿಕೆ ಹೇಗೆ ಕಾರ್ಯನಿರ್ವಹಿಸುತ್ತವೆ"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"ಆ್ಯಪ್ ಒಂದನ್ನು ಸೇರಿಸಿ"</string>
<string name="edit_data_sources" msgid="79641360876849547">"ಆ್ಯಪ್ ಮೂಲಗಳನ್ನು ಎಡಿಟ್ ಮಾಡಿ"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"ಸಾಧನದ ಡೀಫಾಲ್ಟ್"</string>
<string name="app_data_title" msgid="6499967982291000837">"ಆ್ಯಪ್ ಡೇಟಾ"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Health Connect ಗೆ ಆ್ಯಕ್ಸೆಸ್ ಹೊಂದಿರುವ ಆ್ಯಪ್‌ಗಳ ಡೇಟಾವನ್ನು ಇಲ್ಲಿ ತೋರಿಸಲಾಗುತ್ತದೆ"</string>
<string name="date_picker_day" msgid="3076687507968958991">"ದಿನ"</string>
diff --git a/apk/res/values-ko/strings.xml b/apk/res/values-ko/strings.xml
index 3d53352e..84ed660b 100644
--- a/apk/res/values-ko/strings.xml
+++ b/apk/res/values-ko/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"없음"</string>
<string name="entry_details_title" msgid="590184849040247850">"항목 세부정보"</string>
<string name="backup_title" msgid="211503191266235085">"백업"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"데이터 소스 및 우선순위"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"데이터 소스 및 우선순위"</string>
<string name="set_units_title" msgid="2657822539603758029">"단위 설정"</string>
<string name="recent_access_header" msgid="7623497371790225888">"최근 데이터에 액세스한 앱"</string>
<string name="no_recent_access" msgid="4724297929902441784">"최근에 헬스 커넥트를 사용한 앱이 없음"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"소스 및 우선순위 지정의 작동 방식"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"앱 추가하기"</string>
<string name="edit_data_sources" msgid="79641360876849547">"앱 소스 수정"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"기기 기본값"</string>
<string name="app_data_title" msgid="6499967982291000837">"앱 데이터"</string>
<string name="no_data_footer" msgid="4777297654713673100">"헬스 커넥트에 액세스할 수 있는 앱의 데이터가 여기에 표시됩니다."</string>
<string name="date_picker_day" msgid="3076687507968958991">"일"</string>
diff --git a/apk/res/values-ky/strings.xml b/apk/res/values-ky/strings.xml
index 77e6b597..dde2c444 100644
--- a/apk/res/values-ky/strings.xml
+++ b/apk/res/values-ky/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Жок"</string>
<string name="entry_details_title" msgid="590184849040247850">"Киргизилген маалыматтын чоо-жайы"</string>
<string name="backup_title" msgid="211503191266235085">"Камдык көчүрмөнү сактоо"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Маалымат булактары жана маанилүүлүгү"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Маалымат булактары жана маанилүүлүгү"</string>
<string name="set_units_title" msgid="2657822539603758029">"Бирдиктерди тууралоо"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Соңку колдонмолор"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Health Connect\'ти бир да колдонмо пайдаланган жок"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Булактар жана маанилүүлүк кандайча иштейт"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Колдонмо кошуу"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Колдонмо булактарын түзөтүү"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Түзмөктүн демейки параметри"</string>
<string name="app_data_title" msgid="6499967982291000837">"Колдонмонун дайындары"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Колдонмолордогу Health Connect кызматына байланыштуу нерселер ушул жерде көрүнөт"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Күн"</string>
diff --git a/apk/res/values-lo/strings.xml b/apk/res/values-lo/strings.xml
index 9a859d30..b02202c3 100644
--- a/apk/res/values-lo/strings.xml
+++ b/apk/res/values-lo/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"ບໍ່ມີ"</string>
<string name="entry_details_title" msgid="590184849040247850">"ລາຍລະອຽດລາຍການ"</string>
<string name="backup_title" msgid="211503191266235085">"ສຳຮອງຂໍ້ມູນ"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"ແຫຼ່ງຂໍ້ມູນ ແລະ ຄວາມສຳຄັນ"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"ແຫຼ່ງຂໍ້ມູນ ແລະ ລຳດັບຄວາມສຳຄັນ"</string>
<string name="set_units_title" msgid="2657822539603758029">"ຕັ້ງຫົວໜ່ວຍ"</string>
<string name="recent_access_header" msgid="7623497371790225888">"ການເຂົ້າເຖິງຫຼ້າສຸດ"</string>
<string name="no_recent_access" msgid="4724297929902441784">"ບໍ່ມີແອັບໃດເຂົ້າເຖິງ Health Connect ເມື່ອບໍ່ດົນມານີ້"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"ແຫຼ່ງຂໍ້ມູນ ແລະ ການຈັດຄວາມສຳຄັນເຮັດວຽກແນວໃດ"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"ເພີ່ມແອັບ"</string>
<string name="edit_data_sources" msgid="79641360876849547">"ແກ້ໄຂແຫຼ່ງທີ່ມາຂອງແອັບ"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"ຄ່າເລີ່ມຕົ້ນອຸປະກອນ"</string>
<string name="app_data_title" msgid="6499967982291000837">"ຂໍ້ມູນແອັບ"</string>
<string name="no_data_footer" msgid="4777297654713673100">"ຂໍ້ມູນຈາກແອັບທີ່ມີສິດເຂົ້າເຖິງ Health Connect ຈະສະແດງຢູ່ບ່ອນນີ້"</string>
<string name="date_picker_day" msgid="3076687507968958991">"ມື້"</string>
diff --git a/apk/res/values-lt/strings.xml b/apk/res/values-lt/strings.xml
index 2c9776e7..3e1d1770 100644
--- a/apk/res/values-lt/strings.xml
+++ b/apk/res/values-lt/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Nėra"</string>
<string name="entry_details_title" msgid="590184849040247850">"Išsami įvedimo informacija"</string>
<string name="backup_title" msgid="211503191266235085">"Atsarginė kopija"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Duomenų šaltiniai ir prioritetas"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Duomenų šaltiniai ir prioritetas"</string>
<string name="set_units_title" msgid="2657822539603758029">"Žr. vienetus"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Naujausia prieiga"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Nėra neseniai „Health Connect“ pasiekusių programų"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Kaip veikia šaltiniai ir prioritetų nustatymas"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Programos pridėjimas"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Redaguoti programų šaltinius"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Numatytasis įrenginio nustatymas"</string>
<string name="app_data_title" msgid="6499967982291000837">"Programos duomenys"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Čia bus rodomi programų, turinčių prieigą prie „Health Connect“, duomenys"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Diena"</string>
diff --git a/apk/res/values-lv/strings.xml b/apk/res/values-lv/strings.xml
index 887808e3..a310af9e 100644
--- a/apk/res/values-lv/strings.xml
+++ b/apk/res/values-lv/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Nav"</string>
<string name="entry_details_title" msgid="590184849040247850">"Detalizēta informācija par ierakstu"</string>
<string name="backup_title" msgid="211503191266235085">"Dublēšana"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Datu avoti un prioritāte"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Datu avoti un prioritāte"</string>
<string name="set_units_title" msgid="2657822539603758029">"Iestatīt vienības"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Nesena piekļuve"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Neviena lietotne pēdējā laikā nav piekļuvusi platformai Health Connect"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Avotu un prioritātes noteikšanas principi"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Pievienot lietotni"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Rediģēt lietotņu avotus"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Ierīces noklusējuma lietotne"</string>
<string name="app_data_title" msgid="6499967982291000837">"Lietotnes dati"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Šeit būs redzami dati no lietotnēm, kurām ir piekļuve platformai Health Connect."</string>
<string name="date_picker_day" msgid="3076687507968958991">"Diena"</string>
diff --git a/apk/res/values-mk/strings.xml b/apk/res/values-mk/strings.xml
index 30e6a2ba..f0a5285a 100644
--- a/apk/res/values-mk/strings.xml
+++ b/apk/res/values-mk/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Нема"</string>
<string name="entry_details_title" msgid="590184849040247850">"Детали на записот"</string>
<string name="backup_title" msgid="211503191266235085">"Бекап"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Извори и приоритет на податоци"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Приоритет и извори на податоци"</string>
<string name="set_units_title" msgid="2657822539603758029">"Поставете единици"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Најнов пристап"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Нема апликации што неодамна пристапиле до Health Connect"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Како функционираат изворите и приоритизацијата"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Додајте апликација"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Изменување на изворите на апликации"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Стандардни поставки за уредот"</string>
<string name="app_data_title" msgid="6499967982291000837">"Податоци од апликација"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Податоците од апликациите со пристап до Health Connect ќе се прикажат тука"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Ден"</string>
diff --git a/apk/res/values-ml/strings.xml b/apk/res/values-ml/strings.xml
index f6886d76..f14e98c5 100644
--- a/apk/res/values-ml/strings.xml
+++ b/apk/res/values-ml/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"ഒന്നുമില്ല"</string>
<string name="entry_details_title" msgid="590184849040247850">"എൻട്രിയുടെ വിശദാംശങ്ങൾ"</string>
<string name="backup_title" msgid="211503191266235085">"ബാക്കപ്പ് ചെയ്യുക"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"ഡാറ്റാ ഉറവിടങ്ങളും മുൻഗണനയും"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"ഡാറ്റ ഉറവിടങ്ങളും മുൻഗണനയും"</string>
<string name="set_units_title" msgid="2657822539603758029">"യൂണിറ്റുകൾ സജ്ജീകരിക്കുക"</string>
<string name="recent_access_header" msgid="7623497371790225888">"അടുത്തിടെയുള്ള ആക്‌സസ്"</string>
<string name="no_recent_access" msgid="4724297929902441784">"അടുത്തിടെ Health Connect ആക്‌സസ് ചെയ്ത ആപ്പുകളൊന്നുമില്ല"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"ഉറവിടങ്ങളും മുൻഗണനകളും എങ്ങനെ പ്രവർത്തിക്കുന്നു"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"ആപ്പ് ചേർക്കുക"</string>
<string name="edit_data_sources" msgid="79641360876849547">"ആപ്പ് ഉറവിടങ്ങൾ എഡിറ്റ് ചെയ്യുക"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"ഉപകരണത്തിൽ ഡിഫോൾട്ടായുള്ളത്"</string>
<string name="app_data_title" msgid="6499967982291000837">"ആപ്പ് ഡാറ്റ"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Health Connect-ലേക്ക് ആക്‌സസ് ഉള്ള ആപ്പുകളിൽ നിന്നുള്ള ഡാറ്റ ഇവിടെ കാണിക്കും"</string>
<string name="date_picker_day" msgid="3076687507968958991">"ദിവസം"</string>
diff --git a/apk/res/values-mn/strings.xml b/apk/res/values-mn/strings.xml
index 6f77bbbf..f6b01b27 100644
--- a/apk/res/values-mn/strings.xml
+++ b/apk/res/values-mn/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Байхгүй"</string>
<string name="entry_details_title" msgid="590184849040247850">"Орсон мэдээллийн дэлгэрэнгүй"</string>
<string name="backup_title" msgid="211503191266235085">"Нөөцлөх"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Дата сурвалж болон чухалчлал"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Дата сурвалж болон чухалчлал"</string>
<string name="set_units_title" msgid="2657822539603758029">"Нэгжүүд тохируулах"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Саяхны хандалт"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Сүүлийн үед ямар ч апп Health Connect-д хандаагүй байна"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Эх сурвалж болон чухалчлал хэрхэн ажилладаг вэ?"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Апп нэмэх"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Аппын эх сурвалжуудыг засах"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Төхөөрөмжийн өгөгдмөл"</string>
<string name="app_data_title" msgid="6499967982291000837">"Aппын өгөгдөл"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Health Connect-д хандах эрхтэй аппуудын өгөгдлийг энд харуулна"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Өдөр"</string>
diff --git a/apk/res/values-mr/strings.xml b/apk/res/values-mr/strings.xml
index 689f5cd3..ad0b2476 100644
--- a/apk/res/values-mr/strings.xml
+++ b/apk/res/values-mr/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"काहीही नाही"</string>
<string name="entry_details_title" msgid="590184849040247850">"एंट्रीचे तपशील"</string>
<string name="backup_title" msgid="211503191266235085">"बॅकअप"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"डेटा स्रोत &amp; प्राधान्य"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"डेटा स्रोत आणि प्राधान्य"</string>
<string name="set_units_title" msgid="2657822539603758029">"युनिट सेट करा"</string>
<string name="recent_access_header" msgid="7623497371790225888">"अलीकडील अ‍ॅक्सेस"</string>
<string name="no_recent_access" msgid="4724297929902441784">"अलीकडे कोणत्याही अ‍ॅप्सनी Health Connect अ‍ॅक्सेस केले नाही"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"स्रोत &amp; प्राधान्य देणे कसे काम करते"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"एखादे ॲप जोडा"</string>
<string name="edit_data_sources" msgid="79641360876849547">"अ‍ॅप स्रोत संपादित करा"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"डिव्हाइस डीफॉल्ट"</string>
<string name="app_data_title" msgid="6499967982291000837">"अ‍ॅप डेटा"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Health Connect चा अ‍ॅक्सेस असलेल्या ॲप्सचा डेटा येथे दर्शवला जाईल"</string>
<string name="date_picker_day" msgid="3076687507968958991">"दिवस"</string>
diff --git a/apk/res/values-ms/strings.xml b/apk/res/values-ms/strings.xml
index 1a25b38a..61011786 100644
--- a/apk/res/values-ms/strings.xml
+++ b/apk/res/values-ms/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Tiada"</string>
<string name="entry_details_title" msgid="590184849040247850">"Butiran kemasukan"</string>
<string name="backup_title" msgid="211503191266235085">"Sandaran"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Sumber &amp; keutamaan data"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Sumber dan keutamaan data"</string>
<string name="set_units_title" msgid="2657822539603758029">"Tetapkan unit"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Akses terbaharu"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Tiada apl yang mengakses Health Connect baru-baru ini"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Cara sumber &amp; keutamaan berfungsi"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Tambahkan apl"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Edit sumber apl"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Lalai peranti"</string>
<string name="app_data_title" msgid="6499967982291000837">"Data apl"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Data daripada apl dengan akses kepada Health Connect akan dipaparkan di sini"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Hari"</string>
diff --git a/apk/res/values-my/strings.xml b/apk/res/values-my/strings.xml
index be02b49f..a3768708 100644
--- a/apk/res/values-my/strings.xml
+++ b/apk/res/values-my/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"မရှိ"</string>
<string name="entry_details_title" msgid="590184849040247850">"ထည့်သွင်းမှု အသေးစိတ်"</string>
<string name="backup_title" msgid="211503191266235085">"အရန်သိမ်းရန်"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"ဒေတာရင်းမြစ်နှင့် ဦးစားပေး"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"ဒေတာရင်းမြစ်များနှင့် ဦးစားပေး"</string>
<string name="set_units_title" msgid="2657822539603758029">"ယူနစ်သတ်မှတ်ရန်"</string>
<string name="recent_access_header" msgid="7623497371790225888">"မကြာသေးမီက အသုံးပြုမှု"</string>
<string name="no_recent_access" msgid="4724297929902441784">"မည်သည်အက်ပ်မျှ Health Connect ကို မကြာသေးမီကသုံးမထားပါ"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"ရင်းမြစ်များနှင့် ဦးစားပေးခြင်း အလုပ်လုပ်ပုံ"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"အက်ပ် ထည့်ရန်"</string>
<string name="edit_data_sources" msgid="79641360876849547">"အက်ပ်ရင်းမြစ်များ တည်းဖြတ်ရန်"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"စက်ပစ္စည်းမူရင်း"</string>
<string name="app_data_title" msgid="6499967982291000837">"အက်ပ်ဒေတာ"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Health Connect သုံးခွင့်ရှိသော အက်ပ်များမှ ဒေတာကို ဤနေရာတွင် ပြပါမည်"</string>
<string name="date_picker_day" msgid="3076687507968958991">"ရက်"</string>
diff --git a/apk/res/values-nb/strings.xml b/apk/res/values-nb/strings.xml
index 6da6cca3..a0a2f160 100644
--- a/apk/res/values-nb/strings.xml
+++ b/apk/res/values-nb/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Ingen"</string>
<string name="entry_details_title" msgid="590184849040247850">"Oppføringsdetaljer"</string>
<string name="backup_title" msgid="211503191266235085">"Sikkerhetskopiering"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Datakilder og prioritet"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Datakilder og prioritet"</string>
<string name="set_units_title" msgid="2657822539603758029">"Angi enheter"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Nylig tilgang"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Ingen apper har nylig brukt Health Connect"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Hvordan kilder og prioritering fungerer"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Legg til en app"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Endre appkilder"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Standard for enheten"</string>
<string name="app_data_title" msgid="6499967982291000837">"Appdata"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Data fra apper med tilgang til Health Connect vises her"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Dag"</string>
diff --git a/apk/res/values-ne/strings.xml b/apk/res/values-ne/strings.xml
index dd0e98ae..fb339510 100644
--- a/apk/res/values-ne/strings.xml
+++ b/apk/res/values-ne/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"कुनै पनि एप कनेक्ट गरिएको छैन"</string>
<string name="entry_details_title" msgid="590184849040247850">"इन्ट्रीसम्बन्धी विवरणहरू"</string>
<string name="backup_title" msgid="211503191266235085">"ब्याकअप गर्नुहोस्"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"डेटाका स्रोत तथा प्राथमिकता"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"जानकारीका स्रोत तथा प्राथमिकता"</string>
<string name="set_units_title" msgid="2657822539603758029">"एकाइ सेट गर्नुहोस्"</string>
<string name="recent_access_header" msgid="7623497371790225888">"हालसालै हेर्ने र प्रयोग गर्ने एप"</string>
<string name="no_recent_access" msgid="4724297929902441784">"कुनै पनि एपले हालसालै Health Connect प्रयोग गरेको छैन"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"स्रोत तथा प्राथमिकताले काम गर्ने तरिका"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"कुनै एप हाल्नुहोस्"</string>
<string name="edit_data_sources" msgid="79641360876849547">"एपका स्रोत सम्पादन गर्नुहोस्"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"डिफल्ट डिभाइस"</string>
<string name="app_data_title" msgid="6499967982291000837">"एपसम्बन्धी डेटा"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Health Connect एक्सेस गर्न सक्ने एपहरूको डेटा यहाँ देखिने छ"</string>
<string name="date_picker_day" msgid="3076687507968958991">"दिन"</string>
diff --git a/apk/res/values-nl/strings.xml b/apk/res/values-nl/strings.xml
index 8b539b27..388d1a8c 100644
--- a/apk/res/values-nl/strings.xml
+++ b/apk/res/values-nl/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Geen"</string>
<string name="entry_details_title" msgid="590184849040247850">"Invoerdetails"</string>
<string name="backup_title" msgid="211503191266235085">"Back-up"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Gegevensbronnen en prioriteit"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Gegevensbronnen en prioriteit"</string>
<string name="set_units_title" msgid="2657822539603758029">"Eenheden instellen"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Recente toegang"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Er zijn geen apps die recent toegang tot Health Connect hebben gehad"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Hoe bronnen en prioritering werken"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Een app toevoegen"</string>
<string name="edit_data_sources" msgid="79641360876849547">"App-bronnen bewerken"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Apparaatstandaard"</string>
<string name="app_data_title" msgid="6499967982291000837">"App-gegevens"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Gegevens van apps met toegang tot Health Connect worden hier getoond"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Dag"</string>
diff --git a/apk/res/values-or/strings.xml b/apk/res/values-or/strings.xml
index 67818ccf..d6fcc13b 100644
--- a/apk/res/values-or/strings.xml
+++ b/apk/res/values-or/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"କିଛି ନାହିଁ"</string>
<string name="entry_details_title" msgid="590184849040247850">"ଏଣ୍ଟ୍ରି ବିବରଣୀ"</string>
<string name="backup_title" msgid="211503191266235085">"ବେକଅପ ନିଅନ୍ତୁ"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"ଡାଟା ସୋର୍ସ ଏବଂ ପ୍ରାଥମିକତା"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"ଡାଟା ସୋର୍ସ ଏବଂ ପ୍ରାଥମିକତା"</string>
<string name="set_units_title" msgid="2657822539603758029">"ୟୁନିଟ ସେଟ କରନ୍ତୁ"</string>
<string name="recent_access_header" msgid="7623497371790225888">"ବର୍ତ୍ତମାନର ଆକ୍ସେସ"</string>
<string name="no_recent_access" msgid="4724297929902441784">"କୌଣସି ଆପ୍ସ ବର୍ତ୍ତମାନ Health Connectକୁ ଆକ୍ସେସ କରିନାହିଁ"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"ସୋର୍ସ ଏବଂ ପ୍ରାଥମିକତା କିପରି କାର୍ଯ୍ୟ କରେ"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"ଏକ ଆପ ଯୋଗ କରନ୍ତୁ"</string>
<string name="edit_data_sources" msgid="79641360876849547">"ଆପ ସୋର୍ସଗୁଡ଼ିକୁ ଏଡିଟ କରନ୍ତୁ"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"ଡିଭାଇସ୍ ଡିଫଲ୍ଟ"</string>
<string name="app_data_title" msgid="6499967982291000837">"ଆପ ଡାଟା"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Health Connectକୁ ଆକ୍ସେସ ଥିବା ଆପ୍ସରୁ ଡାଟା ଏଠାରେ ଦେଖାଯିବ"</string>
<string name="date_picker_day" msgid="3076687507968958991">"ଦିନ"</string>
diff --git a/apk/res/values-pa/strings.xml b/apk/res/values-pa/strings.xml
index a290fd60..b37f4fdc 100644
--- a/apk/res/values-pa/strings.xml
+++ b/apk/res/values-pa/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"ਕੋਈ ਨਹੀਂ"</string>
<string name="entry_details_title" msgid="590184849040247850">"ਐਂਟਰੀ ਸੰਬੰਧੀ ਵੇਰਵੇ"</string>
<string name="backup_title" msgid="211503191266235085">"ਬੈਕਅੱਪ"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"ਡਾਟਾ ਸਰੋਤ ਅਤੇ ਤਰਜੀਹ"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"ਡਾਟਾ ਸਰੋਤ ਅਤੇ ਤਰਜੀਹ"</string>
<string name="set_units_title" msgid="2657822539603758029">"ਇਕਾਈਆਂ ਸੈੱਟ ਕਰੋ"</string>
<string name="recent_access_header" msgid="7623497371790225888">"ਹਾਲੀਆ ਪਹੁੰਚ"</string>
<string name="no_recent_access" msgid="4724297929902441784">"ਹਾਲ ਹੀ ਵਿੱਚ ਕਿਸੇ ਵੀ ਐਪ ਨੇ Health Connect ਤੱਕ ਪਹੁੰਚ ਨਹੀਂ ਕੀਤੀ"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"ਸਰੋਤ ਅਤੇ ਤਰਜੀਹੀਕਰਨ ਕਿਵੇਂ ਕੰਮ ਕਰਦੇ ਹਨ"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"ਕੋਈ ਐਪ ਸ਼ਾਮਲ ਕਰੋ"</string>
<string name="edit_data_sources" msgid="79641360876849547">"ਐਪ ਸਰੋਤਾਂ ਦਾ ਸੰਪਾਦਨ ਕਰੋ"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"ਪੂਰਵ-ਨਿਰਧਾਰਿਤ ਡੀਵਾਈਸ"</string>
<string name="app_data_title" msgid="6499967982291000837">"ਐਪ ਡਾਟਾ"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Health Connect ਤੱਕ ਪਹੁੰਚ ਵਾਲੀਆਂ ਐਪਾਂ ਦਾ ਡਾਟਾ ਇੱਥੇ ਦਿਖੇਗਾ"</string>
<string name="date_picker_day" msgid="3076687507968958991">"ਦਿਨ"</string>
diff --git a/apk/res/values-pl/strings.xml b/apk/res/values-pl/strings.xml
index ba5c0a09..8b4eb92a 100644
--- a/apk/res/values-pl/strings.xml
+++ b/apk/res/values-pl/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Brak"</string>
<string name="entry_details_title" msgid="590184849040247850">"Szczegóły wpisu"</string>
<string name="backup_title" msgid="211503191266235085">"Kopia zapasowa"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Źródła danych i priorytety"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Źródła danych i priorytet"</string>
<string name="set_units_title" msgid="2657822539603758029">"Jednostki zestawu"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Ostatni dostęp"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Żadne aplikacje nie uzyskiwały ostatnio dostępu do Health Connect"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Jak działają priorytety źródeł"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Dodaj aplikację"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Edytuj źródła aplikacji"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Ustawienie domyślne urządzenia"</string>
<string name="app_data_title" msgid="6499967982291000837">"Dane aplikacji"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Tutaj pojawią się dane z aplikacji z dostępem do Health Connect"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Dzień"</string>
diff --git a/apk/res/values-pt-rPT/strings.xml b/apk/res/values-pt-rPT/strings.xml
index 172192cc..94ad3881 100644
--- a/apk/res/values-pt-rPT/strings.xml
+++ b/apk/res/values-pt-rPT/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Nenhuma"</string>
<string name="entry_details_title" msgid="590184849040247850">"Detalhes da entrada"</string>
<string name="backup_title" msgid="211503191266235085">"Cópia de segurança"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Origens de dados e prioridade"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Origens de dados e prioridade"</string>
<string name="set_units_title" msgid="2657822539603758029">"Definir unidades"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Acesso recente"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Nenhuma app acedeu recentemente à Saúde Connect"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Como funcionam as origens e a atribuição de prioridade"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Adicionar app"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Editar origens de apps"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Predefinição do dispositivo"</string>
<string name="app_data_title" msgid="6499967982291000837">"Dados de apps"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Os dados de apps com acesso à Saúde Connect são apresentados aqui"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Dia"</string>
diff --git a/apk/res/values-pt/strings.xml b/apk/res/values-pt/strings.xml
index d3f058e4..4403b81b 100644
--- a/apk/res/values-pt/strings.xml
+++ b/apk/res/values-pt/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Nenhuma"</string>
<string name="entry_details_title" msgid="590184849040247850">"Detalhes da entrada"</string>
<string name="backup_title" msgid="211503191266235085">"Backup"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Origens de dados e prioridade"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Fontes de dados e prioridade"</string>
<string name="set_units_title" msgid="2657822539603758029">"Definir unidades"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Acessos recentes"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Nenhum app acessou a plataforma Conexão Saúde recentemente"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Como origens e priorização funcionam"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Adicionar um app"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Editar fontes de apps"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Padrão do dispositivo"</string>
<string name="app_data_title" msgid="6499967982291000837">"Dados do app"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Os dados de apps com acesso à Conexão Saúde vão aparecer aqui"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Dia"</string>
diff --git a/apk/res/values-ro/strings.xml b/apk/res/values-ro/strings.xml
index c0fa680d..a6a6d22c 100644
--- a/apk/res/values-ro/strings.xml
+++ b/apk/res/values-ro/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Niciuna"</string>
<string name="entry_details_title" msgid="590184849040247850">"Detalii despre intrare"</string>
<string name="backup_title" msgid="211503191266235085">"Backup"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Surse de date și prioritate"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Surse de date și prioritate"</string>
<string name="set_units_title" msgid="2657822539603758029">"Setează unitățile"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Acces recent"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Nicio aplicație nu a accesat recent Health Connect"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Cum funcționează sursele și stabilirea priorității"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Adaugă o aplicație"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Editează sursele de aplicații"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Prestabilită pentru dispozitiv"</string>
<string name="app_data_title" msgid="6499967982291000837">"Datele aplicației"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Datele din aplicațiile cu acces la Health Connect se vor afișa aici"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Zi"</string>
diff --git a/apk/res/values-ru/strings.xml b/apk/res/values-ru/strings.xml
index 78214d6e..52a25245 100644
--- a/apk/res/values-ru/strings.xml
+++ b/apk/res/values-ru/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Нет приложений"</string>
<string name="entry_details_title" msgid="590184849040247850">"Сведения о записи"</string>
<string name="backup_title" msgid="211503191266235085">"Резервное копирование"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Источники данных и приоритет"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Источники данных и приоритет"</string>
<string name="set_units_title" msgid="2657822539603758029">"Настроить единицы измерения"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Доступ за последнее время"</string>
<string name="no_recent_access" msgid="4724297929902441784">"В последнее время приложения не получали доступ к сервису \"Здоровье и спорт\""</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Сведения об источниках данных и приоритете"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Добавить приложение"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Изменить список приложений"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Приложение по умолчанию"</string>
<string name="app_data_title" msgid="6499967982291000837">"Данные приложения"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Здесь будут появляться данные из приложений, у которых есть доступ к приложению \"Здоровье и спорт\"."</string>
<string name="date_picker_day" msgid="3076687507968958991">"День"</string>
diff --git a/apk/res/values-si/strings.xml b/apk/res/values-si/strings.xml
index bf0429b8..55833d66 100644
--- a/apk/res/values-si/strings.xml
+++ b/apk/res/values-si/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"කිසිවක් නැත"</string>
<string name="entry_details_title" msgid="590184849040247850">"ඇතුළත් කිරීමේ විස්තර"</string>
<string name="backup_title" msgid="211503191266235085">"උපස්ථය"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"දත්ත මූලාශ්‍ර සහ ප්‍රමුඛතාවය"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"දත්ත මූලාශ්‍ර සහ ප්‍රමුඛතාවය"</string>
<string name="set_units_title" msgid="2657822539603758029">"ඒකක සකසන්න"</string>
<string name="recent_access_header" msgid="7623497371790225888">"මෑත ප්‍රවේශය"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Health Connect වෙත මෑතදී ප්‍රවේශ වූ යෙදුම් කිසිවක් නැත"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"මූලාශ්‍ර සහ ප්‍රමුඛතා දීම ක්‍රියා කරන ආකාරය"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"යෙදුමක් එක් කරන්න"</string>
<string name="edit_data_sources" msgid="79641360876849547">"යෙදුම් මූලාශ්‍ර සංස්කරණය කරන්න"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"උපාංගයේ පෙරනිමිය"</string>
<string name="app_data_title" msgid="6499967982291000837">"යෙදුම් දත්ත"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Health Connect වෙත ප්‍රවේශය ඇති යෙදුම් වෙතින් දත්ත මෙහි පෙන්වනු ඇත"</string>
<string name="date_picker_day" msgid="3076687507968958991">"දිනය"</string>
diff --git a/apk/res/values-sk/strings.xml b/apk/res/values-sk/strings.xml
index 0d853fcc..5df59a08 100644
--- a/apk/res/values-sk/strings.xml
+++ b/apk/res/values-sk/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Žiadne"</string>
<string name="entry_details_title" msgid="590184849040247850">"Podrobnosti o zázname"</string>
<string name="backup_title" msgid="211503191266235085">"Záloha"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Zdroje údajov a priorita"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Zdroje údajov a priorita"</string>
<string name="set_units_title" msgid="2657822539603758029">"Nastaviť jednotky"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Nedávny prístup"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Dáta o zdraví v poslednom čase nepoužili žiadne aplikácie"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Ako fungujú zdroje a priorizácia"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Pridať aplikáciu"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Upraviť zdroje aplikácií"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Predvolená aplikácia zariadenia"</string>
<string name="app_data_title" msgid="6499967982291000837">"Dáta aplikácie"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Údaje z aplikácií s prístupom k Dátam o zdraví sa budú zobrazovať tu"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Deň"</string>
diff --git a/apk/res/values-sl/strings.xml b/apk/res/values-sl/strings.xml
index b5541bc8..2a97c482 100644
--- a/apk/res/values-sl/strings.xml
+++ b/apk/res/values-sl/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Brez"</string>
<string name="entry_details_title" msgid="590184849040247850">"Podrobnosti vnosa"</string>
<string name="backup_title" msgid="211503191266235085">"Varnostno kopiranje"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Podatkovni viri in prednost"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Podatkovni viri in prednost"</string>
<string name="set_units_title" msgid="2657822539603758029">"Nastavitev enot"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Nedavni dostop"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Nobena aplikacija ni nedavno dostopala do storitve Health Connect."</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Kako delujejo viri in dodeljevanje prednosti"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Dodajanje aplikacije"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Urejanje virov aplikacij"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Privzeta aplikacija v napravi"</string>
<string name="app_data_title" msgid="6499967982291000837">"Podatki aplikacije"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Podatki iz aplikacij z dostopom do aplikacije Health Connect bodo prikazani tukaj"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Dan"</string>
diff --git a/apk/res/values-sq/strings.xml b/apk/res/values-sq/strings.xml
index 1ff1aa24..55f62b2e 100644
--- a/apk/res/values-sq/strings.xml
+++ b/apk/res/values-sq/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Asnjë"</string>
<string name="entry_details_title" msgid="590184849040247850">"Detajet e hyrjes"</string>
<string name="backup_title" msgid="211503191266235085">"Rezervimi"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Burimet e të dhënave dhe përparësia"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Burimet e të dhënave dhe përparësia"</string>
<string name="set_units_title" msgid="2657822539603758029">"Cakto njësitë"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Qasja së fundi"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Asnjë aplikacion nuk ka pasur qasje së fundi te Health Connect"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Si funksionojnë burimet dhe dhënia e përparësisë"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Shto një aplikacion"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Modifiko burimet e aplikacioneve"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Parazgjedhja e pajisjes"</string>
<string name="app_data_title" msgid="6499967982291000837">"Të dhënat e aplikacionit"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Të dhënat nga aplikacionet që kanë qasje te Health Connect do të shfaqen këtu"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Dita"</string>
diff --git a/apk/res/values-sr/strings.xml b/apk/res/values-sr/strings.xml
index 0fa793f1..6434c73d 100644
--- a/apk/res/values-sr/strings.xml
+++ b/apk/res/values-sr/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Ништа"</string>
<string name="entry_details_title" msgid="590184849040247850">"Детаљи уноса"</string>
<string name="backup_title" msgid="211503191266235085">"Резервна копија"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Извори података и приоритет"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Извори података и приоритет"</string>
<string name="set_units_title" msgid="2657822539603758029">"Подеси јединице"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Недавни приступ"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Ниједна апликација није недавно приступила Повезивању здравља"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Како извори и одређивање приоритета раде"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Додај апликацију"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Измени изворе апликација"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Подразумевано за уређај"</string>
<string name="app_data_title" msgid="6499967982291000837">"Подаци апликација"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Подаци из апликација са приступом Повезивању здравља приказаће се овде"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Дан"</string>
diff --git a/apk/res/values-sv/strings.xml b/apk/res/values-sv/strings.xml
index 93e77b06..ed0718db 100644
--- a/apk/res/values-sv/strings.xml
+++ b/apk/res/values-sv/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Inga"</string>
<string name="entry_details_title" msgid="590184849040247850">"Information om dataposten"</string>
<string name="backup_title" msgid="211503191266235085">"Säkerhetskopiering"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Datakällor och -prioritet"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Datakällor och prioritet"</string>
<string name="set_units_title" msgid="2657822539603758029">"Ställ in enheter"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Senaste åtkomst"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Inga appar har nyligen kommit åt Health Connect"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Så fungerar källor och prioritering"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Lägg till en app"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Redigera appkällor"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Enhetens standardinställning"</string>
<string name="app_data_title" msgid="6499967982291000837">"Appdata"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Data från appar med åtkomst till Health Connect visas här"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Dag"</string>
diff --git a/apk/res/values-sw/strings.xml b/apk/res/values-sw/strings.xml
index 4eace2a3..81532f8e 100644
--- a/apk/res/values-sw/strings.xml
+++ b/apk/res/values-sw/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Hamna"</string>
<string name="entry_details_title" msgid="590184849040247850">"Maelezo ya kipengee"</string>
<string name="backup_title" msgid="211503191266235085">"Hifadhi nakala"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Vyanzo vya data na kipaumbele"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Vyanzo vya data na kipaumbele"</string>
<string name="set_units_title" msgid="2657822539603758029">"Weka vipimo"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Ufikiaji wa hivi karibuni"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Hakuna programu zilizofikia Health Connect hivi karibuni"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Jinsi vyanzo na uwekaji kipaumbele vinavyofanya kazi"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Ongeza programu"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Badilisha vyanzo vya programu"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Hali chaguomsingi ya kifaa"</string>
<string name="app_data_title" msgid="6499967982291000837">"Data ya programu"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Data kutoka kwa programu zinazofikia Health Connect itaonekana hapa"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Siku"</string>
diff --git a/apk/res/values-ta/strings.xml b/apk/res/values-ta/strings.xml
index 2178f2cf..2b3298a9 100644
--- a/apk/res/values-ta/strings.xml
+++ b/apk/res/values-ta/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"ஏதுமில்லை"</string>
<string name="entry_details_title" msgid="590184849040247850">"உள்ளீட்டு விவரங்கள்"</string>
<string name="backup_title" msgid="211503191266235085">"காப்புப் பிரதி எடு"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"தரவு மூலங்களும் முன்னுரிமையும்"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"தரவு மூலங்களும் முன்னுரிமையும்"</string>
<string name="set_units_title" msgid="2657822539603758029">"அலகுகளை அமை"</string>
<string name="recent_access_header" msgid="7623497371790225888">"சமீபத்திய அணுகல்"</string>
<string name="no_recent_access" msgid="4724297929902441784">"சமீபத்தில் Health Connect ஆப்ஸை எந்த ஆப்ஸும் அணுகவில்லை"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"மூலங்களும் முன்னுரிமையும் செயல்படும் விதம்"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"ஆப்ஸைச் சேர்த்தல்"</string>
<string name="edit_data_sources" msgid="79641360876849547">"ஆப்ஸ் ஆதாரங்களை மாற்றுதல்"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"சாதனத்தின் இயல்புநிலை"</string>
<string name="app_data_title" msgid="6499967982291000837">"ஆப்ஸ் தரவு"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Health Connectடுக்கான அணுகல் உள்ள ஆப்ஸின் தரவு இங்கே காட்டப்படும்"</string>
<string name="date_picker_day" msgid="3076687507968958991">"நாள்"</string>
diff --git a/apk/res/values-te/strings.xml b/apk/res/values-te/strings.xml
index d458d77f..334d6966 100644
--- a/apk/res/values-te/strings.xml
+++ b/apk/res/values-te/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"ఏదీ లేదు"</string>
<string name="entry_details_title" msgid="590184849040247850">"ఎంట్రీ వివరాలు"</string>
<string name="backup_title" msgid="211503191266235085">"బ్యాకప్"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"డేటా సోర్స్‌లు &amp; ప్రాధాన్యత"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"డేటా సోర్స్‌లు, ప్రాధాన్యత"</string>
<string name="set_units_title" msgid="2657822539603758029">"యూనిట్‌లను సెట్ చేయండి"</string>
<string name="recent_access_header" msgid="7623497371790225888">"ఇటీవలి యాక్సెస్"</string>
<string name="no_recent_access" msgid="4724297929902441784">"యాప్‌లు ఏవీ ఇటీవల Health Connectను యాక్సెస్ చేయలేదు"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"సోర్స్‌లు &amp; ప్రాధాన్యత ఎలా పని చేస్తాయి"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"యాప్‌ను జోడించండి"</string>
<string name="edit_data_sources" msgid="79641360876849547">"యాప్ సోర్స్‌లను ఎడిట్ చేయండి"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"పరికరంలో ఆటోమేటిక్‌గా సెట్ చేయబడి ఉన్న యాప్"</string>
<string name="app_data_title" msgid="6499967982291000837">"యాప్ డేటా"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Health Connectకు యాక్సెస్ ఉన్న యాప్‌ల డేటా ఇక్కడ చూపబడుతుంది"</string>
<string name="date_picker_day" msgid="3076687507968958991">"రోజు"</string>
diff --git a/apk/res/values-th/strings.xml b/apk/res/values-th/strings.xml
index 8546ad26..81ba9455 100644
--- a/apk/res/values-th/strings.xml
+++ b/apk/res/values-th/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"ไม่มี"</string>
<string name="entry_details_title" msgid="590184849040247850">"รายละเอียดรายการ"</string>
<string name="backup_title" msgid="211503191266235085">"การสำรองข้อมูล"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"แหล่งข้อมูลและลำดับความสำคัญ"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"แหล่งข้อมูลและลำดับความสำคัญ"</string>
<string name="set_units_title" msgid="2657822539603758029">"ตั้งค่าหน่วย"</string>
<string name="recent_access_header" msgid="7623497371790225888">"การเข้าถึงล่าสุด"</string>
<string name="no_recent_access" msgid="4724297929902441784">"ไม่มีแอปที่เข้าถึง Health Connect เมื่อเร็วๆ นี้"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"แหล่งที่มาและลำดับความสำคัญทำงานอย่างไร"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"เพิ่มแอป"</string>
<string name="edit_data_sources" msgid="79641360876849547">"แก้ไขแหล่งที่มาของแอป"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"ค่าเริ่มต้นของอุปกรณ์"</string>
<string name="app_data_title" msgid="6499967982291000837">"ข้อมูลแอป"</string>
<string name="no_data_footer" msgid="4777297654713673100">"ข้อมูลจากแอปที่มีสิทธิ์เข้าถึง Health Connect จะแสดงที่นี่"</string>
<string name="date_picker_day" msgid="3076687507968958991">"วัน"</string>
diff --git a/apk/res/values-tl/strings.xml b/apk/res/values-tl/strings.xml
index c079047e..fe3365c8 100644
--- a/apk/res/values-tl/strings.xml
+++ b/apk/res/values-tl/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Wala"</string>
<string name="entry_details_title" msgid="590184849040247850">"Mga detalye ng entry"</string>
<string name="backup_title" msgid="211503191266235085">"Backup"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Mga data source at priyoridad"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Mga data source at priyoridad"</string>
<string name="set_units_title" msgid="2657822539603758029">"Itakda ang mga unit"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Kamakailang na-access"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Walang app na kamakailangang nag-access ng Health Connect"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Paano gumagana ang mga source at pagbibigay ng priyoridad"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Magdagdag ng app"</string>
<string name="edit_data_sources" msgid="79641360876849547">"I-edit ang mga source ng app"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Default ng device"</string>
<string name="app_data_title" msgid="6499967982291000837">"Data ng app"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Lalabas ang data mula sa mga app na may access sa Health Connect dito"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Araw"</string>
diff --git a/apk/res/values-tr/strings.xml b/apk/res/values-tr/strings.xml
index 81a8a6c3..20f61eab 100644
--- a/apk/res/values-tr/strings.xml
+++ b/apk/res/values-tr/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Yok"</string>
<string name="entry_details_title" msgid="590184849040247850">"Giriş ayrıntıları"</string>
<string name="backup_title" msgid="211503191266235085">"Yedekle"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Veri kaynakları ve önceliği"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Veri kaynakları ve öncelik"</string>
<string name="set_units_title" msgid="2657822539603758029">"Birimleri ayarla"</string>
<string name="recent_access_header" msgid="7623497371790225888">"En son erişim"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Yakın zamanda hiçbir uygulama Health Connect\'e erişmedi"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Kaynaklar ve önceliklendirme nasıl çalışır?"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Uygulama ekle"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Uygulama kaynaklarını düzenleyin"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Cihaz varsayılanı"</string>
<string name="app_data_title" msgid="6499967982291000837">"Uygulama verileri"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Health Connect\'e erişimi olan uygulamaların verileri burada görünecek"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Gün"</string>
diff --git a/apk/res/values-uk/strings.xml b/apk/res/values-uk/strings.xml
index ea9f422e..c9242156 100644
--- a/apk/res/values-uk/strings.xml
+++ b/apk/res/values-uk/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Немає"</string>
<string name="entry_details_title" msgid="590184849040247850">"Деталі запису"</string>
<string name="backup_title" msgid="211503191266235085">"Резервне копіювання"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Джерела даних і пріоритет"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Джерела даних і пріоритет"</string>
<string name="set_units_title" msgid="2657822539603758029">"Вибрати одиниці вимірювання"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Нещодавній доступ"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Немає додатків, які нещодавно використовували Health Connect"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Як працюють джерела й пріоритети"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Вибрати додаток"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Змінити додатки-джерела"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"За умовчанням для пристрою"</string>
<string name="app_data_title" msgid="6499967982291000837">"Дані додатка"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Тут відображатимуться дані з додатків, які мають доступ до Health Connect"</string>
<string name="date_picker_day" msgid="3076687507968958991">"День"</string>
diff --git a/apk/res/values-ur/strings.xml b/apk/res/values-ur/strings.xml
index ef5596f2..fc635663 100644
--- a/apk/res/values-ur/strings.xml
+++ b/apk/res/values-ur/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"کوئی نہیں"</string>
<string name="entry_details_title" msgid="590184849040247850">"اندراج کی تفصیلات"</string>
<string name="backup_title" msgid="211503191266235085">"بیک اپ لیں"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"ڈیٹا کے ذرائع اور ترجیح"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"ڈیٹا کا ماخذات اور ترجیح"</string>
<string name="set_units_title" msgid="2657822539603758029">"یونٹس سیٹ کریں"</string>
<string name="recent_access_header" msgid="7623497371790225888">"حالیہ رسائی"</string>
<string name="no_recent_access" msgid="4724297929902441784">"فی الحال کسی بھی ایپ نے Health Connect تک رسائی حاصل نہیں کی"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"ذرائع اور ترجیح کیسے کام کرتے ہیں"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"ایپ شامل کریں"</string>
<string name="edit_data_sources" msgid="79641360876849547">"ایپ کے مآخذ میں ترمیم کریں"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"آلہ ڈیفالٹ"</string>
<string name="app_data_title" msgid="6499967982291000837">"ایپ کا ڈیٹا"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Health Connect تک رسائی والی ایپس کا ڈیٹا یہاں ظاہر ہوگا"</string>
<string name="date_picker_day" msgid="3076687507968958991">"دن"</string>
diff --git a/apk/res/values-uz/strings.xml b/apk/res/values-uz/strings.xml
index d4c310e3..d331b7a6 100644
--- a/apk/res/values-uz/strings.xml
+++ b/apk/res/values-uz/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Hech biri"</string>
<string name="entry_details_title" msgid="590184849040247850">"Qayd tafsilotlari"</string>
<string name="backup_title" msgid="211503191266235085">"Zaxiralash"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Axborot manbalari va tartiblanishi"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Axborot manbalari va tartiblanishi"</string>
<string name="set_units_title" msgid="2657822539603758029">"Birliklarni belgilash"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Oxirgi kirish"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Yaqin orada hech qanday ilova Health Connect ilovasiga ulanmagan"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Manbalar qanday tartiblanishi haqida"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Ilova kiriting"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Ilova manbalarini tahrirlash"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Qurilma standarti"</string>
<string name="app_data_title" msgid="6499967982291000837">"Ilovaga tegishli maʼlumotlar"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Health Connect xizmatiga ruxsati bor ilovalar axboroti shu yerda chiqadi"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Kun"</string>
diff --git a/apk/res/values-vi/strings.xml b/apk/res/values-vi/strings.xml
index 52ae93cb..0997d09c 100644
--- a/apk/res/values-vi/strings.xml
+++ b/apk/res/values-vi/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Không có"</string>
<string name="entry_details_title" msgid="590184849040247850">"Thông tin nhập"</string>
<string name="backup_title" msgid="211503191266235085">"Sao lưu"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Nguồn dữ liệu và mức độ ưu tiên"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Nguồn dữ liệu và mức độ ưu tiên"</string>
<string name="set_units_title" msgid="2657822539603758029">"Cài đặt đơn vị"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Lần truy cập gần đây"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Không có ứng dụng nào truy cập vào Health Connect gần đây"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Cách hoạt động của nguồn dữ liệu và mức độ ưu tiên"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Thêm ứng dụng"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Chỉnh sửa nguồn ứng dụng"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Theo chế độ cài đặt mặc định của thiết bị"</string>
<string name="app_data_title" msgid="6499967982291000837">"Dữ liệu ứng dụng"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Dữ liệu của các ứng dụng có quyền truy cập vào Health Connect sẽ xuất hiện ở đây"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Ngày"</string>
diff --git a/apk/res/values-zh-rCN/strings.xml b/apk/res/values-zh-rCN/strings.xml
index 37366a6b..b78b8720 100644
--- a/apk/res/values-zh-rCN/strings.xml
+++ b/apk/res/values-zh-rCN/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"无"</string>
<string name="entry_details_title" msgid="590184849040247850">"详细数据"</string>
<string name="backup_title" msgid="211503191266235085">"备份"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"数据源和优先级"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"数据源和优先级"</string>
<string name="set_units_title" msgid="2657822539603758029">"设置单位"</string>
<string name="recent_access_header" msgid="7623497371790225888">"近期数据访问情况"</string>
<string name="no_recent_access" msgid="4724297929902441784">"最近没有任何应用访问 Health Connect"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"来源和优先级的运作方式"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"添加应用"</string>
<string name="edit_data_sources" msgid="79641360876849547">"编辑应用来源"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"设备默认设置"</string>
<string name="app_data_title" msgid="6499967982291000837">"应用数据"</string>
<string name="no_data_footer" msgid="4777297654713673100">"具有 Health Connect 访问权的应用中的数据将显示在此处"</string>
<string name="date_picker_day" msgid="3076687507968958991">"日"</string>
diff --git a/apk/res/values-zh-rHK/strings.xml b/apk/res/values-zh-rHK/strings.xml
index 6f98a402..0fabfcdb 100644
--- a/apk/res/values-zh-rHK/strings.xml
+++ b/apk/res/values-zh-rHK/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"無"</string>
<string name="entry_details_title" msgid="590184849040247850">"項目詳情"</string>
<string name="backup_title" msgid="211503191266235085">"備份"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"資料來源和優先次序"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"資料來源和優先次序"</string>
<string name="set_units_title" msgid="2657822539603758029">"設定單位"</string>
<string name="recent_access_header" msgid="7623497371790225888">"近期存取記錄"</string>
<string name="no_recent_access" msgid="4724297929902441784">"最近沒有應用程式存取 Health Connect"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"來源和優先次序的運作方式"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"新增應用程式"</string>
<string name="edit_data_sources" msgid="79641360876849547">"編輯應用程式來源清單"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"裝置預設設定"</string>
<string name="app_data_title" msgid="6499967982291000837">"應用程式資料"</string>
<string name="no_data_footer" msgid="4777297654713673100">"有 Health Connect 存取權的應用程式所提供的資料會在這裡顯示"</string>
<string name="date_picker_day" msgid="3076687507968958991">"日"</string>
diff --git a/apk/res/values-zh-rTW/strings.xml b/apk/res/values-zh-rTW/strings.xml
index 635dc081..4ba3892d 100644
--- a/apk/res/values-zh-rTW/strings.xml
+++ b/apk/res/values-zh-rTW/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"無"</string>
<string name="entry_details_title" msgid="590184849040247850">"項目詳細資料"</string>
<string name="backup_title" msgid="211503191266235085">"備份"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"資料來源與優先順序"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"資料來源與優先順序"</string>
<string name="set_units_title" msgid="2657822539603758029">"設定單位"</string>
<string name="recent_access_header" msgid="7623497371790225888">"近期存取記錄"</string>
<string name="no_recent_access" msgid="4724297929902441784">"最近沒有任何應用程式存取 Health Connect"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"來源與優先順序的運作方式"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"新增應用程式"</string>
<string name="edit_data_sources" msgid="79641360876849547">"編輯應用程式來源清單"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"裝置預設設定"</string>
<string name="app_data_title" msgid="6499967982291000837">"應用程式資料"</string>
<string name="no_data_footer" msgid="4777297654713673100">"如果資料是來自有權存取 Health Connect 的應用程式,就會顯示在這裡"</string>
<string name="date_picker_day" msgid="3076687507968958991">"一天"</string>
diff --git a/apk/res/values-zu/strings.xml b/apk/res/values-zu/strings.xml
index bd9831fc..6b009199 100644
--- a/apk/res/values-zu/strings.xml
+++ b/apk/res/values-zu/strings.xml
@@ -33,7 +33,7 @@
<string name="connected_apps_button_no_permissions_subtitle" msgid="1651994862419752908">"Lutho"</string>
<string name="entry_details_title" msgid="590184849040247850">"Imininingwane yokungena"</string>
<string name="backup_title" msgid="211503191266235085">"Yenza isipele"</string>
- <string name="data_sources_and_priority_title" msgid="3871795785418187899">"Imithombo yedatha nokubalulekile"</string>
+ <string name="data_sources_and_priority_title" msgid="2360222350913604558">"Imithombo yedatha kanye nokubalulekile"</string>
<string name="set_units_title" msgid="2657822539603758029">"Setha amayunithi"</string>
<string name="recent_access_header" msgid="7623497371790225888">"Ukufinyelela kwakamuva"</string>
<string name="no_recent_access" msgid="4724297929902441784">"Awekho ama-app asanda kufinyelela ku-Health Connect"</string>
@@ -790,8 +790,7 @@
<string name="data_sources_help_link" msgid="7740264923634947915">"Indlela imithombo nomsebenzi wokwenza kube okubalulekile"</string>
<string name="data_sources_add_app" msgid="319926596123692514">"Faka i-app"</string>
<string name="edit_data_sources" msgid="79641360876849547">"Hlela izinsiza ze-app"</string>
- <!-- no translation found for default_app_summary (6183876151011837062) -->
- <skip />
+ <string name="default_app_summary" msgid="6183876151011837062">"Idivayisi ezenzakalelayo"</string>
<string name="app_data_title" msgid="6499967982291000837">"Idatha ye-app"</string>
<string name="no_data_footer" msgid="4777297654713673100">"Idatha ekuma-app akwazi ukufinyelela ku-Health Connect izovela lapha"</string>
<string name="date_picker_day" msgid="3076687507968958991">"Usuku"</string>
diff --git a/apk/res/values/strings.xml b/apk/res/values/strings.xml
index 38c40352..cb1893d8 100644
--- a/apk/res/values/strings.xml
+++ b/apk/res/values/strings.xml
@@ -35,7 +35,7 @@
<!-- region Manage data -->
<string name="backup_title" description="Label for a button that takes the user to backup settings. [CHAR_LIMIT=40]">Backup</string>
- <string name="data_sources_and_priority_title" description="Label for a button that takes the user to app priority settings. [CHAR_LIMIT=40]">Data sources &amp; priority</string>
+ <string name="data_sources_and_priority_title" description="Label for a button that takes the user to app priority settings. [CHAR_LIMIT=40]">Data sources and priority</string>
<string name="set_units_title" description="Label for a button that takes the user to unit settings. [CHAR_LIMIT=40]">Set units</string>
<!-- endregion -->
diff --git a/apk/res/xml/data_sources_and_priority_screen.xml b/apk/res/xml/data_sources_and_priority_screen.xml
index 4767019c..ee70416c 100644
--- a/apk/res/xml/data_sources_and_priority_screen.xml
+++ b/apk/res/xml/data_sources_and_priority_screen.xml
@@ -23,9 +23,11 @@
<PreferenceCategory
android:key="app_sources_group"
android:title="@string/app_sources_header"
- android:order="2"/>
+ android:order="2"
+ app:isPreferenceVisible="false"/>
<com.android.settingslib.widget.FooterPreference
android:key="data_sources_footer"
- android:title="@string/data_sources_footer"/>
+ android:title="@string/data_sources_footer"
+ app:isPreferenceVisible="false"/>
</PreferenceScreen> \ No newline at end of file
diff --git a/apk/res/xml/empty_preference_screen.xml b/apk/res/xml/empty_preference_screen.xml
new file mode 100644
index 00000000..624ed13a
--- /dev/null
+++ b/apk/res/xml/empty_preference_screen.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+
+</PreferenceScreen> \ No newline at end of file
diff --git a/apk/src/com/android/healthconnect/controller/MainActivity.kt b/apk/src/com/android/healthconnect/controller/MainActivity.kt
index 4b13be60..93414f76 100644
--- a/apk/src/com/android/healthconnect/controller/MainActivity.kt
+++ b/apk/src/com/android/healthconnect/controller/MainActivity.kt
@@ -15,28 +15,37 @@
*/
package com.android.healthconnect.controller
+import android.app.Activity
import android.os.Bundle
+import androidx.activity.result.contract.ActivityResultContracts.*
import androidx.activity.viewModels
import androidx.navigation.findNavController
import com.android.healthconnect.controller.migration.MigrationActivity.Companion.maybeRedirectToMigrationActivity
import com.android.healthconnect.controller.migration.MigrationViewModel
import com.android.healthconnect.controller.navigation.DestinationChangedListener
-import com.android.healthconnect.controller.onboarding.OnboardingActivity.Companion.maybeRedirectToOnboardingActivity
-import com.android.healthconnect.controller.onboarding.OnboardingActivityContract
-import com.android.healthconnect.controller.onboarding.OnboardingActivityContract.Companion.INTENT_RESULT_CANCELLED
+import com.android.healthconnect.controller.onboarding.OnboardingActivity
+import com.android.healthconnect.controller.onboarding.OnboardingActivity.Companion.shouldRedirectToOnboardingActivity
import com.android.healthconnect.controller.utils.activity.EmbeddingUtils.maybeRedirectIntoTwoPaneSettings
import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity
import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.coroutines.runBlocking
import javax.inject.Inject
/** Entry point activity for Health Connect. */
@AndroidEntryPoint(CollapsingToolbarBaseActivity::class)
class MainActivity : Hilt_MainActivity() {
+
@Inject lateinit var logger: HealthConnectLogger
+
private val migrationViewModel: MigrationViewModel by viewModels()
+ private val openOnboardingActivity =
+ registerForActivityResult(StartActivityForResult()) { result ->
+ if (result.resultCode == Activity.RESULT_CANCELED) {
+ finish()
+ }
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
@@ -47,8 +56,8 @@ class MainActivity : Hilt_MainActivity() {
return
}
- if (maybeRedirectToOnboardingActivity(this) && savedInstanceState == null) {
- openOnboardingActivity.launch(1)
+ if (savedInstanceState == null && shouldRedirectToOnboardingActivity(this)) {
+ openOnboardingActivity.launch(OnboardingActivity.createIntent(this))
}
val currentMigrationState = migrationViewModel.getCurrentMigrationUiState()
@@ -78,7 +87,6 @@ class MainActivity : Hilt_MainActivity() {
if (!navController.popBackStack()) {
finish()
}
-
}
override fun onNavigateUp(): Boolean {
@@ -90,13 +98,6 @@ class MainActivity : Hilt_MainActivity() {
return true
}
- val openOnboardingActivity =
- registerForActivityResult(OnboardingActivityContract()) { result ->
- if (result == INTENT_RESULT_CANCELLED) {
- finish()
- }
- }
-
// TODO (b/270864219): implement interaction logging for the menu button
// override fun onMenuOpened(featureId: Int, menu: Menu?): Boolean {
// logger.logInteraction(ElementName.TOOLBAR_SETTINGS_BUTTON)
diff --git a/apk/src/com/android/healthconnect/controller/data/DataManagementActivity.kt b/apk/src/com/android/healthconnect/controller/data/DataManagementActivity.kt
index 60316557..36166c69 100644
--- a/apk/src/com/android/healthconnect/controller/data/DataManagementActivity.kt
+++ b/apk/src/com/android/healthconnect/controller/data/DataManagementActivity.kt
@@ -19,32 +19,39 @@
package com.android.healthconnect.controller.data
import android.os.Bundle
+import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.activity.viewModels
import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment
import com.android.healthconnect.controller.R
-import com.android.healthconnect.controller.migration.MigrationActivity
import com.android.healthconnect.controller.migration.MigrationActivity.Companion.maybeRedirectToMigrationActivity
import com.android.healthconnect.controller.migration.MigrationActivity.Companion.maybeShowWhatsNewDialog
import com.android.healthconnect.controller.migration.MigrationViewModel
import com.android.healthconnect.controller.migration.api.MigrationState
import com.android.healthconnect.controller.navigation.DestinationChangedListener
-import com.android.healthconnect.controller.onboarding.OnboardingActivity.Companion.maybeRedirectToOnboardingActivity
-import com.android.healthconnect.controller.onboarding.OnboardingActivityContract
+import com.android.healthconnect.controller.onboarding.OnboardingActivity
+import com.android.healthconnect.controller.onboarding.OnboardingActivity.Companion.shouldRedirectToOnboardingActivity
import com.android.healthconnect.controller.utils.FeatureUtils
import com.android.healthconnect.controller.utils.activity.EmbeddingUtils.maybeRedirectIntoTwoPaneSettings
import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
-import kotlinx.coroutines.runBlocking
/** Entry point activity for Health Connect Data Management controllers. */
@AndroidEntryPoint(CollapsingToolbarBaseActivity::class)
class DataManagementActivity : Hilt_DataManagementActivity() {
+
@Inject lateinit var featureUtils: FeatureUtils
private val migrationViewModel: MigrationViewModel by viewModels()
+ private val openOnboardingActivity =
+ registerForActivityResult(StartActivityForResult()) { result ->
+ if (result.resultCode == RESULT_CANCELED) {
+ finish()
+ }
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_data_management)
@@ -56,8 +63,8 @@ class DataManagementActivity : Hilt_DataManagementActivity() {
return
}
- if (maybeRedirectToOnboardingActivity(this) && savedInstanceState == null) {
- openOnboardingActivity.launch(1)
+ if (savedInstanceState == null && shouldRedirectToOnboardingActivity(this)) {
+ openOnboardingActivity.launch(OnboardingActivity.createIntent(this))
}
val currentMigrationState = migrationViewModel.getCurrentMigrationUiState()
@@ -119,11 +126,4 @@ class DataManagementActivity : Hilt_DataManagementActivity() {
}
return true
}
-
- val openOnboardingActivity =
- registerForActivityResult(OnboardingActivityContract()) { result ->
- if (result == OnboardingActivityContract.INTENT_RESULT_CANCELLED) {
- finish()
- }
- }
}
diff --git a/apk/src/com/android/healthconnect/controller/data/access/AccessViewModel.kt b/apk/src/com/android/healthconnect/controller/data/access/AccessViewModel.kt
index 0cd86d83..5701c68d 100644
--- a/apk/src/com/android/healthconnect/controller/data/access/AccessViewModel.kt
+++ b/apk/src/com/android/healthconnect/controller/data/access/AccessViewModel.kt
@@ -35,7 +35,7 @@ import kotlinx.coroutines.launch
* [com.android.healthconnect.controller.dataaccess.HealthDataAccessFragment].
*/
@HiltViewModel
-class AccessViewModel @Inject constructor(private val loadAccessUseCase: LoadAccessUseCase) :
+class AccessViewModel @Inject constructor(private val loadAccessUseCase: ILoadAccessUseCase) :
ViewModel() {
private val _appMetadataMap = MutableLiveData<AccessScreenState>()
diff --git a/apk/src/com/android/healthconnect/controller/data/access/LoadAccessUseCase.kt b/apk/src/com/android/healthconnect/controller/data/access/LoadAccessUseCase.kt
index f83ba9c5..4c679dbd 100644
--- a/apk/src/com/android/healthconnect/controller/data/access/LoadAccessUseCase.kt
+++ b/apk/src/com/android/healthconnect/controller/data/access/LoadAccessUseCase.kt
@@ -15,7 +15,7 @@
*/
package com.android.healthconnect.controller.data.access
-import com.android.healthconnect.controller.permissions.api.GetGrantedHealthPermissionsUseCase
+import com.android.healthconnect.controller.permissions.api.IGetGrantedHealthPermissionsUseCase
import com.android.healthconnect.controller.permissions.data.HealthPermission
import com.android.healthconnect.controller.permissions.data.HealthPermissionType
import com.android.healthconnect.controller.permissions.data.PermissionsAccessType
@@ -33,14 +33,14 @@ import kotlinx.coroutines.withContext
class LoadAccessUseCase
@Inject
constructor(
- private val loadPermissionTypeContributorAppsUseCase: LoadPermissionTypeContributorAppsUseCase,
- private val loadGrantedHealthPermissionsUseCase: GetGrantedHealthPermissionsUseCase,
+ private val loadPermissionTypeContributorAppsUseCase: ILoadPermissionTypeContributorAppsUseCase,
+ private val loadGrantedHealthPermissionsUseCase: IGetGrantedHealthPermissionsUseCase,
private val healthPermissionReader: HealthPermissionReader,
private val appInfoReader: AppInfoReader,
@IoDispatcher private val dispatcher: CoroutineDispatcher
-) {
+) : ILoadAccessUseCase {
/** Returns a map of [AppAccessState] to apps. */
- suspend operator fun invoke(
+ override suspend operator fun invoke(
permissionType: HealthPermissionType
): UseCaseResults<Map<AppAccessState, List<AppMetadata>>> =
withContext(dispatcher) {
@@ -97,3 +97,9 @@ constructor(
return packageNames.sortedBy { appMetadata -> appMetadata.appName }
}
}
+
+interface ILoadAccessUseCase {
+ suspend fun invoke(
+ permissionType: HealthPermissionType
+ ): UseCaseResults<Map<AppAccessState, List<AppMetadata>>>
+}
diff --git a/apk/src/com/android/healthconnect/controller/data/access/LoadPermissionTypeContributorAppsUseCase.kt b/apk/src/com/android/healthconnect/controller/data/access/LoadPermissionTypeContributorAppsUseCase.kt
index 36134810..6ae1266b 100644
--- a/apk/src/com/android/healthconnect/controller/data/access/LoadPermissionTypeContributorAppsUseCase.kt
+++ b/apk/src/com/android/healthconnect/controller/data/access/LoadPermissionTypeContributorAppsUseCase.kt
@@ -37,10 +37,10 @@ constructor(
private val appInfoReader: AppInfoReader,
private val healthConnectManager: HealthConnectManager,
@IoDispatcher private val dispatcher: CoroutineDispatcher
-) {
+) : ILoadPermissionTypeContributorAppsUseCase {
/** Returns a list of [AppMetadata]s that have data in this [HealthPermissionType]. */
- suspend operator fun invoke(permissionType: HealthPermissionType): List<AppMetadata> =
+ override suspend operator fun invoke(permissionType: HealthPermissionType): List<AppMetadata> =
withContext(dispatcher) {
try {
val recordTypeInfoMap: Map<Class<out Record>, RecordTypeInfoResponse> =
@@ -64,3 +64,7 @@ constructor(
}
}
}
+
+interface ILoadPermissionTypeContributorAppsUseCase {
+ suspend fun invoke(permissionType: HealthPermissionType): List<AppMetadata>
+}
diff --git a/apk/src/com/android/healthconnect/controller/data/entries/api/LoadDataEntriesUseCase.kt b/apk/src/com/android/healthconnect/controller/data/entries/api/LoadDataEntriesUseCase.kt
index 475439da..a8b7ebae 100644
--- a/apk/src/com/android/healthconnect/controller/data/entries/api/LoadDataEntriesUseCase.kt
+++ b/apk/src/com/android/healthconnect/controller/data/entries/api/LoadDataEntriesUseCase.kt
@@ -19,7 +19,6 @@ import com.android.healthconnect.controller.data.entries.FormattedEntry
import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod
import com.android.healthconnect.controller.permissions.data.HealthPermissionType
import com.android.healthconnect.controller.service.IoDispatcher
-import com.android.healthconnect.controller.shared.HealthPermissionToDatatypeMapper.getDataTypes
import com.android.healthconnect.controller.shared.usecase.BaseUseCase
import com.android.healthconnect.controller.shared.usecase.UseCaseResults
import java.time.Instant
@@ -37,17 +36,7 @@ constructor(
) : BaseUseCase<LoadDataEntriesInput, List<FormattedEntry>>(dispatcher), ILoadDataEntriesUseCase {
override suspend fun execute(input: LoadDataEntriesInput): List<FormattedEntry> {
- val timeFilterRange =
- loadEntriesHelper.getTimeFilter(
- input.displayedStartTime, input.period, endTimeExclusive = true)
- val dataTypes = getDataTypes(input.permissionType)
-
- val entryRecords =
- dataTypes
- .map { dataType ->
- loadEntriesHelper.readDataType(dataType, timeFilterRange, input.packageName)
- }
- .flatten()
+ val entryRecords = loadEntriesHelper.readRecords(input)
return loadEntriesHelper.maybeAddDateSectionHeaders(
entryRecords, input.period, input.showDataOrigin)
diff --git a/apk/src/com/android/healthconnect/controller/data/entries/api/LoadEntriesHelper.kt b/apk/src/com/android/healthconnect/controller/data/entries/api/LoadEntriesHelper.kt
index c761e3d4..241f350f 100644
--- a/apk/src/com/android/healthconnect/controller/data/entries/api/LoadEntriesHelper.kt
+++ b/apk/src/com/android/healthconnect/controller/data/entries/api/LoadEntriesHelper.kt
@@ -31,6 +31,7 @@ import com.android.healthconnect.controller.data.entries.FormattedEntry
import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod
import com.android.healthconnect.controller.data.entries.datenavigation.toPeriod
import com.android.healthconnect.controller.dataentries.formatters.shared.HealthDataEntryFormatter
+import com.android.healthconnect.controller.shared.HealthPermissionToDatatypeMapper
import com.android.healthconnect.controller.utils.LocalDateTimeFormatter
import com.android.healthconnect.controller.utils.SystemTimeSource
import com.android.healthconnect.controller.utils.TimeSource
@@ -64,6 +65,9 @@ constructor(
private const val TAG = "LoadDataUseCaseHelper"
}
+ /**
+ * Returns a list of records from a data type sorted in descending order of their start time.
+ */
suspend fun readDataType(
data: Class<out Record>,
timeFilterRange: TimeInstantRangeFilter,
@@ -80,6 +84,17 @@ constructor(
return records
}
+ /** Returns a list of records from an input sorted in descending order of their start time. */
+ suspend fun readRecords(input: LoadDataEntriesInput): List<Record> {
+ val timeFilterRange =
+ getTimeFilter(input.displayedStartTime, input.period, endTimeExclusive = true)
+ val dataTypes = HealthPermissionToDatatypeMapper.getDataTypes(input.permissionType)
+
+ return dataTypes
+ .map { dataType -> readDataType(dataType, timeFilterRange, input.packageName) }
+ .flatten()
+ }
+
/**
* If more than one day's data is displayed, inserts a section header for each day: 'Today',
* 'Yesterday', then date format.
@@ -172,6 +187,7 @@ constructor(
period: DateNavigationPeriod,
endTimeExclusive: Boolean
): TimeInstantRangeFilter {
+
val start =
startTime
.atZone(ZoneId.systemDefault())
@@ -182,6 +198,7 @@ constructor(
if (endTimeExclusive) {
end = end.minus(Duration.ofMillis(1))
}
+
return TimeInstantRangeFilter.Builder().setStartTime(start).setEndTime(end).build()
}
diff --git a/apk/src/com/android/healthconnect/controller/data/entries/api/LoadSleepDataUseCase.kt b/apk/src/com/android/healthconnect/controller/data/entries/api/LoadSleepDataUseCase.kt
deleted file mode 100644
index d8f865cf..00000000
--- a/apk/src/com/android/healthconnect/controller/data/entries/api/LoadSleepDataUseCase.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-package com.android.healthconnect.controller.data.entries.api
-
-import android.health.connect.datatypes.Record
-import com.android.healthconnect.controller.service.IoDispatcher
-import com.android.healthconnect.controller.shared.HealthPermissionToDatatypeMapper
-import com.android.healthconnect.controller.shared.usecase.BaseUseCase
-import com.android.healthconnect.controller.shared.usecase.UseCaseResults
-import javax.inject.Inject
-import javax.inject.Singleton
-import kotlinx.coroutines.CoroutineDispatcher
-
-@Singleton
-class LoadSleepDataUseCase
-@Inject
-constructor(
- @IoDispatcher private val dispatcher: CoroutineDispatcher,
- private val loadEntriesHelper: LoadEntriesHelper
-) : BaseUseCase<LoadDataEntriesInput, List<Record>>(dispatcher), ILoadSleepDataUseCase {
-
- /**
- * Used to load the sleep session records. For aggregating sleep we need to know both the start
- * and end times of each session.
- */
- override suspend fun execute(input: LoadDataEntriesInput): List<Record> {
- val timeFilterRange =
- loadEntriesHelper.getTimeFilter(
- input.displayedStartTime, input.period, endTimeExclusive = true)
- val dataTypes = HealthPermissionToDatatypeMapper.getDataTypes(input.permissionType)
-
- return dataTypes
- .map { dataType ->
- loadEntriesHelper.readDataType(dataType, timeFilterRange, input.packageName)
- }
- .flatten()
- }
-}
-
-interface ILoadSleepDataUseCase {
- suspend fun invoke(input: LoadDataEntriesInput): UseCaseResults<List<Record>>
-
- suspend fun execute(input: LoadDataEntriesInput): List<Record>
-}
diff --git a/apk/src/com/android/healthconnect/controller/datasources/AddAnAppFragment.kt b/apk/src/com/android/healthconnect/controller/datasources/AddAnAppFragment.kt
index 9628dc97..ae379db4 100644
--- a/apk/src/com/android/healthconnect/controller/datasources/AddAnAppFragment.kt
+++ b/apk/src/com/android/healthconnect/controller/datasources/AddAnAppFragment.kt
@@ -18,7 +18,6 @@ package com.android.healthconnect.controller.datasources
import android.os.Bundle
import android.view.View
import androidx.fragment.app.activityViewModels
-import androidx.lifecycle.MediatorLiveData
import androidx.navigation.fragment.findNavController
import com.android.healthconnect.controller.R
import com.android.healthconnect.controller.categories.HealthDataCategoriesFragment.Companion.CATEGORY_KEY
@@ -28,6 +27,8 @@ import com.android.healthconnect.controller.shared.HealthDataCategoryInt
import com.android.healthconnect.controller.shared.app.AppMetadata
import com.android.healthconnect.controller.shared.preference.HealthPreference
import com.android.healthconnect.controller.shared.preference.HealthPreferenceFragment
+import com.android.healthconnect.controller.utils.logging.AddAnAppElement
+import com.android.healthconnect.controller.utils.logging.PageName
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint(HealthPreferenceFragment::class)
@@ -38,6 +39,10 @@ class AddAnAppFragment : Hilt_AddAnAppFragment() {
private var currentPriority: List<AppMetadata> = listOf()
+ init {
+ this.setPageName(PageName.ADD_AN_APP_PAGE)
+ }
+
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
super.onCreatePreferences(savedInstanceState, rootKey)
setPreferencesFromResource(R.xml.add_an_app_screen, rootKey)
@@ -58,8 +63,13 @@ class AddAnAppFragment : Hilt_AddAnAppFragment() {
setError(true)
} else if (dataSourcesInfoState.isWithData()) {
setLoading(false)
- val currentPriorityList = (dataSourcesInfoState.priorityListState as PriorityListState.WithData).priorityList
- val potentialAppSources = (dataSourcesInfoState.potentialAppSourcesState as PotentialAppSourcesState.WithData).appSources
+ val currentPriorityList =
+ (dataSourcesInfoState.priorityListState as PriorityListState.WithData)
+ .priorityList
+ val potentialAppSources =
+ (dataSourcesInfoState.potentialAppSourcesState
+ as PotentialAppSourcesState.WithData)
+ .appSources
currentPriorityList.let { currentPriority = it }
updateAppsList(potentialAppSources)
}
@@ -75,15 +85,18 @@ class AddAnAppFragment : Hilt_AddAnAppFragment() {
HealthPreference(requireContext()).also { preference ->
preference.title = appMetadata.appName
preference.icon = appMetadata.icon
+ preference.logName = AddAnAppElement.POTENTIAL_PRIORITY_APP_BUTTON
preference.setOnPreferenceClickListener {
// add this app to the bottom of the priority list
- val newPriority = currentPriority.toMutableList().also {
- it.add(appMetadata) }.toList()
+ val newPriority =
+ currentPriority
+ .toMutableList()
+ .also { it.add(appMetadata) }
+ .toList()
dataSourcesViewModel.updatePriorityList(
newPriority.map { it.packageName }.toList(), category)
- findNavController().navigate(
- R.id.action_addAnAppFragment_to_dataSourcesFragment
- )
+ findNavController()
+ .navigate(R.id.action_addAnAppFragment_to_dataSourcesFragment)
true
}
})
diff --git a/apk/src/com/android/healthconnect/controller/datasources/DataSourcesFragment.kt b/apk/src/com/android/healthconnect/controller/datasources/DataSourcesFragment.kt
index 94eafb5d..45d6dac9 100644
--- a/apk/src/com/android/healthconnect/controller/datasources/DataSourcesFragment.kt
+++ b/apk/src/com/android/healthconnect/controller/datasources/DataSourcesFragment.kt
@@ -41,7 +41,9 @@ import com.android.healthconnect.controller.shared.preference.HealthPreferenceFr
import com.android.healthconnect.controller.utils.AttributeResolver
import com.android.healthconnect.controller.utils.DeviceInfoUtilsImpl
import com.android.healthconnect.controller.utils.TimeSource
+import com.android.healthconnect.controller.utils.logging.DataSourcesElement
import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
+import com.android.healthconnect.controller.utils.logging.PageName
import com.android.healthconnect.controller.utils.setupMenu
import com.android.healthconnect.controller.utils.setupSharedMenu
import com.android.settingslib.widget.FooterPreference
@@ -69,8 +71,7 @@ class DataSourcesFragment :
}
init {
- // TODO (b/292270118) update to correct name
- // this.setPageName(PageName.MANAGE_DATA_PAGE)
+ this.setPageName(PageName.DATA_SOURCES_PAGE)
}
@Inject lateinit var logger: HealthConnectLogger
@@ -116,16 +117,24 @@ class DataSourcesFragment :
dataSourcesCategoriesStrings =
dataSourcesCategories.map { category -> getString(category.uppercaseTitle()) }
- setupSpinnerPreference()
- }
+ if (requireArguments().containsKey(CATEGORY_KEY)) {
+ // Only require this from the HealthPermissionTypes screen
+ // When navigating here from the Manage Data screen we pass Unknown
+ // so that going back and forth to this screen does not restrict users to just one
+ // category
+ val argCategory = requireArguments().getInt(CATEGORY_KEY)
+ if (argCategory != HealthDataCategory.UNKNOWN) {
+ currentCategorySelection = argCategory
+ dataSourcesViewModel.setCurrentSelection(currentCategorySelection)
+ }
+ }
- override fun onResume() {
- super.onResume()
- dataSourcesViewModel.loadData(currentCategorySelection)
+ setupSpinnerPreference()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
+ setLoading(true)
val currentStringSelection = spinnerPreference.selectedItem
currentCategorySelection =
dataSourcesCategories[dataSourcesCategoriesStrings.indexOf(currentStringSelection)]
@@ -154,7 +163,7 @@ class DataSourcesFragment :
if (priorityList.isEmpty() && potentialAppSources.isEmpty()) {
addEmptyState()
} else {
- updateMenu(priorityList.size > 1)
+ updateMenu(priorityList.size > 1 && !dataSourcesViewModel.isEditMode)
updateAppSourcesSection(priorityList, potentialAppSources)
updateDataTotalsSection(cardInfos)
}
@@ -177,6 +186,11 @@ class DataSourcesFragment :
}
}
+ override fun onResume() {
+ super.onResume()
+ dataSourcesViewModel.loadData(currentCategorySelection)
+ }
+
private fun updateMenu(shouldShowEditButton: Boolean) {
if (shouldShowEditButton) {
setupMenu(R.menu.data_sources, viewLifecycleOwner, logger, onEditMenuItemSelected)
@@ -186,6 +200,7 @@ class DataSourcesFragment :
}
private fun editPriorityList() {
+ dataSourcesViewModel.isEditMode = true
updateMenu(shouldShowEditButton = false)
appSourcesPreferenceGroup?.removePreferenceRecursively(ADD_AN_APP_PREFERENCE_KEY)
val appSourcesPreference =
@@ -199,6 +214,7 @@ class DataSourcesFragment :
?.toggleEditMode(false)
updateMenu(dataSourcesViewModel.getEditedPriorityList().size > 1)
updateAddApp(dataSourcesViewModel.getEditedPotentialAppSources().isNotEmpty())
+ dataSourcesViewModel.isEditMode = false
}
/** Updates the priority list preference. */
@@ -218,9 +234,12 @@ class DataSourcesFragment :
dataSourcesViewModel,
currentCategorySelection,
this)
- .also { it.key = APP_SOURCES_PREFERENCE_KEY })
+ .also {
+ it.key = APP_SOURCES_PREFERENCE_KEY
+ it.setEditMode(dataSourcesViewModel.isEditMode)
+ })
- updateAddApp(potentialAppSources.isNotEmpty())
+ updateAddApp(potentialAppSources.isNotEmpty() && !dataSourcesViewModel.isEditMode)
nonEmptyFooterPreference?.isVisible = true
}
@@ -241,6 +260,7 @@ class DataSourcesFragment :
HealthPreference(requireContext()).also {
it.icon = AttributeResolver.getDrawable(requireContext(), R.attr.addIcon)
it.title = getString(R.string.data_sources_add_app)
+ it.logName = DataSourcesElement.ADD_AN_APP_BUTTON
it.key = ADD_AN_APP_PREFERENCE_KEY
it.order = 100 // Arbitrary number to ensure the button is added at the end of the
// priority list
@@ -285,8 +305,8 @@ class DataSourcesFragment :
dataTotalsPreferenceGroup?.isVisible = false
} else {
dataTotalsPreferenceGroup?.isVisible = true
- cardContainerPreference?.setLoading(false)
cardContainerPreference?.setAggregationCardInfo(cardInfos)
+ cardContainerPreference?.setLoading(false)
}
}
}
@@ -354,6 +374,8 @@ class DataSourcesFragment :
position: Int,
id: Long
) {
+ logger.logInteraction(DataSourcesElement.DATA_TYPE_SPINNER)
+
val currentCategory = dataSourcesCategories[position]
currentCategorySelection = dataSourcesCategories[position]
@@ -369,5 +391,6 @@ class DataSourcesFragment :
dataSourcesCategories.indexOf(dataSourcesViewModel.getCurrentSelection()))
preferenceScreen.addPreference(spinnerPreference)
+ logger.logImpression(DataSourcesElement.DATA_TYPE_SPINNER)
}
}
diff --git a/apk/src/com/android/healthconnect/controller/datasources/DataSourcesViewModel.kt b/apk/src/com/android/healthconnect/controller/datasources/DataSourcesViewModel.kt
index 32f19843..0d28ea53 100644
--- a/apk/src/com/android/healthconnect/controller/datasources/DataSourcesViewModel.kt
+++ b/apk/src/com/android/healthconnect/controller/datasources/DataSourcesViewModel.kt
@@ -75,6 +75,8 @@ constructor(
val dataSourcesInfo: LiveData<DataSourcesInfo>
get() = _dataSourcesInfo
+ var isEditMode = false
+
init {
_dataSourcesAndAggregationsInfo.addSource(_currentPriorityList) { priorityListState ->
if (!priorityListState.shouldObserve) {
diff --git a/apk/src/com/android/healthconnect/controller/datasources/api/LoadLastDateWithPriorityDataUseCase.kt b/apk/src/com/android/healthconnect/controller/datasources/api/LoadLastDateWithPriorityDataUseCase.kt
new file mode 100644
index 00000000..64232cb8
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/datasources/api/LoadLastDateWithPriorityDataUseCase.kt
@@ -0,0 +1,145 @@
+/**
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.android.healthconnect.controller.datasources.api
+
+import android.health.connect.HealthConnectManager
+import androidx.core.os.asOutcomeReceiver
+import com.android.healthconnect.controller.data.entries.api.LoadDataEntriesInput
+import com.android.healthconnect.controller.data.entries.api.LoadEntriesHelper
+import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType
+import com.android.healthconnect.controller.permissiontypes.api.ILoadPriorityListUseCase
+import com.android.healthconnect.controller.service.IoDispatcher
+import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.fromHealthPermissionType
+import com.android.healthconnect.controller.shared.HealthPermissionToDatatypeMapper
+import com.android.healthconnect.controller.shared.usecase.UseCaseResults
+import com.android.healthconnect.controller.utils.TimeSource
+import com.android.healthconnect.controller.utils.toInstantAtStartOfDay
+import com.android.healthconnect.controller.utils.toLocalDate
+import com.google.common.collect.Comparators.max
+import java.time.LocalDate
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+
+@Singleton
+class LoadLastDateWithPriorityDataUseCase
+@Inject
+constructor(
+ private val healthConnectManager: HealthConnectManager,
+ private val loadEntriesHelper: LoadEntriesHelper,
+ private val loadPriorityListUseCase: ILoadPriorityListUseCase,
+ private val timeSource: TimeSource,
+ @IoDispatcher private val dispatcher: CoroutineDispatcher
+) : ILoadLastDateWithPriorityDataUseCase {
+
+ /**
+ * Returns the last local date with data for this health permission type, from the data owned by
+ * apps on the priority list.
+ */
+ override suspend fun invoke(
+ healthPermissionType: HealthPermissionType
+ ): UseCaseResults<LocalDate?> =
+ withContext(dispatcher) {
+ var latestDateWithData: LocalDate? = null
+ try {
+ when (val priorityAppsResult =
+ loadPriorityListUseCase.invoke(
+ fromHealthPermissionType(healthPermissionType))) {
+ is UseCaseResults.Success -> {
+ val priorityApps = priorityAppsResult.data
+
+ priorityApps.forEach { priorityApp ->
+ val lastDateWithDataForApp =
+ loadLastDateWithDataForApp(
+ healthPermissionType, priorityApp.packageName)
+
+ latestDateWithData =
+ maxDateOrNull(latestDateWithData, lastDateWithDataForApp)
+ }
+ }
+ is UseCaseResults.Failed -> {
+ return@withContext UseCaseResults.Failed(priorityAppsResult.exception)
+ }
+ }
+
+ return@withContext UseCaseResults.Success(latestDateWithData)
+ } catch (e: Exception) {
+ UseCaseResults.Failed(e)
+ }
+ }
+
+ /**
+ * Returns the last date with data from a particular packageName, or null if no such date
+ * exists.
+ *
+ * To avoid querying all entries of all time, we first query for the activity dates for this
+ * healthPermissionType. We sort the dates in descending order and we find the first date which
+ * contains data from this packageName.
+ */
+ private suspend fun loadLastDateWithDataForApp(
+ healthPermissionType: HealthPermissionType,
+ packageName: String
+ ): LocalDate? {
+
+ val recordTypes = HealthPermissionToDatatypeMapper.getDataTypes(healthPermissionType)
+
+ val datesWithData = suspendCancellableCoroutine { continuation ->
+ healthConnectManager.queryActivityDates(
+ recordTypes, Runnable::run, continuation.asOutcomeReceiver())
+ }
+
+ val today = timeSource.currentLocalDateTime().toLocalDate()
+ val recentDates =
+ datesWithData.filter { date ->
+ date.isAfter(today.minusMonths(1)) && !date.isAfter(today)
+ }
+
+ if (recentDates.isEmpty()) return null
+
+ val minDate = recentDates.min()
+
+ // Query the data entries from this last month in one single API call
+ val input =
+ LoadDataEntriesInput(
+ permissionType = healthPermissionType,
+ packageName = packageName,
+ displayedStartTime = minDate.toInstantAtStartOfDay(),
+ period = DateNavigationPeriod.PERIOD_MONTH,
+ showDataOrigin = false)
+
+ val entryRecords = loadEntriesHelper.readRecords(input)
+
+ if (entryRecords.isNotEmpty()) {
+ // The records are returned in descending order by startTime
+ return loadEntriesHelper.getStartTime(entryRecords[0]).toLocalDate()
+ }
+
+ return null
+ }
+
+ private fun maxDateOrNull(firstDate: LocalDate?, secondDate: LocalDate?): LocalDate? {
+ if (firstDate == null && secondDate == null) return null
+ if (firstDate == null) return secondDate
+ if (secondDate == null) return firstDate
+
+ return max(firstDate, secondDate)
+ }
+}
+
+interface ILoadLastDateWithPriorityDataUseCase {
+ suspend fun invoke(healthPermissionType: HealthPermissionType): UseCaseResults<LocalDate?>
+}
diff --git a/apk/src/com/android/healthconnect/controller/datasources/api/LoadMostRecentAggregationsUseCase.kt b/apk/src/com/android/healthconnect/controller/datasources/api/LoadMostRecentAggregationsUseCase.kt
index 12534aa4..4d202cfb 100644
--- a/apk/src/com/android/healthconnect/controller/datasources/api/LoadMostRecentAggregationsUseCase.kt
+++ b/apk/src/com/android/healthconnect/controller/datasources/api/LoadMostRecentAggregationsUseCase.kt
@@ -13,49 +13,39 @@
*/
package com.android.healthconnect.controller.datasources.api
-import android.health.connect.HealthConnectManager
import android.health.connect.HealthDataCategory
-import android.health.connect.datatypes.IntervalRecord
-import android.health.connect.datatypes.Record
-import androidx.core.os.asOutcomeReceiver
import com.android.healthconnect.controller.data.entries.api.ILoadDataAggregationsUseCase
-import com.android.healthconnect.controller.data.entries.api.ILoadSleepDataUseCase
import com.android.healthconnect.controller.data.entries.api.LoadAggregationInput
-import com.android.healthconnect.controller.data.entries.api.LoadDataEntriesInput
import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod
import com.android.healthconnect.controller.datasources.AggregationCardInfo
import com.android.healthconnect.controller.permissions.data.HealthPermissionType
import com.android.healthconnect.controller.service.IoDispatcher
import com.android.healthconnect.controller.shared.HealthDataCategoryInt
-import com.android.healthconnect.controller.shared.HealthPermissionToDatatypeMapper
import com.android.healthconnect.controller.shared.usecase.UseCaseResults
-import com.android.healthconnect.controller.utils.atStartOfDay
-import com.android.healthconnect.controller.utils.isAtLeastOneDayAfter
-import com.android.healthconnect.controller.utils.isOnDayAfter
-import com.android.healthconnect.controller.utils.isOnSameDay
import com.android.healthconnect.controller.utils.toInstantAtStartOfDay
-import com.android.healthconnect.controller.utils.toLocalDate
-import com.google.common.collect.Comparators.max
-import com.google.common.collect.Comparators.min
import java.time.Instant
import java.time.LocalDate
-import java.time.ZoneId
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
@Singleton
class LoadMostRecentAggregationsUseCase
@Inject
constructor(
- private val healthConnectManager: HealthConnectManager,
private val loadDataAggregationsUseCase: ILoadDataAggregationsUseCase,
- private val loadSleepDataUseCase: ILoadSleepDataUseCase,
+ private val loadLastDateWithPriorityDataUseCase: ILoadLastDateWithPriorityDataUseCase,
+ private val sleepSessionHelper: ISleepSessionHelper,
@IoDispatcher private val dispatcher: CoroutineDispatcher,
) : ILoadMostRecentAggregationsUseCase {
- /** Invoked to provide [AggregationDataCard]s info for Activity and Sleep */
+
+ /**
+ * Provides the most recent [AggregationDataCard]s info for Activity or Sleep.
+ *
+ * The latest aggregation always belongs to apps on the priority list. Apps not on the priority
+ * list do not contribute to aggregations or the last displayed date.
+ */
override suspend operator fun invoke(
healthDataCategory: @HealthDataCategoryInt Int
): UseCaseResults<List<AggregationCardInfo>> =
@@ -63,59 +53,45 @@ constructor(
try {
val resultsList = mutableListOf<AggregationCardInfo>()
if (healthDataCategory == HealthDataCategory.ACTIVITY) {
- val stepsRecordTypes =
- HealthPermissionToDatatypeMapper.getDataTypes(HealthPermissionType.STEPS)
- val datesWithStepsData = suspendCancellableCoroutine { continuation ->
- healthConnectManager.queryActivityDates(
- stepsRecordTypes, Runnable::run, continuation.asOutcomeReceiver())
- }
-
- if (datesWithStepsData.isNotEmpty()) {
- val stepsCardInfo =
- getLastAvailableAggregation(
- datesWithStepsData, HealthPermissionType.STEPS)
- stepsCardInfo?.let { resultsList.add(it) }
- }
-
- val distanceRecordTypes =
- HealthPermissionToDatatypeMapper.getDataTypes(HealthPermissionType.DISTANCE)
- val datesWithDistanceData = suspendCancellableCoroutine { continuation ->
- healthConnectManager.queryActivityDates(
- distanceRecordTypes, Runnable::run, continuation.asOutcomeReceiver())
- }
-
- if (datesWithDistanceData.isNotEmpty()) {
- val distanceCardInfo =
- getLastAvailableAggregation(
- datesWithDistanceData, HealthPermissionType.DISTANCE)
- distanceCardInfo?.let { resultsList.add(it) }
- }
-
- val caloriesRecordTypes =
- HealthPermissionToDatatypeMapper.getDataTypes(
+ val activityPermissionTypesWithAggregations =
+ listOf(
+ HealthPermissionType.STEPS,
+ HealthPermissionType.DISTANCE,
HealthPermissionType.TOTAL_CALORIES_BURNED)
- val datesWithCaloriesData = suspendCancellableCoroutine { continuation ->
- healthConnectManager.queryActivityDates(
- caloriesRecordTypes, Runnable::run, continuation.asOutcomeReceiver())
- }
- if (datesWithCaloriesData.isNotEmpty()) {
- val caloriesCardInfo =
- getLastAvailableAggregation(
- datesWithCaloriesData, HealthPermissionType.TOTAL_CALORIES_BURNED)
- caloriesCardInfo?.let { resultsList.add(it) }
+ activityPermissionTypesWithAggregations.forEach { permissionType ->
+ val lastDateWithData: LocalDate?
+ when (val lastDateWithDataResult =
+ loadLastDateWithPriorityDataUseCase.invoke(permissionType)) {
+ is UseCaseResults.Success -> {
+ lastDateWithData = lastDateWithDataResult.data
+ }
+ is UseCaseResults.Failed -> {
+ return@withContext UseCaseResults.Failed(
+ lastDateWithDataResult.exception)
+ }
+ }
+
+ val cardInfo =
+ getLastAvailableActivityAggregation(lastDateWithData, permissionType)
+ cardInfo?.let { resultsList.add(it) }
}
} else if (healthDataCategory == HealthDataCategory.SLEEP) {
- val sleepRecordTypes =
- HealthPermissionToDatatypeMapper.getDataTypes(HealthPermissionType.SLEEP)
- val datesWithSleepData = suspendCancellableCoroutine { continuation ->
- healthConnectManager.queryActivityDates(
- sleepRecordTypes, Runnable::run, continuation.asOutcomeReceiver())
- }
- if (datesWithSleepData.isNotEmpty()) {
- val sleepCardInfo = getLastAvailableSleepAggregation(datesWithSleepData)
- sleepCardInfo?.let { resultsList.add(it) }
+
+ val lastDateWithSleepData: LocalDate?
+ when (val lastDateWithSleepDataResult =
+ loadLastDateWithPriorityDataUseCase.invoke(HealthPermissionType.SLEEP)) {
+ is UseCaseResults.Success -> {
+ lastDateWithSleepData = lastDateWithSleepDataResult.data
+ }
+ is UseCaseResults.Failed -> {
+ return@withContext UseCaseResults.Failed(
+ lastDateWithSleepDataResult.exception)
+ }
}
+
+ val sleepCardInfo = getLastAvailableSleepAggregation(lastDateWithSleepData)
+ sleepCardInfo?.let { resultsList.add(it) }
}
UseCaseResults.Success(resultsList.toList())
@@ -124,13 +100,16 @@ constructor(
}
}
- private suspend fun getLastAvailableAggregation(
- datesWithData: List<LocalDate>,
+ private suspend fun getLastAvailableActivityAggregation(
+ lastDateWithData: LocalDate?,
healthPermissionType: HealthPermissionType
): AggregationCardInfo? {
+ if (lastDateWithData == null) {
+ return null
+ }
+
// Get aggregate for last day
- val lastDate = datesWithData.maxOf { it }
- val lastDateInstant = lastDate.atStartOfDay(ZoneId.systemDefault()).toInstant()
+ val lastDateInstant = lastDateWithData.toInstantAtStartOfDay()
// call for aggregate
val input =
@@ -147,190 +126,30 @@ constructor(
AggregationCardInfo(healthPermissionType, useCaseResult.data, lastDateInstant)
}
is UseCaseResults.Failed -> {
- // Something went wrong here, so return nothing
- null
+ throw useCaseResult.exception
}
}
}
private suspend fun getLastAvailableSleepAggregation(
- datesWithData: List<LocalDate>
+ lastDateWithData: LocalDate?
): AggregationCardInfo? {
- // Get last date with data (the start date of sleep sessions)
- val lastDateWithData = datesWithData.last()
- val lastDateInstant = lastDateWithData.toInstantAtStartOfDay()
-
- // Get all sleep sessions starting on that date
- val input =
- LoadDataEntriesInput(
- HealthPermissionType.SLEEP,
- packageName = null,
- displayedStartTime = lastDateInstant,
- period = DateNavigationPeriod.PERIOD_DAY,
- showDataOrigin = false)
-
- return when (val result = loadSleepDataUseCase.invoke(input)) {
- is UseCaseResults.Success -> {
- val sleepRecords = result.data
- val (minStartTime, maxEndTime) =
- clusterSleepSessions(sleepRecords, lastDateWithData)
- computeSleepAggregation(minStartTime, maxEndTime)
- }
- is UseCaseResults.Failed -> {
- null
- }
- }
- }
-
- /**
- * Given a list of sleep session records starting on the last date with data, returns a pair of
- * Instants representing a time interval [minStartTime, maxEndTime] between which we will query
- * the aggregated time of sleep sessions.
- */
- private suspend fun clusterSleepSessions(
- entries: List<Record>,
- lastDateWithData: LocalDate
- ): Pair<Instant, Instant> {
-
- var minStartTime: Instant = Instant.MAX
- var maxEndTime: Instant = Instant.MIN
-
- // Determine if there is at least one session starting on Day 2 and finishing on Day 3
- // (Case 3)
- val sessionsCrossingMidnight =
- entries.any { record ->
- val currentSleepSession = (record as IntervalRecord)
- (currentSleepSession.endTime.isAtLeastOneDayAfter(currentSleepSession.startTime))
- }
-
- // Handle Case 3 - at least one sleep session starts on Day 2 and finishes on Day 3
- if (sessionsCrossingMidnight) {
- return handleSessionsCrossingMidnight(entries)
+ if (lastDateWithData == null) {
+ return null
}
- // case 1 - start and end times on the same day (Day 2)
- // case 2 - there might be sessions starting on Day 1 and finishing on Day 2
- // All sessions start and end on this day
- // now we look at the date before to see if there is a session
- // that ends today
- val secondToLastDateInstant =
- lastDateWithData.minusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant()
- val lastDateWithDataInstant = lastDateWithData.toInstantAtStartOfDay()
-
- // Get all sleep sessions starting on secondToLastDate
- val input =
- LoadDataEntriesInput(
- HealthPermissionType.SLEEP,
- packageName = null,
- displayedStartTime = secondToLastDateInstant,
- period = DateNavigationPeriod.PERIOD_DAY,
- showDataOrigin = false)
-
- when (val result = loadSleepDataUseCase.invoke(input)) {
+ when (val result = sleepSessionHelper.clusterSleepSessions(lastDateWithData)) {
is UseCaseResults.Success -> {
- val previousDaySleepData = result.data
- // For each session check if the end date is last date
- // If we find it, extend minStartTime to the start time of that session
-
- if (previousDaySleepData.isEmpty()) {
- // Case 1 - All sessions start and end on this day (Day 2)
- minStartTime = entries.minOf { (it as IntervalRecord).startTime }
- maxEndTime = entries.maxOf { (it as IntervalRecord).endTime }
- } else {
- // Case 2 - At least one session starts on Day 1 and finishes on Day 2 or later
- return handleSessionsStartingOnSecondToLastDate(
- previousDaySleepData, lastDateWithDataInstant)
+ result.data?.let { pair ->
+ return computeSleepAggregation(pair.first, pair.second)
}
}
is UseCaseResults.Failed -> {
- Pair(Instant.MAX, Instant.MAX)
- }
- }
-
- return Pair(minStartTime, maxEndTime)
- }
-
- /** Handles sleep session case 3 - At least one session crosses midnight into Day 3. */
- private fun handleSessionsCrossingMidnight(entries: List<Record>): Pair<Instant, Instant> {
- // We show aggregation for all sessions ending on day 3
- // Find the max end time from all sessions crossing midnight
- // and the min start time from all sessions that end on day 3
- // There can be no session starting on day 3, otherwise that would be the latest date
- var minStartTime: Instant = Instant.MAX
- var maxEndTime: Instant = Instant.MIN
-
- entries.forEach { record ->
- val currentSleepSession = (record as IntervalRecord)
- // Start day = Day 2
- // We look at most 2 calendar days in the future, so the max possible end time
- // is Day 4 at 12:00am
- val maxPossibleEnd =
- currentSleepSession.startTime
- .toLocalDate()
- .atStartOfDay(ZoneId.systemDefault())
- .plusDays(2)
- .toInstant()
-
- if (currentSleepSession.endTime.isOnSameDay(currentSleepSession.startTime)) {
- // This sleep session starts and ends on Day 2
- // So we do not count this for either min or max
- // As it belongs to the aggregations for Day 2
- } else if (currentSleepSession.endTime.isOnDayAfter(currentSleepSession.startTime)) {
- // This is a session [Day 2 - Day 3]
- // min and max candidate
- minStartTime = min(minStartTime, currentSleepSession.startTime)
- maxEndTime = max(maxEndTime, currentSleepSession.endTime)
- } else {
- // currentSleepSession.endTime is further than Day 3
- // Max End time should be Day 4 at 12am
- minStartTime = min(minStartTime, currentSleepSession.startTime)
- maxEndTime = max(maxEndTime, maxPossibleEnd)
- }
- }
-
- return Pair(minStartTime, maxEndTime)
- }
-
- /**
- * Handles sleep session Case 2 - At least one session starts on Day 1 and finishes on Day 2 or
- * later.
- */
- private fun handleSessionsStartingOnSecondToLastDate(
- previousDaySleepData: List<Record>,
- lastDateWithDataInstant: Instant
- ): Pair<Instant, Instant> {
- var minStartTime: Instant = Instant.MAX
- var maxEndTime: Instant = Instant.MIN
-
- previousDaySleepData.forEach { record ->
- val currentSleepSession = (record as IntervalRecord)
-
- // Start date is Day 1, so the max possible end date is Day 3 12am
- val maxPossibleEnd =
- currentSleepSession.startTime
- .toLocalDate()
- .atStartOfDay(ZoneId.systemDefault())
- .plusDays(2)
- .toInstant()
-
- if (currentSleepSession.endTime.isOnSameDay(lastDateWithDataInstant)) {
- // This is a sleep session that starts on Day 1 and finishes on Day 2
- // min/max candidate
- minStartTime = min(minStartTime, currentSleepSession.startTime)
- maxEndTime = max(maxEndTime, currentSleepSession.endTime)
- } else if (currentSleepSession.endTime.isOnSameDay(currentSleepSession.startTime)) {
- // This is a sleep session that starts and ends on Day 1
- // We do not count it for min/max because this belongs to Day 1
- // aggregation
- } else {
- // This is a sleep session that start on Day 1 and ends after Day 2
- // Then the max end time should be Day 3 at 12am
- minStartTime = min(minStartTime, currentSleepSession.startTime)
- maxEndTime = max(maxEndTime, maxPossibleEnd)
+ throw result.exception
}
}
- return Pair(minStartTime, maxEndTime)
+ return null
}
/**
@@ -340,7 +159,7 @@ constructor(
private suspend fun computeSleepAggregation(
minStartTime: Instant,
maxEndTime: Instant
- ): AggregationCardInfo? {
+ ): AggregationCardInfo {
val aggregationInput =
LoadAggregationInput.CustomAggregation(
permissionType = HealthPermissionType.SLEEP,
@@ -353,14 +172,10 @@ constructor(
is UseCaseResults.Success -> {
// use this aggregation value to construct the card
AggregationCardInfo(
- HealthPermissionType.SLEEP,
- useCaseResult.data,
- minStartTime.atStartOfDay(),
- maxEndTime.atStartOfDay())
+ HealthPermissionType.SLEEP, useCaseResult.data, minStartTime, maxEndTime)
}
is UseCaseResults.Failed -> {
- // Something went wrong here, so return nothing
- null
+ throw useCaseResult.exception
}
}
}
diff --git a/apk/src/com/android/healthconnect/controller/datasources/api/LoadPriorityEntriesUseCase.kt b/apk/src/com/android/healthconnect/controller/datasources/api/LoadPriorityEntriesUseCase.kt
new file mode 100644
index 00000000..3ffaf13b
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/datasources/api/LoadPriorityEntriesUseCase.kt
@@ -0,0 +1,80 @@
+package com.android.healthconnect.controller.datasources.api
+
+import android.health.connect.datatypes.Record
+import com.android.healthconnect.controller.data.entries.api.LoadDataEntriesInput
+import com.android.healthconnect.controller.data.entries.api.LoadEntriesHelper
+import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType
+import com.android.healthconnect.controller.permissiontypes.api.ILoadPriorityListUseCase
+import com.android.healthconnect.controller.service.IoDispatcher
+import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions
+import com.android.healthconnect.controller.shared.usecase.UseCaseResults
+import com.android.healthconnect.controller.utils.toInstantAtStartOfDay
+import java.time.LocalDate
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+@Singleton
+class LoadPriorityEntriesUseCase
+@Inject
+constructor(
+ private val loadEntriesHelper: LoadEntriesHelper,
+ private val loadPriorityListUseCase: ILoadPriorityListUseCase,
+ @IoDispatcher private val dispatcher: CoroutineDispatcher
+) : ILoadPriorityEntriesUseCase {
+
+ /**
+ * Returns a list of records from the specified date originating from any of the apps on the
+ * priority list for this healthPermissionType.
+ */
+ override suspend fun invoke(
+ healthPermissionType: HealthPermissionType,
+ localDate: LocalDate
+ ): UseCaseResults<List<Record>> =
+ withContext(dispatcher) {
+ try {
+ val localDateInstant = localDate.toInstantAtStartOfDay()
+ val records = mutableListOf<Record>()
+
+ when (val priorityAppsResult =
+ loadPriorityListUseCase.invoke(
+ HealthDataCategoryExtensions.fromHealthPermissionType(
+ healthPermissionType))) {
+ is UseCaseResults.Success -> {
+ val priorityApps = priorityAppsResult.data
+
+ priorityApps.forEach { priorityApp ->
+ val input =
+ LoadDataEntriesInput(
+ HealthPermissionType.SLEEP,
+ packageName = priorityApp.packageName,
+ displayedStartTime = localDateInstant,
+ period = DateNavigationPeriod.PERIOD_DAY,
+ showDataOrigin = false)
+ val entryRecords = loadEntriesHelper.readRecords(input)
+
+ records.addAll(entryRecords)
+ }
+ }
+ is UseCaseResults.Failed -> {
+ throw priorityAppsResult.exception
+ }
+ }
+
+ // Sorted for testing
+ UseCaseResults.Success(
+ records.sortedByDescending { loadEntriesHelper.getStartTime(it) })
+ } catch (e: Exception) {
+ UseCaseResults.Failed(e)
+ }
+ }
+}
+
+interface ILoadPriorityEntriesUseCase {
+ suspend fun invoke(
+ healthPermissionType: HealthPermissionType,
+ localDate: LocalDate
+ ): UseCaseResults<List<Record>>
+}
diff --git a/apk/src/com/android/healthconnect/controller/datasources/api/SleepSessionHelper.kt b/apk/src/com/android/healthconnect/controller/datasources/api/SleepSessionHelper.kt
new file mode 100644
index 00000000..728bc765
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/datasources/api/SleepSessionHelper.kt
@@ -0,0 +1,207 @@
+package com.android.healthconnect.controller.datasources.api
+
+import android.health.connect.datatypes.IntervalRecord
+import android.health.connect.datatypes.Record
+import android.health.connect.datatypes.SleepSessionRecord
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType
+import com.android.healthconnect.controller.service.IoDispatcher
+import com.android.healthconnect.controller.shared.usecase.UseCaseResults
+import com.android.healthconnect.controller.utils.isAtLeastOneDayAfter
+import com.android.healthconnect.controller.utils.isOnDayAfter
+import com.android.healthconnect.controller.utils.isOnSameDay
+import com.android.healthconnect.controller.utils.toInstantAtStartOfDay
+import com.android.healthconnect.controller.utils.toLocalDate
+import com.google.common.collect.Comparators
+import java.lang.Exception
+import java.time.Instant
+import java.time.LocalDate
+import java.time.ZoneId
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+@Singleton
+class SleepSessionHelper
+@Inject
+constructor(
+ private val loadPriorityEntriesUseCase: ILoadPriorityEntriesUseCase,
+ @IoDispatcher private val dispatcher: CoroutineDispatcher,
+) : ISleepSessionHelper {
+
+ /**
+ * Given a list of sleep session records starting on the last date with data, returns a pair of
+ * Instants representing a time interval [minStartTime, maxEndTime] between which we will query
+ * the aggregated time of sleep sessions.
+ */
+ override suspend fun clusterSleepSessions(
+ lastDateWithData: LocalDate
+ ): UseCaseResults<Pair<Instant, Instant>?> =
+ withContext(dispatcher) {
+ try {
+ val currentDaySleepData = getPrioritySleepRecords(lastDateWithData)
+
+ if (currentDaySleepData.isEmpty()) {
+ return@withContext UseCaseResults.Success(null)
+ }
+
+ // Determine if there is at least one session starting on Day 2 and finishing on Day
+ // 3
+ // (Case 3)
+ val sessionsCrossingMidnight =
+ currentDaySleepData.any { record ->
+ val currentSleepSession = (record as IntervalRecord)
+ (currentSleepSession.endTime.isAtLeastOneDayAfter(
+ currentSleepSession.startTime))
+ }
+
+ // Handle Case 3 - at least one sleep session starts on Day 2 and finishes on Day 3
+ if (sessionsCrossingMidnight) {
+ return@withContext UseCaseResults.Success(
+ handleSessionsCrossingMidnight(currentDaySleepData))
+ }
+
+ // case 1 - start and end times on the same day (Day 2)
+ // case 2 - there might be sessions starting on Day 1 and finishing on Day 2
+ // All sessions start and end on this day
+ // now we look at the date before to see if there is a session
+ // that ends today
+ val secondToLastDayWithData = lastDateWithData.minusDays(1)
+ val lastDateWithDataInstant = lastDateWithData.toInstantAtStartOfDay()
+
+ // Get all sleep sessions starting on secondToLastDate
+ val previousDaySleepData = getPrioritySleepRecords(secondToLastDayWithData)
+
+ // For each session check if the end date is last date
+ // If we find it, extend minStartTime to the start time of that session
+ // Case 1 - All sessions start and end on this day (Day 2)
+ // We also need these for case2
+ val minStartTime: Instant =
+ currentDaySleepData.minOf { (it as IntervalRecord).startTime }
+ val maxEndTime: Instant =
+ currentDaySleepData.maxOf { (it as IntervalRecord).endTime }
+
+ if (previousDaySleepData.isNotEmpty()) {
+ // Case 2 - At least one session starts on Day 1 and finishes on Day 2 or later
+ return@withContext UseCaseResults.Success(
+ handleSessionsStartingOnSecondToLastDate(
+ previousDaySleepData,
+ lastDateWithDataInstant,
+ minStartTime,
+ maxEndTime))
+ }
+
+ return@withContext UseCaseResults.Success(Pair(minStartTime, maxEndTime))
+ } catch (e: Exception) {
+ return@withContext UseCaseResults.Failed(e)
+ }
+ }
+
+ /** Handles sleep session case 3 - At least one session crosses midnight into Day 3. */
+ private fun handleSessionsCrossingMidnight(entries: List<Record>): Pair<Instant, Instant> {
+ // We show aggregation for all sessions ending on day 3
+ // Find the max end time from all sessions crossing midnight
+ // and the min start time from all sessions that end on day 3
+ // There can be no session starting on day 3, otherwise that would be the latest date
+ var minStartTime: Instant = Instant.MAX
+ var maxEndTime: Instant = Instant.MIN
+
+ entries.forEach { record ->
+ val currentSleepSession = (record as IntervalRecord)
+ // Start day = Day 2
+ // We look at most 2 calendar days in the future, so the max possible end time
+ // is Day 4 at 12:00am
+ val maxPossibleEnd =
+ currentSleepSession.startTime
+ .toLocalDate()
+ .atStartOfDay(ZoneId.systemDefault())
+ .plusDays(2)
+ .toInstant()
+
+ if (currentSleepSession.endTime.isOnSameDay(currentSleepSession.startTime)) {
+ // This sleep session starts and ends on Day 2
+ // So we do not count this for either min or max
+ // As it belongs to the aggregations for Day 2
+ } else if (currentSleepSession.endTime.isOnDayAfter(currentSleepSession.startTime)) {
+ // This is a session [Day 2 - Day 3]
+ // min and max candidate
+ minStartTime = Comparators.min(minStartTime, currentSleepSession.startTime)
+ maxEndTime = Comparators.max(maxEndTime, currentSleepSession.endTime)
+ } else {
+ // currentSleepSession.endTime is further than Day 3
+ // Max End time should be Day 4 at 12am
+ minStartTime = Comparators.min(minStartTime, currentSleepSession.startTime)
+ maxEndTime = Comparators.max(maxEndTime, maxPossibleEnd)
+ }
+ }
+
+ return Pair(minStartTime, maxEndTime)
+ }
+
+ /**
+ * Handles sleep session Case 2 - At least one session starts on Day 1 and finishes on Day 2 or
+ * later.
+ */
+ private fun handleSessionsStartingOnSecondToLastDate(
+ previousDaySleepData: List<Record>,
+ lastDateWithDataInstant: Instant,
+ lastDayMinStartTime: Instant,
+ lastDayMaxEndTime: Instant
+ ): Pair<Instant, Instant> {
+
+ // This ensures we also take into account the sessions from lastDateWithData
+ var minStartTime = lastDayMinStartTime
+ var maxEndTime = lastDayMaxEndTime
+
+ previousDaySleepData.forEach { record ->
+ val currentSleepSession = (record as IntervalRecord)
+
+ // Start date is Day 1, so the max possible end date is Day 3 12am
+ val maxPossibleEnd =
+ currentSleepSession.startTime
+ .toLocalDate()
+ .atStartOfDay(ZoneId.systemDefault())
+ .plusDays(2)
+ .toInstant()
+
+ if (currentSleepSession.endTime.isOnSameDay(lastDateWithDataInstant)) {
+ // This is a sleep session that starts on Day 1 and finishes on Day 2
+ // min/max candidate
+ minStartTime = Comparators.min(minStartTime, currentSleepSession.startTime)
+ maxEndTime = Comparators.max(maxEndTime, currentSleepSession.endTime)
+ } else if (currentSleepSession.endTime.isOnSameDay(currentSleepSession.startTime)) {
+ // This is a sleep session that starts and ends on Day 1
+ // We do not count it for min/max because this belongs to Day 1
+ // aggregation
+ } else {
+ // This is a sleep session that start on Day 1 and ends after Day 2
+ // Then the max end time should be Day 3 at 12am
+ minStartTime = Comparators.min(minStartTime, currentSleepSession.startTime)
+ maxEndTime = Comparators.max(maxEndTime, maxPossibleEnd)
+ }
+ }
+
+ return Pair(minStartTime, maxEndTime)
+ }
+
+ /** Returns all priority sleep records starting on lastDateWithData. */
+ private suspend fun getPrioritySleepRecords(
+ lastDateWithData: LocalDate
+ ): List<SleepSessionRecord> {
+ when (val result =
+ loadPriorityEntriesUseCase.invoke(HealthPermissionType.SLEEP, lastDateWithData)) {
+ is UseCaseResults.Success -> {
+ return result.data.map { it as SleepSessionRecord }
+ }
+ is UseCaseResults.Failed -> {
+ throw result.exception
+ }
+ }
+ }
+}
+
+interface ISleepSessionHelper {
+ suspend fun clusterSleepSessions(
+ lastDateWithData: LocalDate
+ ): UseCaseResults<Pair<Instant, Instant>?>
+}
diff --git a/apk/src/com/android/healthconnect/controller/datasources/api/UpdatePriorityListUseCase.kt b/apk/src/com/android/healthconnect/controller/datasources/api/UpdatePriorityListUseCase.kt
index 5f5e0e3a..21d48705 100644
--- a/apk/src/com/android/healthconnect/controller/datasources/api/UpdatePriorityListUseCase.kt
+++ b/apk/src/com/android/healthconnect/controller/datasources/api/UpdatePriorityListUseCase.kt
@@ -1,3 +1,16 @@
+/**
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
package com.android.healthconnect.controller.datasources.api
import android.health.connect.HealthConnectManager
@@ -17,10 +30,13 @@ class UpdatePriorityListUseCase
constructor(
private val healthConnectManager: HealthConnectManager,
@IoDispatcher private val dispatcher: CoroutineDispatcher
-): IUpdatePriorityListUseCase {
+) : IUpdatePriorityListUseCase {
/** Updates the priority list of the stored [DataOrigin]s for given [HealthDataCategory]. */
- override suspend operator fun invoke(priorityList: List<String>, category: @HealthDataCategoryInt Int) {
+ override suspend operator fun invoke(
+ priorityList: List<String>,
+ category: @HealthDataCategoryInt Int
+ ) {
withContext(dispatcher) {
val dataOrigins: List<DataOrigin> =
priorityList
diff --git a/apk/src/com/android/healthconnect/controller/datasources/appsources/AppSourcesAdapter.kt b/apk/src/com/android/healthconnect/controller/datasources/appsources/AppSourcesAdapter.kt
index c0497c29..4b9b6bb5 100644
--- a/apk/src/com/android/healthconnect/controller/datasources/appsources/AppSourcesAdapter.kt
+++ b/apk/src/com/android/healthconnect/controller/datasources/appsources/AppSourcesAdapter.kt
@@ -29,6 +29,11 @@ import com.android.healthconnect.controller.shared.HealthDataCategoryInt
import com.android.healthconnect.controller.shared.app.AppMetadata
import com.android.healthconnect.controller.shared.app.AppUtils
import com.android.healthconnect.controller.utils.AttributeResolver
+import com.android.healthconnect.controller.utils.logging.DataSourcesElement
+import com.android.healthconnect.controller.utils.logging.ElementName
+import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
+import com.android.healthconnect.controller.utils.logging.HealthConnectLoggerEntryPoint
+import dagger.hilt.android.EntryPointAccessors
import java.text.NumberFormat
/** RecyclerView adapter that holds the list of app sources for this [HealthDataCategory]. */
@@ -50,6 +55,16 @@ class AppSourcesAdapter(
private val POSITION_CHANGED_PAYLOAD = Any()
+ private var logger: HealthConnectLogger
+ var logName: ElementName = DataSourcesElement.DATA_TOTALS_CARD
+
+ init {
+ val hiltEntryPoint =
+ EntryPointAccessors.fromApplication(
+ context.applicationContext, HealthConnectLoggerEntryPoint::class.java)
+ logger = hiltEntryPoint.logger()
+ }
+
interface OnAppRemovedFromPriorityListListener {
fun onAppRemovedFromPriorityList()
}
@@ -153,6 +168,8 @@ class AppSourcesAdapter(
} else {
setupItemForDragMode(isOnlyApp)
}
+
+ logger.logImpression(DataSourcesElement.APP_SOURCE_BUTTON)
}
private fun setupItemForEditMode(appPosition: Int) {
@@ -162,6 +179,8 @@ class AppSourcesAdapter(
AttributeResolver.getDrawable(itemView.context, R.attr.closeIcon)
actionView.setOnTouchListener(null)
actionView.setOnClickListener {
+ logger.logInteraction(DataSourcesElement.REMOVE_APP_SOURCE_BUTTON)
+
val currentPriority = priorityList.toMutableList()
val removedItem = currentPriority.removeAt(appPosition)
dataSourcesViewModel.setEditedPriorityList(currentPriority)
@@ -175,6 +194,7 @@ class AppSourcesAdapter(
removeItem(appPosition)
onAppRemovedListener.onAppRemovedFromPriorityList()
}
+ logger.logImpression(DataSourcesElement.REMOVE_APP_SOURCE_BUTTON)
}
// These items are not clickable and so onTouch does not need to reimplement click
@@ -191,12 +211,14 @@ class AppSourcesAdapter(
AttributeResolver.getDrawable(itemView.context, R.attr.priorityItemDragIcon)
actionView.setOnClickListener(null)
actionView.setOnTouchListener { _, event ->
+ logger.logInteraction(DataSourcesElement.REORDER_APP_SOURCE_BUTTON)
if (event.action == MotionEvent.ACTION_DOWN ||
event.action == MotionEvent.ACTION_UP) {
onItemDragStartedListener?.startDrag(this)
}
false
}
+ logger.logImpression(DataSourcesElement.REORDER_APP_SOURCE_BUTTON)
}
}
}
diff --git a/apk/src/com/android/healthconnect/controller/datasources/appsources/AppSourcesPreference.kt b/apk/src/com/android/healthconnect/controller/datasources/appsources/AppSourcesPreference.kt
index 4c7843bb..453dd3ad 100644
--- a/apk/src/com/android/healthconnect/controller/datasources/appsources/AppSourcesPreference.kt
+++ b/apk/src/com/android/healthconnect/controller/datasources/appsources/AppSourcesPreference.kt
@@ -39,6 +39,7 @@ constructor(
private var potentialAppSourcesList: List<AppMetadata> = listOf()
private lateinit var priorityListView: RecyclerView
private lateinit var adapter: AppSourcesAdapter
+ private var isEditMode = false
init {
layoutResource = R.layout.widget_linear_layout_preference
@@ -64,6 +65,8 @@ constructor(
priorityListView.adapter = adapter
priorityListView.layoutManager = AppSourcesLinearLayoutManager(context, adapter)
createAndAttachItemMoveCallback()
+
+ adapter.toggleEditMode(isEditMode)
}
override fun attachCallback() {
@@ -77,10 +80,20 @@ constructor(
priorityListMover.attachToRecyclerView(priorityListView)
}
+ /** Toggles the edit mode on/off after the preference has been created */
fun toggleEditMode(isEditMode: Boolean) {
+ setEditMode(isEditMode)
adapter.toggleEditMode(isEditMode)
}
+ /**
+ * Sets the edit mode on/off before the preference is fully created and the onBindViewHolder
+ * method is called.
+ */
+ fun setEditMode(isEditMode: Boolean) {
+ this.isEditMode = isEditMode
+ }
+
override fun isSameItem(preference: Preference): Boolean {
return preference is AppSourcesPreference && this == preference
}
diff --git a/apk/src/com/android/healthconnect/controller/managedata/ManageDataFragment.kt b/apk/src/com/android/healthconnect/controller/managedata/ManageDataFragment.kt
index d47abd84..ab58823c 100644
--- a/apk/src/com/android/healthconnect/controller/managedata/ManageDataFragment.kt
+++ b/apk/src/com/android/healthconnect/controller/managedata/ManageDataFragment.kt
@@ -1,13 +1,16 @@
package com.android.healthconnect.controller.managedata
+import android.health.connect.HealthDataCategory
import android.icu.text.MessageFormat
import android.os.Bundle
import android.view.View
+import androidx.core.os.bundleOf
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import com.android.healthconnect.controller.R
import com.android.healthconnect.controller.autodelete.AutoDeleteRange
import com.android.healthconnect.controller.autodelete.AutoDeleteViewModel
+import com.android.healthconnect.controller.categories.HealthDataCategoriesFragment
import com.android.healthconnect.controller.shared.preference.HealthPreference
import com.android.healthconnect.controller.shared.preference.HealthPreferenceFragment
import com.android.healthconnect.controller.utils.FeatureUtils
@@ -30,8 +33,7 @@ class ManageDataFragment : Hilt_ManageDataFragment() {
}
private val autoDeleteViewModel: AutoDeleteViewModel by activityViewModels()
- @Inject
- lateinit var featureUtils: FeatureUtils
+ @Inject lateinit var featureUtils: FeatureUtils
private val mAutoDeletePreference: HealthPreference? by lazy {
preferenceScreen.findPreference(AUTO_DELETE_PREFERENCE_KEY)
@@ -58,7 +60,12 @@ class ManageDataFragment : Hilt_ManageDataFragment() {
if (featureUtils.isNewAppPriorityEnabled()) {
mDataSourcesPreference?.logName = ManageDataElement.DATA_SOURCES_AND_PRIORITY_BUTTON
mDataSourcesPreference?.setOnPreferenceClickListener {
- findNavController().navigate(R.id.action_manageData_to_dataSources)
+ findNavController()
+ .navigate(
+ R.id.action_manageData_to_dataSources,
+ bundleOf(
+ HealthDataCategoriesFragment.CATEGORY_KEY to
+ HealthDataCategory.UNKNOWN))
true
}
} else {
@@ -96,13 +103,13 @@ class ManageDataFragment : Hilt_ManageDataFragment() {
AutoDeleteRange.AUTO_DELETE_RANGE_THREE_MONTHS -> {
val count = AutoDeleteRange.AUTO_DELETE_RANGE_THREE_MONTHS.numberOfMonths
MessageFormat.format(
- getString(R.string.range_after_x_months), mapOf("count" to count))
+ getString(R.string.range_after_x_months), mapOf("count" to count))
}
AutoDeleteRange.AUTO_DELETE_RANGE_EIGHTEEN_MONTHS -> {
val count = AutoDeleteRange.AUTO_DELETE_RANGE_EIGHTEEN_MONTHS.numberOfMonths
MessageFormat.format(
- getString(R.string.range_after_x_months), mapOf("count" to count))
+ getString(R.string.range_after_x_months), mapOf("count" to count))
}
}
}
-} \ No newline at end of file
+}
diff --git a/apk/src/com/android/healthconnect/controller/migration/AppUpdateRequiredFragment.kt b/apk/src/com/android/healthconnect/controller/migration/AppUpdateRequiredFragment.kt
index f92e872d..f5dcfc14 100644
--- a/apk/src/com/android/healthconnect/controller/migration/AppUpdateRequiredFragment.kt
+++ b/apk/src/com/android/healthconnect/controller/migration/AppUpdateRequiredFragment.kt
@@ -27,7 +27,8 @@ import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.android.healthconnect.controller.R
-import com.android.healthconnect.controller.utils.getAppStoreLink
+import com.android.healthconnect.controller.utils.AppStoreUtils
+import com.android.healthconnect.controller.utils.NavigationUtils
import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
import com.android.healthconnect.controller.utils.logging.MigrationElement
import com.android.healthconnect.controller.utils.logging.PageName
@@ -38,6 +39,8 @@ import javax.inject.Inject
class AppUpdateRequiredFragment : Hilt_AppUpdateRequiredFragment() {
@Inject lateinit var logger: HealthConnectLogger
+ @Inject lateinit var appStoreUtils: AppStoreUtils
+ @Inject lateinit var navigationUtils: NavigationUtils
companion object {
private const val TAG = "AppUpdateFragment"
@@ -83,8 +86,8 @@ class AppUpdateRequiredFragment : Hilt_AppUpdateRequiredFragment() {
try {
val packageName =
getString(resources.getIdentifier(HC_PACKAGE_NAME_CONFIG_NAME, null, null))
- val intent = getAppStoreLink(requireContext(), packageName)
- startActivity(intent!!)
+ val intent = appStoreUtils.getAppStoreLink(packageName)
+ navigationUtils.startActivity(this, intent!!)
} catch (exception: Exception) {
Log.e(TAG, "App store activity does not exist", exception)
Toast.makeText(requireContext(), R.string.default_error, Toast.LENGTH_SHORT).show()
@@ -103,8 +106,8 @@ class AppUpdateRequiredFragment : Hilt_AppUpdateRequiredFragment() {
putBoolean(getString(R.string.app_update_needed_seen), true)
apply()
}
- findNavController()
- .navigate(R.id.action_migrationAppUpdateNeededFragment_to_homeScreen)
+ navigationUtils.navigate(
+ this, R.id.action_migrationAppUpdateNeededFragment_to_homeScreen)
}
requireActivity().finish()
}
diff --git a/apk/src/com/android/healthconnect/controller/migration/MigrationNavigationFragment.kt b/apk/src/com/android/healthconnect/controller/migration/MigrationNavigationFragment.kt
index 07433568..ca7213ae 100644
--- a/apk/src/com/android/healthconnect/controller/migration/MigrationNavigationFragment.kt
+++ b/apk/src/com/android/healthconnect/controller/migration/MigrationNavigationFragment.kt
@@ -3,28 +3,26 @@ package com.android.healthconnect.controller.migration
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
-import android.view.LayoutInflater
import android.view.View
-import android.view.ViewGroup
import androidx.fragment.app.viewModels
-import androidx.navigation.fragment.findNavController
import com.android.healthconnect.controller.R
import com.android.healthconnect.controller.migration.api.MigrationState
import com.android.healthconnect.controller.shared.preference.HealthPreferenceFragment
+import com.android.healthconnect.controller.utils.NavigationUtils
import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
@AndroidEntryPoint(HealthPreferenceFragment::class)
class MigrationNavigationFragment : Hilt_MigrationNavigationFragment() {
+ @Inject lateinit var navigationUtils: NavigationUtils
+
private val migrationViewModel: MigrationViewModel by viewModels()
private lateinit var sharedPreference: SharedPreferences
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View? {
- return inflater.inflate(R.layout.fragment_migration_navigation, container, false)
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+ super.onCreatePreferences(savedInstanceState, rootKey)
+ setPreferencesFromResource(R.xml.empty_preference_screen, rootKey)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -75,28 +73,27 @@ class MigrationNavigationFragment : Hilt_MigrationNavigationFragment() {
}
private fun showInProgressFragment() {
- findNavController()
- .navigate(R.id.action_migrationNavigationFragment_to_migrationInProgressFragment)
+ navigationUtils.navigate(
+ this, R.id.action_migrationNavigationFragment_to_migrationInProgressFragment)
}
private fun showAppUpdateRequiredFragment() {
- findNavController()
- .navigate(R.id.action_migrationNavigationFragment_to_migrationAppUpdateNeededFragment)
+ navigationUtils.navigate(
+ this, R.id.action_migrationNavigationFragment_to_migrationAppUpdateNeededFragment)
}
private fun showModuleUpdateRequiredFragment() {
- findNavController()
- .navigate(
- R.id.action_migrationNavigationFragment_to_migrationModuleUpdateNeededFragment)
+ navigationUtils.navigate(
+ this, R.id.action_migrationNavigationFragment_to_migrationModuleUpdateNeededFragment)
}
private fun showMigrationPausedFragment() {
- findNavController()
- .navigate(R.id.action_migrationNavigationFragment_to_migrationPausedFragment)
+ navigationUtils.navigate(
+ this, R.id.action_migrationNavigationFragment_to_migrationPausedFragment)
}
private fun navigateToHomeFragment() {
- findNavController().navigate(R.id.action_migrationNavigationFragment_to_homeFragment)
+ navigationUtils.navigate(this, R.id.action_migrationNavigationFragment_to_homeFragment)
}
private fun markMigrationComplete() {
diff --git a/apk/src/com/android/healthconnect/controller/migration/MigrationPausedFragment.kt b/apk/src/com/android/healthconnect/controller/migration/MigrationPausedFragment.kt
index e98a57e5..d55ca6fd 100644
--- a/apk/src/com/android/healthconnect/controller/migration/MigrationPausedFragment.kt
+++ b/apk/src/com/android/healthconnect/controller/migration/MigrationPausedFragment.kt
@@ -24,8 +24,8 @@ import android.view.ViewGroup
import android.widget.Button
import android.widget.Toast
import androidx.fragment.app.Fragment
-import androidx.navigation.fragment.findNavController
import com.android.healthconnect.controller.R
+import com.android.healthconnect.controller.utils.NavigationUtils
import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
import com.android.healthconnect.controller.utils.logging.MigrationElement
import com.android.healthconnect.controller.utils.logging.PageName
@@ -36,6 +36,7 @@ import javax.inject.Inject
class MigrationPausedFragment : Hilt_MigrationPausedFragment() {
@Inject lateinit var logger: HealthConnectLogger
+ @Inject lateinit var navigationUtils: NavigationUtils
companion object {
private const val TAG = "MigrationPausedFragment"
@@ -60,7 +61,7 @@ class MigrationPausedFragment : Hilt_MigrationPausedFragment() {
resumeButton.setOnClickListener {
logger.logInteraction(MigrationElement.MIGRATION_PAUSED_CONTINUE_BUTTON)
try {
- findNavController().navigate(R.id.action_migrationPausedFragment_to_migrationApk)
+ navigationUtils.navigate(this, R.id.action_migrationPausedFragment_to_migrationApk)
} catch (exception: Exception) {
Log.e(TAG, "Migration APK does not exist", exception)
Toast.makeText(requireContext(), R.string.default_error, Toast.LENGTH_SHORT).show()
@@ -70,21 +71,19 @@ class MigrationPausedFragment : Hilt_MigrationPausedFragment() {
cancelButton.setOnClickListener {
logger.logInteraction(MigrationElement.MIGRATION_UPDATE_NEEDED_CANCEL_BUTTON)
val sharedPreferences =
- requireActivity()
- .getSharedPreferences("USER_ACTIVITY_TRACKER", Context.MODE_PRIVATE)
+ requireActivity()
+ .getSharedPreferences("USER_ACTIVITY_TRACKER", Context.MODE_PRIVATE)
val integrationPausedSeen =
- sharedPreferences.getBoolean(INTEGRATION_PAUSED_SEEN_KEY, false)
+ sharedPreferences.getBoolean(INTEGRATION_PAUSED_SEEN_KEY, false)
if (!integrationPausedSeen) {
sharedPreferences.edit().apply {
putBoolean(INTEGRATION_PAUSED_SEEN_KEY, true)
apply()
}
- findNavController()
- .navigate(R.id.action_migrationPausedFragment_to_homeScreen)
+ navigationUtils.navigate(this, R.id.action_migrationPausedFragment_to_homeScreen)
}
requireActivity().finish()
}
-
}
override fun onResume() {
diff --git a/apk/src/com/android/healthconnect/controller/migration/ModuleUpdateRequiredFragment.kt b/apk/src/com/android/healthconnect/controller/migration/ModuleUpdateRequiredFragment.kt
index c713f28f..b5f144b5 100644
--- a/apk/src/com/android/healthconnect/controller/migration/ModuleUpdateRequiredFragment.kt
+++ b/apk/src/com/android/healthconnect/controller/migration/ModuleUpdateRequiredFragment.kt
@@ -27,6 +27,7 @@ import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.android.healthconnect.controller.R
+import com.android.healthconnect.controller.utils.NavigationUtils
import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
import com.android.healthconnect.controller.utils.logging.MigrationElement
import com.android.healthconnect.controller.utils.logging.PageName
@@ -37,6 +38,7 @@ import javax.inject.Inject
class ModuleUpdateRequiredFragment : Hilt_ModuleUpdateRequiredFragment() {
@Inject lateinit var logger: HealthConnectLogger
+ @Inject lateinit var navigationUtils: NavigationUtils
companion object {
private const val TAG = "ModuleUpdateRequiredFragment"
@@ -76,9 +78,8 @@ class ModuleUpdateRequiredFragment : Hilt_ModuleUpdateRequiredFragment() {
updateButton.setOnClickListener {
logger.logInteraction(MigrationElement.MIGRATION_UPDATE_NEEDED_UPDATE_BUTTON)
try {
- findNavController()
- .navigate(
- R.id.action_migrationModuleUpdateNeededFragment_to_systemUpdateActivity)
+ navigationUtils.navigate(
+ this, R.id.action_migrationModuleUpdateNeededFragment_to_systemUpdateActivity)
} catch (exception: Exception) {
Log.e(TAG, "System update activity does not exist", exception)
Toast.makeText(requireContext(), R.string.default_error, Toast.LENGTH_SHORT).show()
@@ -98,8 +99,8 @@ class ModuleUpdateRequiredFragment : Hilt_ModuleUpdateRequiredFragment() {
putBoolean(getString(R.string.module_update_needed_seen), true)
apply()
}
- findNavController()
- .navigate(R.id.action_migrationModuleUpdateNeededFragment_to_homeScreen)
+ navigationUtils.navigate(
+ this, R.id.action_migrationModuleUpdateNeededFragment_to_homeScreen)
}
requireActivity().finish()
diff --git a/apk/src/com/android/healthconnect/controller/onboarding/OnboardingActivity.kt b/apk/src/com/android/healthconnect/controller/onboarding/OnboardingActivity.kt
index 6bf4ae38..3f7528d1 100644
--- a/apk/src/com/android/healthconnect/controller/onboarding/OnboardingActivity.kt
+++ b/apk/src/com/android/healthconnect/controller/onboarding/OnboardingActivity.kt
@@ -2,6 +2,7 @@ package com.android.healthconnect.controller.onboarding
import android.app.Activity
import android.content.Context
+import android.content.Intent
import android.os.Bundle
import android.widget.Button
import androidx.fragment.app.FragmentActivity
@@ -22,7 +23,7 @@ class OnboardingActivity : Hilt_OnboardingActivity() {
@VisibleForTesting const val USER_ACTIVITY_TRACKER = "USER_ACTIVITY_TRACKER"
@VisibleForTesting const val ONBOARDING_SHOWN_PREF_KEY = "ONBOARDING_SHOWN_PREF_KEY"
- fun maybeRedirectToOnboardingActivity(activity: Activity): Boolean {
+ fun shouldRedirectToOnboardingActivity(activity: Activity): Boolean {
val sharedPreference =
activity.getSharedPreferences(USER_ACTIVITY_TRACKER, Context.MODE_PRIVATE)
val previouslyOpened = sharedPreference.getBoolean(ONBOARDING_SHOWN_PREF_KEY, false)
@@ -31,6 +32,11 @@ class OnboardingActivity : Hilt_OnboardingActivity() {
}
return false
}
+
+ fun createIntent(context: Context): Intent {
+ return Intent(context, OnboardingActivity::class.java)
+ .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ }
}
@Inject lateinit var logger: HealthConnectLogger
diff --git a/apk/src/com/android/healthconnect/controller/onboarding/OnboardingActivityContract.kt b/apk/src/com/android/healthconnect/controller/onboarding/OnboardingActivityContract.kt
deleted file mode 100644
index 70c883d7..00000000
--- a/apk/src/com/android/healthconnect/controller/onboarding/OnboardingActivityContract.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.android.healthconnect.controller.onboarding
-
-import android.app.Activity
-import android.content.Context
-import android.content.Intent
-import androidx.activity.result.contract.ActivityResultContract
-
-class OnboardingActivityContract : ActivityResultContract<Int, String?>() {
-
- companion object {
- const val INTENT_RESULT_CANCELLED = "CANCELLED"
- }
-
- override fun createIntent(context: Context, input: Int): Intent {
- return Intent(context, OnboardingActivity::class.java)
- .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
- }
-
- override fun parseResult(resultCode: Int, intent: Intent?): String? {
- return when (resultCode) {
- Activity.RESULT_CANCELED -> INTENT_RESULT_CANCELLED
- else -> null
- }
- }
-}
diff --git a/apk/src/com/android/healthconnect/controller/permissions/api/GetGrantedHealthPermissionsUseCase.kt b/apk/src/com/android/healthconnect/controller/permissions/api/GetGrantedHealthPermissionsUseCase.kt
index 9b0ae076..75420417 100644
--- a/apk/src/com/android/healthconnect/controller/permissions/api/GetGrantedHealthPermissionsUseCase.kt
+++ b/apk/src/com/android/healthconnect/controller/permissions/api/GetGrantedHealthPermissionsUseCase.kt
@@ -23,11 +23,13 @@ import javax.inject.Singleton
@Singleton
class GetGrantedHealthPermissionsUseCase
@Inject
-constructor(private val healthPermissionManager: HealthPermissionManager) {
+constructor(private val healthPermissionManager: HealthPermissionManager) :
+ IGetGrantedHealthPermissionsUseCase {
companion object {
private const val TAG = "GetGrantedHealthPermiss"
}
- operator fun invoke(packageName: String): List<String> {
+
+ override operator fun invoke(packageName: String): List<String> {
return try {
healthPermissionManager.getGrantedHealthPermissions(packageName)
} catch (ex: Exception) {
@@ -36,3 +38,7 @@ constructor(private val healthPermissionManager: HealthPermissionManager) {
}
}
}
+
+interface IGetGrantedHealthPermissionsUseCase {
+ operator fun invoke(packageName: String): List<String>
+}
diff --git a/apk/src/com/android/healthconnect/controller/permissions/api/LoadAccessDateUseCase.kt b/apk/src/com/android/healthconnect/controller/permissions/api/LoadAccessDateUseCase.kt
index ca6aad37..89e601f2 100644
--- a/apk/src/com/android/healthconnect/controller/permissions/api/LoadAccessDateUseCase.kt
+++ b/apk/src/com/android/healthconnect/controller/permissions/api/LoadAccessDateUseCase.kt
@@ -32,7 +32,7 @@ constructor(private val healthPermissionManager: HealthPermissionManager) {
return try {
healthPermissionManager.loadStartAccessDate(it)
} catch (ex: Exception) {
- Log.e(TAG, "GetGrantedHealthPermissionsUseCase.invoke", ex)
+ Log.e(TAG, "LoadStartAccessDate failed", ex)
null
}
}
diff --git a/apk/src/com/android/healthconnect/controller/permissions/request/PermissionsActivity.kt b/apk/src/com/android/healthconnect/controller/permissions/request/PermissionsActivity.kt
index 9dcccfd5..066c5695 100644
--- a/apk/src/com/android/healthconnect/controller/permissions/request/PermissionsActivity.kt
+++ b/apk/src/com/android/healthconnect/controller/permissions/request/PermissionsActivity.kt
@@ -18,7 +18,7 @@
package com.android.healthconnect.controller.permissions.request
-import android.app.Activity
+import android.app.Activity.*
import android.content.Intent
import android.content.Intent.EXTRA_PACKAGE_NAME
import android.content.pm.PackageManager
@@ -27,6 +27,7 @@ import android.content.pm.PackageManager.EXTRA_REQUEST_PERMISSIONS_RESULTS
import android.os.Bundle
import android.util.Log
import android.view.View
+import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.activity.viewModels
import androidx.fragment.app.FragmentActivity
import com.android.healthconnect.controller.R
@@ -35,9 +36,8 @@ import com.android.healthconnect.controller.migration.MigrationActivity.Companio
import com.android.healthconnect.controller.migration.MigrationActivity.Companion.showMigrationPendingDialog
import com.android.healthconnect.controller.migration.MigrationViewModel
import com.android.healthconnect.controller.migration.api.MigrationState
-import com.android.healthconnect.controller.onboarding.OnboardingActivity.Companion.maybeRedirectToOnboardingActivity
-import com.android.healthconnect.controller.onboarding.OnboardingActivityContract
-import com.android.healthconnect.controller.onboarding.OnboardingActivityContract.Companion.INTENT_RESULT_CANCELLED
+import com.android.healthconnect.controller.onboarding.OnboardingActivity
+import com.android.healthconnect.controller.onboarding.OnboardingActivity.Companion.shouldRedirectToOnboardingActivity
import com.android.healthconnect.controller.permissions.data.HealthPermission
import com.android.healthconnect.controller.permissions.data.PermissionState
import com.android.healthconnect.controller.shared.HealthPermissionReader
@@ -57,10 +57,20 @@ class PermissionsActivity : Hilt_PermissionsActivity() {
}
@Inject lateinit var logger: HealthConnectLogger
- private val viewModel: RequestPermissionViewModel by viewModels()
- private val migrationViewModel: MigrationViewModel by viewModels()
+
@Inject lateinit var healthPermissionReader: HealthPermissionReader
+ private val requestPermissionsViewModel: RequestPermissionViewModel by viewModels()
+
+ private val migrationViewModel: MigrationViewModel by viewModels()
+
+ private val openOnboardingActivity =
+ registerForActivityResult(StartActivityForResult()) { result ->
+ if (result.resultCode == RESULT_CANCELED) {
+ finish()
+ }
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -75,8 +85,8 @@ class PermissionsActivity : Hilt_PermissionsActivity() {
return
}
- if (maybeRedirectToOnboardingActivity(this) && savedInstanceState == null) {
- openOnboardingActivity.launch(1)
+ if (savedInstanceState == null && shouldRedirectToOnboardingActivity(this)) {
+ openOnboardingActivity.launch(OnboardingActivity.createIntent(this))
}
val rationalIntentDeclared =
@@ -86,10 +96,10 @@ class PermissionsActivity : Hilt_PermissionsActivity() {
finish()
}
- viewModel.init(getPackageNameExtra(), getPermissionStrings())
- viewModel.permissionsList.observe(this) { notGrantedPermissions ->
+ requestPermissionsViewModel.init(getPackageNameExtra(), getPermissionStrings())
+ requestPermissionsViewModel.permissionsList.observe(this) { notGrantedPermissions ->
if (notGrantedPermissions.isEmpty()) {
- handleResults(viewModel.request(getPackageNameExtra()))
+ handleResults(requestPermissionsViewModel.request(getPackageNameExtra()))
}
}
migrationViewModel.migrationState.observe(this) { migrationState ->
@@ -124,8 +134,8 @@ class PermissionsActivity : Hilt_PermissionsActivity() {
cancelButton.setOnClickListener {
logger.logInteraction(PermissionsElement.CANCEL_PERMISSIONS_BUTTON)
- viewModel.updatePermissions(false)
- handleResults(viewModel.request(getPackageNameExtra()))
+ requestPermissionsViewModel.updatePermissions(false)
+ handleResults(requestPermissionsViewModel.request(getPackageNameExtra()))
}
}
@@ -136,12 +146,12 @@ class PermissionsActivity : Hilt_PermissionsActivity() {
val parentView = allowButton.parent.parent as View
increaseViewTouchTargetSize(this, allowButton, parentView)
- viewModel.grantedPermissions.observe(this) { grantedPermissions ->
+ requestPermissionsViewModel.grantedPermissions.observe(this) { grantedPermissions ->
allowButton.isEnabled = grantedPermissions.isNotEmpty()
}
allowButton.setOnClickListener {
logger.logInteraction(PermissionsElement.ALLOW_PERMISSIONS_BUTTON)
- handleResults(viewModel.request(getPackageNameExtra()))
+ handleResults(requestPermissionsViewModel.request(getPackageNameExtra()))
}
}
@@ -152,7 +162,7 @@ class PermissionsActivity : Hilt_PermissionsActivity() {
this,
getString(
R.string.migration_in_progress_permissions_dialog_content,
- viewModel.appMetadata.value?.appName)) { _, _ ->
+ requestPermissionsViewModel.appMetadata.value?.appName)) { _, _ ->
finish()
}
}
@@ -164,11 +174,11 @@ class PermissionsActivity : Hilt_PermissionsActivity() {
this,
getString(
R.string.migration_pending_permissions_dialog_content,
- viewModel.appMetadata.value?.appName),
+ requestPermissionsViewModel.appMetadata.value?.appName),
null,
) { _, _ ->
- viewModel.updatePermissions(false)
- handleResults(viewModel.request(getPackageNameExtra()))
+ requestPermissionsViewModel.updatePermissions(false)
+ handleResults(requestPermissionsViewModel.request(getPackageNameExtra()))
finish()
}
}
@@ -195,7 +205,7 @@ class PermissionsActivity : Hilt_PermissionsActivity() {
val result = Intent()
result.putExtra(EXTRA_REQUEST_PERMISSIONS_NAMES, getPermissionStrings())
result.putExtra(EXTRA_REQUEST_PERMISSIONS_RESULTS, grants)
- setResult(Activity.RESULT_OK, result)
+ setResult(RESULT_OK, result)
finish()
}
@@ -206,11 +216,4 @@ class PermissionsActivity : Hilt_PermissionsActivity() {
private fun getPackageNameExtra(): String {
return intent.getStringExtra(EXTRA_PACKAGE_NAME).orEmpty()
}
-
- val openOnboardingActivity =
- registerForActivityResult(OnboardingActivityContract()) { result ->
- if (result == INTENT_RESULT_CANCELLED) {
- finish()
- }
- }
}
diff --git a/apk/src/com/android/healthconnect/controller/permissions/shared/SettingsActivity.kt b/apk/src/com/android/healthconnect/controller/permissions/shared/SettingsActivity.kt
index 7a4561a7..63f226f6 100644
--- a/apk/src/com/android/healthconnect/controller/permissions/shared/SettingsActivity.kt
+++ b/apk/src/com/android/healthconnect/controller/permissions/shared/SettingsActivity.kt
@@ -35,15 +35,15 @@ package com.android.healthconnect.controller.permissions.shared
import android.content.Intent.EXTRA_PACKAGE_NAME
import android.os.Bundle
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.os.bundleOf
import androidx.navigation.Navigation
import androidx.navigation.findNavController
import com.android.healthconnect.controller.R
import com.android.healthconnect.controller.navigation.DestinationChangedListener
-import com.android.healthconnect.controller.onboarding.OnboardingActivity.Companion.maybeRedirectToOnboardingActivity
-import com.android.healthconnect.controller.onboarding.OnboardingActivityContract
-import com.android.healthconnect.controller.onboarding.OnboardingActivityContract.Companion.INTENT_RESULT_CANCELLED
+import com.android.healthconnect.controller.onboarding.OnboardingActivity
+import com.android.healthconnect.controller.onboarding.OnboardingActivity.Companion.shouldRedirectToOnboardingActivity
import com.android.healthconnect.controller.permissions.app.AppPermissionViewModel
import com.android.healthconnect.controller.shared.HealthPermissionReader
import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity
@@ -53,20 +53,24 @@ import javax.inject.Inject
@AndroidEntryPoint(CollapsingToolbarBaseActivity::class)
class SettingsActivity : Hilt_SettingsActivity() {
- companion object {
- private const val TAG = "SettingsActivity"
- }
-
@Inject lateinit var healthPermissionReader: HealthPermissionReader
+
private val viewModel: AppPermissionViewModel by viewModels()
+ private val openOnboardingActivity =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ if (result.resultCode == RESULT_CANCELED) {
+ finish()
+ }
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
setTitle(R.string.permgrouplab_health)
- if (maybeRedirectToOnboardingActivity(this) && savedInstanceState == null) {
- openOnboardingActivity.launch(1)
+ if (savedInstanceState == null && shouldRedirectToOnboardingActivity(this)) {
+ openOnboardingActivity.launch(OnboardingActivity.createIntent(this))
}
}
@@ -74,8 +78,7 @@ class SettingsActivity : Hilt_SettingsActivity() {
super.onStart()
if (intent.hasExtra(EXTRA_PACKAGE_NAME)) {
- val packageName = intent.getStringExtra(
- EXTRA_PACKAGE_NAME)!!
+ val packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME)!!
viewModel.shouldNavigateToFragment.observe(this) { shouldNavigate ->
maybeNavigateToFragment(shouldNavigate)
@@ -83,7 +86,6 @@ class SettingsActivity : Hilt_SettingsActivity() {
viewModel.loadShouldNavigateToFragment(packageName)
}
-
}
private fun maybeNavigateToFragment(shouldNavigate: Boolean) {
@@ -93,8 +95,7 @@ class SettingsActivity : Hilt_SettingsActivity() {
navController.navigate(
R.id.action_deeplink_to_settingsManageAppPermissionsFragment,
bundleOf(EXTRA_PACKAGE_NAME to intent.getStringExtra(EXTRA_PACKAGE_NAME)))
- }
- else {
+ } else {
finish()
}
}
@@ -113,11 +114,4 @@ class SettingsActivity : Hilt_SettingsActivity() {
}
return true
}
-
- val openOnboardingActivity =
- registerForActivityResult(OnboardingActivityContract()) { result ->
- if (result == INTENT_RESULT_CANCELLED) {
- finish()
- }
- }
}
diff --git a/apk/src/com/android/healthconnect/controller/permissiontypes/HealthPermissionTypesFragment.kt b/apk/src/com/android/healthconnect/controller/permissiontypes/HealthPermissionTypesFragment.kt
index b550616a..c20c5512 100644
--- a/apk/src/com/android/healthconnect/controller/permissiontypes/HealthPermissionTypesFragment.kt
+++ b/apk/src/com/android/healthconnect/controller/permissiontypes/HealthPermissionTypesFragment.kt
@@ -78,8 +78,7 @@ open class HealthPermissionTypesFragment : Hilt_HealthPermissionTypesFragment()
}
@Inject lateinit var logger: HealthConnectLogger
- @Inject
- lateinit var featureUtils: FeatureUtils
+ @Inject lateinit var featureUtils: FeatureUtils
@HealthDataCategoryInt private var category: Int = 0
@@ -188,14 +187,45 @@ open class HealthPermissionTypesFragment : Hilt_HealthPermissionTypesFragment()
mManageDataCategory?.removePreferenceRecursively(APP_PRIORITY_BUTTON)
}
is HealthPermissionTypesViewModel.PriorityListState.WithData -> {
- updatePriorityButton(state.priorityList)
+ updateOldPriorityButton(state.priorityList)
}
}
}
+ } else {
+ // Add the new priority list button
+ updateNewPriorityButton()
}
}
- private fun updatePriorityButton(priorityList: List<AppMetadata>) {
+ private fun updateNewPriorityButton() {
+ mManageDataCategory?.removePreferenceRecursively(APP_PRIORITY_BUTTON)
+
+ // Only display the priority button for Activity and Sleep categories
+ if (category !in setOf(HealthDataCategory.ACTIVITY, HealthDataCategory.SLEEP)) {
+ return
+ }
+
+ val newPriorityButton =
+ HealthPreference(requireContext()).also {
+ it.title = resources.getString(R.string.data_sources_and_priority_title)
+ it.icon = AttributeResolver.getDrawable(requireContext(), R.attr.appPriorityIcon)
+ it.logName = PermissionTypesElement.DATA_SOURCES_AND_PRIORITY_BUTTON
+ it.key = APP_PRIORITY_BUTTON
+ it.order = 4
+ it.setOnPreferenceClickListener {
+ // Navigate to the data sources fragment
+ findNavController()
+ .navigate(
+ R.id.action_healthPermissionTypes_to_dataSourcesAndPriority,
+ bundleOf(CATEGORY_KEY to category))
+ true
+ }
+ }
+
+ mManageDataCategory?.addPreference(newPriorityButton)
+ }
+
+ private fun updateOldPriorityButton(priorityList: List<AppMetadata>) {
mManageDataCategory?.removePreferenceRecursively(APP_PRIORITY_BUTTON)
// Only display the priority button for Activity and Sleep categories
@@ -210,8 +240,7 @@ open class HealthPermissionTypesFragment : Hilt_HealthPermissionTypesFragment()
val appPriorityButton =
HealthPreference(requireContext()).also {
it.title = resources.getString(R.string.app_priority_button)
- it.icon =
- AttributeResolver.getDrawable(requireContext(), R.attr.appPriorityIcon)
+ it.icon = AttributeResolver.getDrawable(requireContext(), R.attr.appPriorityIcon)
it.logName = PermissionTypesElement.SET_APP_PRIORITY_BUTTON
it.summary = priorityList.first().appName
it.key = APP_PRIORITY_BUTTON
diff --git a/apk/src/com/android/healthconnect/controller/service/UseCaseModule.kt b/apk/src/com/android/healthconnect/controller/service/UseCaseModule.kt
index ebe43643..5d18819a 100644
--- a/apk/src/com/android/healthconnect/controller/service/UseCaseModule.kt
+++ b/apk/src/com/android/healthconnect/controller/service/UseCaseModule.kt
@@ -16,6 +16,10 @@
package com.android.healthconnect.controller.service
import android.health.connect.HealthConnectManager
+import com.android.healthconnect.controller.data.access.ILoadAccessUseCase
+import com.android.healthconnect.controller.data.access.ILoadPermissionTypeContributorAppsUseCase
+import com.android.healthconnect.controller.data.access.LoadAccessUseCase
+import com.android.healthconnect.controller.data.access.LoadPermissionTypeContributorAppsUseCase
import com.android.healthconnect.controller.data.entries.api.ILoadDataAggregationsUseCase
import com.android.healthconnect.controller.data.entries.api.ILoadDataEntriesUseCase
import com.android.healthconnect.controller.data.entries.api.ILoadMenstruationDataUseCase
@@ -23,19 +27,26 @@ import com.android.healthconnect.controller.data.entries.api.LoadDataAggregation
import com.android.healthconnect.controller.data.entries.api.LoadDataEntriesUseCase
import com.android.healthconnect.controller.data.entries.api.LoadEntriesHelper
import com.android.healthconnect.controller.data.entries.api.LoadMenstruationDataUseCase
-import com.android.healthconnect.controller.data.entries.api.LoadSleepDataUseCase
import com.android.healthconnect.controller.dataentries.formatters.DistanceFormatter
import com.android.healthconnect.controller.dataentries.formatters.MenstruationPeriodFormatter
import com.android.healthconnect.controller.dataentries.formatters.SleepSessionFormatter
import com.android.healthconnect.controller.dataentries.formatters.StepsFormatter
import com.android.healthconnect.controller.dataentries.formatters.TotalCaloriesBurnedFormatter
+import com.android.healthconnect.controller.datasources.api.ILoadLastDateWithPriorityDataUseCase
import com.android.healthconnect.controller.datasources.api.ILoadMostRecentAggregationsUseCase
import com.android.healthconnect.controller.datasources.api.ILoadPotentialPriorityListUseCase
+import com.android.healthconnect.controller.datasources.api.ILoadPriorityEntriesUseCase
+import com.android.healthconnect.controller.datasources.api.ISleepSessionHelper
import com.android.healthconnect.controller.datasources.api.IUpdatePriorityListUseCase
+import com.android.healthconnect.controller.datasources.api.LoadLastDateWithPriorityDataUseCase
import com.android.healthconnect.controller.datasources.api.LoadMostRecentAggregationsUseCase
import com.android.healthconnect.controller.datasources.api.LoadPotentialPriorityListUseCase
+import com.android.healthconnect.controller.datasources.api.LoadPriorityEntriesUseCase
+import com.android.healthconnect.controller.datasources.api.SleepSessionHelper
import com.android.healthconnect.controller.datasources.api.UpdatePriorityListUseCase
import com.android.healthconnect.controller.permissions.api.GetGrantedHealthPermissionsUseCase
+import com.android.healthconnect.controller.permissions.api.HealthPermissionManager
+import com.android.healthconnect.controller.permissions.api.IGetGrantedHealthPermissionsUseCase
import com.android.healthconnect.controller.permissions.connectedapps.ILoadHealthPermissionApps
import com.android.healthconnect.controller.permissions.connectedapps.LoadHealthPermissionApps
import com.android.healthconnect.controller.permissions.shared.QueryRecentAccessLogsUseCase
@@ -125,13 +136,33 @@ class UseCaseModule {
@Provides
fun providesMostRecentAggregationsUseCase(
- healthConnectManager: HealthConnectManager,
loadDataAggregationsUseCase: LoadDataAggregationsUseCase,
- sleepDataUseCase: LoadSleepDataUseCase,
+ loadLastDateWithPriorityDataUseCase: LoadLastDateWithPriorityDataUseCase,
+ sleepSessionHelper: SleepSessionHelper,
@IoDispatcher dispatcher: CoroutineDispatcher
): ILoadMostRecentAggregationsUseCase {
return LoadMostRecentAggregationsUseCase(
- healthConnectManager, loadDataAggregationsUseCase, sleepDataUseCase, dispatcher)
+ loadDataAggregationsUseCase,
+ loadLastDateWithPriorityDataUseCase,
+ sleepSessionHelper,
+ dispatcher)
+ }
+
+ @Provides
+ fun providesSleepSessionHelper(
+ loadPriorityEntriesUseCase: LoadPriorityEntriesUseCase,
+ @IoDispatcher dispatcher: CoroutineDispatcher
+ ): ISleepSessionHelper {
+ return SleepSessionHelper(loadPriorityEntriesUseCase, dispatcher)
+ }
+
+ @Provides
+ fun providesLoadPriorityEntriesUseCase(
+ loadEntriesHelper: LoadEntriesHelper,
+ loadPriorityListUseCase: LoadPriorityListUseCase,
+ @IoDispatcher dispatcher: CoroutineDispatcher
+ ): ILoadPriorityEntriesUseCase {
+ return LoadPriorityEntriesUseCase(loadEntriesHelper, loadPriorityListUseCase, dispatcher)
}
@Provides
@@ -153,6 +184,22 @@ class UseCaseModule {
}
@Provides
+ fun providesLoadLastDateWithPriorityDataUseCase(
+ healthConnectManager: HealthConnectManager,
+ loadEntriesHelper: LoadEntriesHelper,
+ loadPriorityListUseCase: LoadPriorityListUseCase,
+ timeSource: TimeSource,
+ @IoDispatcher dispatcher: CoroutineDispatcher
+ ): ILoadLastDateWithPriorityDataUseCase {
+ return LoadLastDateWithPriorityDataUseCase(
+ healthConnectManager,
+ loadEntriesHelper,
+ loadPriorityListUseCase,
+ timeSource,
+ dispatcher)
+ }
+
+ @Provides
fun providesPriorityListUseCase(
appInfoReader: AppInfoReader,
healthConnectManager: HealthConnectManager,
@@ -168,4 +215,37 @@ class UseCaseModule {
): IUpdatePriorityListUseCase {
return UpdatePriorityListUseCase(healthConnectManager, dispatcher)
}
+
+ @Provides
+ fun providesLoadAccessUseCase(
+ loadPermissionTypeContributorAppsUseCase: ILoadPermissionTypeContributorAppsUseCase,
+ loadGrantedHealthPermissionsUseCase: IGetGrantedHealthPermissionsUseCase,
+ healthPermissionReader: HealthPermissionReader,
+ appInfoReader: AppInfoReader,
+ @IoDispatcher dispatcher: CoroutineDispatcher
+ ): ILoadAccessUseCase {
+ return LoadAccessUseCase(
+ loadPermissionTypeContributorAppsUseCase,
+ loadGrantedHealthPermissionsUseCase,
+ healthPermissionReader,
+ appInfoReader,
+ dispatcher)
+ }
+
+ @Provides
+ fun providesLoadPermissionTypeContributorAppsUseCase(
+ appInfoReader: AppInfoReader,
+ healthConnectManager: HealthConnectManager,
+ @IoDispatcher dispatcher: CoroutineDispatcher
+ ): ILoadPermissionTypeContributorAppsUseCase {
+ return LoadPermissionTypeContributorAppsUseCase(
+ appInfoReader, healthConnectManager, dispatcher)
+ }
+
+ @Provides
+ fun providesGetGrantedHealthPermissionsUseCase(
+ healthPermissionManager: HealthPermissionManager
+ ): IGetGrantedHealthPermissionsUseCase {
+ return GetGrantedHealthPermissionsUseCase(healthPermissionManager)
+ }
}
diff --git a/apk/src/com/android/healthconnect/controller/shared/preference/AggregationDataCard.kt b/apk/src/com/android/healthconnect/controller/shared/preference/AggregationDataCard.kt
index 719e806a..26cb5106 100644
--- a/apk/src/com/android/healthconnect/controller/shared/preference/AggregationDataCard.kt
+++ b/apk/src/com/android/healthconnect/controller/shared/preference/AggregationDataCard.kt
@@ -28,7 +28,9 @@ import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.
import com.android.healthconnect.controller.utils.LocalDateTimeFormatter
import com.android.healthconnect.controller.utils.SystemTimeSource
import com.android.healthconnect.controller.utils.TimeSource
+import com.android.healthconnect.controller.utils.toLocalTime
import java.time.Instant
+import java.time.LocalTime
/** A custom card to display the latest available data aggregations. */
class AggregationDataCard
@@ -122,11 +124,18 @@ constructor(
private fun formatDateText(startDate: Instant, endDate: Instant?): String {
return if (endDate != null) {
+ var localEndDate: Instant = endDate
+
+ // If endDate is midnight, add one millisecond so that DateUtils
+ // correctly formats it as a separate date.
+ if (endDate.toLocalTime() == LocalTime.MIDNIGHT) {
+ localEndDate = endDate.plusMillis(1)
+ }
// display date range
- if (isLessThanOneYearAgo(startDate) && isLessThanOneYearAgo(endDate)) {
- dateFormatter.formatDateRangeWithoutYear(startDate, endDate)
+ if (isLessThanOneYearAgo(startDate) && isLessThanOneYearAgo(localEndDate)) {
+ dateFormatter.formatDateRangeWithoutYear(startDate, localEndDate)
} else {
- dateFormatter.formatDateRangeWithYear(startDate, endDate)
+ dateFormatter.formatDateRangeWithYear(startDate, localEndDate)
}
} else {
// display only one date
diff --git a/apk/src/com/android/healthconnect/controller/shared/preference/CardContainerPreference.kt b/apk/src/com/android/healthconnect/controller/shared/preference/CardContainerPreference.kt
index 0e6034ec..9bf17be6 100644
--- a/apk/src/com/android/healthconnect/controller/shared/preference/CardContainerPreference.kt
+++ b/apk/src/com/android/healthconnect/controller/shared/preference/CardContainerPreference.kt
@@ -26,15 +26,26 @@ import com.android.healthconnect.controller.datasources.AggregationCardInfo
import com.android.healthconnect.controller.permissions.connectedapps.ComparablePreference
import com.android.healthconnect.controller.utils.SystemTimeSource
import com.android.healthconnect.controller.utils.TimeSource
+import com.android.healthconnect.controller.utils.logging.DataSourcesElement
+import com.android.healthconnect.controller.utils.logging.ElementName
+import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
+import com.android.healthconnect.controller.utils.logging.HealthConnectLoggerEntryPoint
+import dagger.hilt.android.EntryPointAccessors
-class CardContainerPreference constructor(
- context: Context,
- private val timeSource: TimeSource = SystemTimeSource
-): Preference(context), ComparablePreference {
+class CardContainerPreference
+constructor(context: Context, private val timeSource: TimeSource = SystemTimeSource) :
+ Preference(context), ComparablePreference {
+
+ private var logger: HealthConnectLogger
+ var logName: ElementName = DataSourcesElement.DATA_TOTALS_CARD
init {
layoutResource = R.layout.widget_card_preference
isSelectable = false
+ val hiltEntryPoint =
+ EntryPointAccessors.fromApplication(
+ context.applicationContext, HealthConnectLoggerEntryPoint::class.java)
+ logger = hiltEntryPoint.logger()
}
private val mAggregationCardInfo: MutableList<AggregationCardInfo> = mutableListOf()
@@ -52,7 +63,6 @@ class CardContainerPreference constructor(
// We display a max of 2 cards, so we take the first two list items
if (aggregationCardInfoList.size > 2) {
this.mAggregationCardInfo.addAll(aggregationCardInfoList.subList(0, 2))
-
} else {
this.mAggregationCardInfo.addAll(aggregationCardInfoList)
}
@@ -65,8 +75,7 @@ class CardContainerPreference constructor(
}
if (!isLoading) {
- holder?.let {
- onBindViewHolder(it) }
+ holder?.let { onBindViewHolder(it) }
} else {
// Get the current width and height on the card container so we don't flash the screen
val width = container?.width
@@ -77,9 +86,10 @@ class CardContainerPreference constructor(
progressBar =
layoutInflater.inflate(R.layout.widget_loading_preference, null) as ConstraintLayout
- val layoutParams = ConstraintLayout.LayoutParams(
- width ?: ConstraintLayout.LayoutParams.WRAP_CONTENT,
- height ?: ConstraintLayout.LayoutParams.WRAP_CONTENT)
+ val layoutParams =
+ ConstraintLayout.LayoutParams(
+ width ?: ConstraintLayout.LayoutParams.WRAP_CONTENT,
+ height ?: ConstraintLayout.LayoutParams.WRAP_CONTENT)
progressBar?.layoutParams = layoutParams
container?.addView(progressBar)
}
@@ -96,6 +106,7 @@ class CardContainerPreference constructor(
setLoading(true)
}
+ logger.logImpression(logName)
}
private fun setupCards() {
@@ -109,14 +120,13 @@ class CardContainerPreference constructor(
}
if (mAggregationCardInfo.size == 1) {
- addSingleLargeCard(mAggregationCardInfo[0])
- container?.removeView(progressBar)
+ val card = addSingleLargeCard(mAggregationCardInfo[0])
+ removeAllChildrenExcept(container, card)
} else {
// Add both types of cards to the container (they will be invisible)
val (firstSmallCard, secondSmallCard) =
- addTwoSmallCards(mAggregationCardInfo[0],
- mAggregationCardInfo[1])
+ addTwoSmallCards(mAggregationCardInfo[0], mAggregationCardInfo[1])
val (firstLargeCard, secondLargeCard) =
addTwoLargeCards(mAggregationCardInfo[0], mAggregationCardInfo[1])
@@ -153,31 +163,31 @@ class CardContainerPreference constructor(
}
/**
- * Adds a single large [AggregationDataCard] to the provided container.
- * This should be called when there is only one available aggregate.
+ * Adds a single large [AggregationDataCard] to the provided container. This should be called
+ * when there is only one available aggregate.
*/
- private fun addSingleLargeCard(cardInfo: AggregationCardInfo) {
- val singleCard = AggregationDataCard(
- context,
- null,
- AggregationDataCard.CardTypeEnum.LARGE_CARD,
- cardInfo,
- timeSource)
+ private fun addSingleLargeCard(cardInfo: AggregationCardInfo): AggregationDataCard {
+ val singleCard =
+ AggregationDataCard(
+ context, null, AggregationDataCard.CardTypeEnum.LARGE_CARD, cardInfo, timeSource)
singleCard.id = View.generateViewId()
- val layoutParams = ConstraintLayout.LayoutParams(
+ val layoutParams =
+ ConstraintLayout.LayoutParams(
ConstraintLayout.LayoutParams.MATCH_PARENT,
ConstraintLayout.LayoutParams.WRAP_CONTENT)
singleCard.layoutParams = layoutParams
container?.addView(singleCard)
+ return singleCard
}
/**
- * Adds two small [AggregationDataCard]s to the provided container stacked horizontally.
- * This should be called when there are two available aggregates.
+ * Adds two small [AggregationDataCard]s to the provided container stacked horizontally. This
+ * should be called when there are two available aggregates.
*/
private fun addTwoSmallCards(
firstCardInfo: AggregationCardInfo,
- secondCardInfo: AggregationCardInfo): Pair<AggregationDataCard, AggregationDataCard> {
+ secondCardInfo: AggregationCardInfo
+ ): Pair<AggregationDataCard, AggregationDataCard> {
// Construct the first card
val firstCard = constructSmallCard(firstCardInfo, addMargin = true)
@@ -203,38 +213,42 @@ class CardContainerPreference constructor(
constraintSet.clone(container)
// Constraints for the first card
- constraintSet.connect(firstCard.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)
- constraintSet.connect(firstCard.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)
- constraintSet.connect(firstCard.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
+ constraintSet.connect(
+ firstCard.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)
+ constraintSet.connect(
+ firstCard.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)
+ constraintSet.connect(
+ firstCard.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
constraintSet.connect(firstCard.id, ConstraintSet.END, secondCard.id, ConstraintSet.START)
// Constraints for the second card
constraintSet.connect(secondCard.id, ConstraintSet.START, firstCard.id, ConstraintSet.END)
- constraintSet.connect(secondCard.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)
- constraintSet.connect(secondCard.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
- constraintSet.connect(secondCard.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
+ constraintSet.connect(
+ secondCard.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)
+ constraintSet.connect(
+ secondCard.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
+ constraintSet.connect(
+ secondCard.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
constraintSet.applyTo(container)
}
private fun constructSmallCard(
cardInfo: AggregationCardInfo,
- addMargin: Boolean) : AggregationDataCard {
- val card = AggregationDataCard(
- context,
- null,
- AggregationDataCard.CardTypeEnum.SMALL_CARD,
- cardInfo,
- timeSource)
+ addMargin: Boolean
+ ): AggregationDataCard {
+ val card =
+ AggregationDataCard(
+ context, null, AggregationDataCard.CardTypeEnum.SMALL_CARD, cardInfo, timeSource)
card.id = View.generateViewId()
- val layoutParams = ConstraintLayout.LayoutParams(0,
- ConstraintLayout.LayoutParams.WRAP_CONTENT)
+ val layoutParams =
+ ConstraintLayout.LayoutParams(0, ConstraintLayout.LayoutParams.WRAP_CONTENT)
if (addMargin) {
// Set a right margin of 16dp for the first (leftmost) card
val marginInDp = 16
val marginInPx = (marginInDp * context.resources.displayMetrics.density).toInt()
- layoutParams.setMargins(0,0, marginInPx, 0)
+ layoutParams.setMargins(0, 0, marginInPx, 0)
}
card.layoutParams = layoutParams
@@ -243,13 +257,14 @@ class CardContainerPreference constructor(
}
/**
- * Adds two large [AggregationDataCard]s to the provided container stacked vertically.
- * This should be called when there are two available aggregates and the text is
- * too large to fit into small cards.
+ * Adds two large [AggregationDataCard]s to the provided container stacked vertically. This
+ * should be called when there are two available aggregates and the text is too large to fit
+ * into small cards.
*/
private fun addTwoLargeCards(
firstCardInfo: AggregationCardInfo,
- secondCardInfo: AggregationCardInfo): Pair<AggregationDataCard, AggregationDataCard> {
+ secondCardInfo: AggregationCardInfo
+ ): Pair<AggregationDataCard, AggregationDataCard> {
// Construct the first card
val firstLongCard = constructLargeCard(firstCardInfo, addMargin = true)
// Construct the second card
@@ -275,16 +290,22 @@ class CardContainerPreference constructor(
constraintSet.clone(container)
// Constraints for the first card
- constraintSet.connect(firstCard.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)
- constraintSet.connect(firstCard.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)
+ constraintSet.connect(
+ firstCard.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)
+ constraintSet.connect(
+ firstCard.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)
constraintSet.connect(firstCard.id, ConstraintSet.BOTTOM, secondCard.id, ConstraintSet.TOP)
- constraintSet.connect(firstCard.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
+ constraintSet.connect(
+ firstCard.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
// Constraints for the first card
constraintSet.connect(secondCard.id, ConstraintSet.TOP, firstCard.id, ConstraintSet.BOTTOM)
- constraintSet.connect(secondCard.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)
- constraintSet.connect(secondCard.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
- constraintSet.connect(secondCard.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
+ constraintSet.connect(
+ secondCard.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)
+ constraintSet.connect(
+ secondCard.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
+ constraintSet.connect(
+ secondCard.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
constraintSet.applyTo(container)
}
@@ -293,31 +314,31 @@ class CardContainerPreference constructor(
cardInfo: AggregationCardInfo,
addMargin: Boolean
): AggregationDataCard {
- val largeCard = AggregationDataCard(context, null,
- AggregationDataCard.CardTypeEnum.LARGE_CARD, cardInfo, timeSource)
+ val largeCard =
+ AggregationDataCard(
+ context, null, AggregationDataCard.CardTypeEnum.LARGE_CARD, cardInfo, timeSource)
largeCard.id = View.generateViewId()
- val layoutParams = ConstraintLayout.LayoutParams(0, ConstraintLayout.LayoutParams.WRAP_CONTENT)
+ val layoutParams =
+ ConstraintLayout.LayoutParams(0, ConstraintLayout.LayoutParams.WRAP_CONTENT)
if (addMargin) {
// Set a bottom margin of 16dp for the first (topmost) card
val marginInDp = 16
val marginInPx = (marginInDp * context.resources.displayMetrics.density).toInt()
- layoutParams.setMargins(0,0, 0, marginInPx)
+ layoutParams.setMargins(0, 0, 0, marginInPx)
}
largeCard.layoutParams = layoutParams
return largeCard
}
- /**
- * Returns true if the provided textView is ellipsized (...)
- */
+ /** Returns true if the provided textView is ellipsized (...) */
private fun isTextEllipsized(textView: TextView): Boolean {
if (textView.layout != null) {
val lines = textView.layout.lineCount
if (lines > 0) {
- if (textView.layout.getEllipsisCount(lines - 1) > 0 ) {
+ if (textView.layout.getEllipsisCount(lines - 1) > 0) {
return true
}
}
@@ -325,13 +346,23 @@ class CardContainerPreference constructor(
return false
}
+ private fun removeAllChildrenExcept(container: ConstraintLayout?, childToKeep: View) {
+ container?.let {
+ for (i in it.childCount - 1 downTo 0) {
+ val currentChild = it.getChildAt(i)
+ if (currentChild != childToKeep) {
+ it.removeViewAt(i)
+ }
+ }
+ }
+ }
+
override fun hasSameContents(preference: Preference): Boolean {
return preference is CardContainerPreference &&
- preference.mAggregationCardInfo == this.mAggregationCardInfo
+ preference.mAggregationCardInfo == this.mAggregationCardInfo
}
override fun isSameItem(preference: Preference): Boolean {
- return preference is CardContainerPreference &&
- this == preference
+ return preference is CardContainerPreference && this == preference
}
-} \ No newline at end of file
+}
diff --git a/apk/src/com/android/healthconnect/controller/utils/AppStoreUtil.kt b/apk/src/com/android/healthconnect/controller/utils/AppStoreUtil.kt
deleted file mode 100644
index 24824138..00000000
--- a/apk/src/com/android/healthconnect/controller/utils/AppStoreUtil.kt
+++ /dev/null
@@ -1,72 +0,0 @@
-package com.android.healthconnect.controller.utils
-
-import android.content.Context
-import android.content.Intent
-import android.content.pm.ApplicationInfo
-import android.content.pm.PackageManager.NameNotFoundException
-import android.util.Log
-
-/** Functions that help dealing with app stores. */
-private const val TAG = "HCAppStoreUtil"
-
-private fun resolveIntent(context: Context, intent: Intent): Intent? {
- val resolveInfoResult = context.packageManager.resolveActivity(intent, 0)
- return if (resolveInfoResult != null) {
- Intent(intent.action)
- .setClassName(
- resolveInfoResult.activityInfo.packageName, resolveInfoResult.activityInfo.name)
- } else null
-}
-
-private fun getInstallerPackageName(context: Context, packageName: String): String? {
- var installerPackageName: String? = null
-
- try {
- val source = context.packageManager.getInstallSourceInfo(packageName)
-
- // By default use the installing package name
- installerPackageName = source.installingPackageName
-
- // Use the recorded originating package name only if the initiating package is a system
- // app (eg. Package Installer). The originating package is not verified by the platform,
- // so we choose to ignore this when supplied by a non-system app.
- val originatingPackageName = source.originatingPackageName
- val initiatingPackageName = source.initiatingPackageName
- if (originatingPackageName != null && initiatingPackageName != null) {
- val ai = context.packageManager.getApplicationInfo(initiatingPackageName, 0)
- if (ai.flags and ApplicationInfo.FLAG_SYSTEM != 0) {
- installerPackageName = originatingPackageName
- }
- }
- } catch (exception: NameNotFoundException) {
- Log.e(TAG, "Exception while retrieving the package installer of $packageName", exception)
- }
-
- return installerPackageName
-}
-
-private fun getAppStoreLink(
- context: Context,
- installerPackageName: String?,
- packageName: String
-): Intent? {
- val intent = Intent(Intent.ACTION_SHOW_APP_INFO)
- if (installerPackageName != null) {
- // if we cannot find the installer package name we can still
- // send the intent which should be handled by one app
- intent.setPackage(installerPackageName)
- }
-
- val result = resolveIntent(context, intent)
- if (result != null) {
- result.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName)
- return result
- }
-
- return null
-}
-
-fun getAppStoreLink(context: Context, packageName: String): Intent? {
- val installerPackageName = getInstallerPackageName(context, packageName)
- return getAppStoreLink(context, installerPackageName, packageName)
-}
diff --git a/apk/src/com/android/healthconnect/controller/utils/AppStoreUtils.kt b/apk/src/com/android/healthconnect/controller/utils/AppStoreUtils.kt
new file mode 100644
index 00000000..529c0e6c
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/utils/AppStoreUtils.kt
@@ -0,0 +1,83 @@
+package com.android.healthconnect.controller.utils
+
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager.NameNotFoundException
+import android.util.Log
+import dagger.hilt.android.qualifiers.ApplicationContext
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Functions that help dealing with app stores. */
+@Singleton
+class AppStoreUtils @Inject constructor(@ApplicationContext private val context: Context) {
+
+ companion object {
+ private const val TAG = "HCAppStoreUtil"
+ }
+
+ private val packageManager = context.packageManager
+
+ /**
+ * Returns the app store intent for a package name, returns null if the package is not installed
+ */
+ fun getAppStoreLink(packageName: String): Intent? {
+ val installerPackageName = getInstallerPackageName(packageName)
+ return getAppStoreLink(installerPackageName, packageName)
+ }
+
+ private fun getInstallerPackageName(packageName: String): String? {
+ var installerPackageName: String? = null
+
+ try {
+ val source = packageManager.getInstallSourceInfo(packageName)
+
+ // By default use the installing package name
+ installerPackageName = source.installingPackageName
+
+ // Use the recorded originating package name only if the initiating package is a system
+ // app (eg. Package Installer). The originating package is not verified by the platform,
+ // so we choose to ignore this when supplied by a non-system app.
+ val originatingPackageName = source.originatingPackageName
+ val initiatingPackageName = source.initiatingPackageName
+ if (originatingPackageName != null && initiatingPackageName != null) {
+ val ai = packageManager.getApplicationInfo(initiatingPackageName, 0)
+ if (ai.flags and ApplicationInfo.FLAG_SYSTEM != 0) {
+ installerPackageName = originatingPackageName
+ }
+ }
+ } catch (exception: NameNotFoundException) {
+ Log.e(
+ TAG, "Exception while retrieving the package installer of $packageName", exception)
+ }
+
+ return installerPackageName
+ }
+
+ private fun getAppStoreLink(installerPackageName: String?, packageName: String): Intent? {
+ val intent = Intent(Intent.ACTION_SHOW_APP_INFO)
+ if (installerPackageName != null) {
+ // if we cannot find the installer package name we can still
+ // send the intent which should be handled by one app
+ intent.setPackage(installerPackageName)
+ }
+
+ val result = resolveIntent(intent)
+ if (result != null) {
+ result.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName)
+ return result
+ }
+
+ return null
+ }
+
+ private fun resolveIntent(intent: Intent): Intent? {
+ val resolveInfoResult = packageManager.resolveActivity(intent, 0)
+ return if (resolveInfoResult != null) {
+ Intent(intent.action)
+ .setClassName(
+ resolveInfoResult.activityInfo.packageName, resolveInfoResult.activityInfo.name)
+ } else null
+ }
+}
diff --git a/apk/src/com/android/healthconnect/controller/utils/NavigationUtils.kt b/apk/src/com/android/healthconnect/controller/utils/NavigationUtils.kt
new file mode 100644
index 00000000..a791e43f
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/utils/NavigationUtils.kt
@@ -0,0 +1,17 @@
+package com.android.healthconnect.controller.utils
+
+import android.content.Intent
+import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.findNavController
+import javax.inject.Inject
+
+class NavigationUtils @Inject constructor() {
+
+ fun navigate(fragment: Fragment, action: Int) {
+ fragment.findNavController().navigate(action)
+ }
+
+ fun startActivity(fragment: Fragment, intent: Intent) {
+ fragment.startActivity(intent)
+ }
+}
diff --git a/apk/src/com/android/healthconnect/controller/utils/TimeExtensions.kt b/apk/src/com/android/healthconnect/controller/utils/TimeExtensions.kt
index 0a42689a..5fcbd39f 100644
--- a/apk/src/com/android/healthconnect/controller/utils/TimeExtensions.kt
+++ b/apk/src/com/android/healthconnect/controller/utils/TimeExtensions.kt
@@ -15,11 +15,14 @@
*/
package com.android.healthconnect.controller.utils
+import java.time.Duration
import java.time.Instant
import java.time.Instant.ofEpochMilli
import java.time.LocalDate
+import java.time.LocalDateTime
import java.time.LocalTime
import java.time.ZoneId
+import kotlin.random.Random
/**
* Returns an Instant with the specified year, month and day-of-month. The day must be valid for the
@@ -46,6 +49,10 @@ fun Instant.toLocalTime(): LocalTime {
return atZone(ZoneId.systemDefault()).toLocalTime()
}
+fun Instant.toLocalDateTime(): LocalDateTime {
+ return atZone(ZoneId.systemDefault()).toLocalDateTime()
+}
+
fun Instant.isOnSameDay(other: Instant): Boolean {
val localDate1 = this.toLocalDate()
val localDate2 = other.toLocalDate()
@@ -77,3 +84,21 @@ fun Instant.isAtLeastOneDayAfter(other: Instant): Boolean {
fun LocalDate.toInstantAtStartOfDay(): Instant {
return this.atStartOfDay(ZoneId.systemDefault()).toInstant()
}
+
+fun LocalDate.randomInstant(): Instant {
+ val startOfDay = this.toInstantAtStartOfDay()
+
+ // Calculate the number of seconds in a day, accounting for daylight saving changes
+ val duration = Duration.between(startOfDay, this.plusDays(1).toInstantAtStartOfDay())
+ val secondsInDay = duration.seconds
+
+ // Generate a random offset in seconds within the day
+ val randomSecondOffset = Random.nextLong(secondsInDay)
+
+ // Return the calculated instant
+ return startOfDay.plusSeconds(randomSecondOffset)
+}
+
+fun LocalDateTime.toInstant(): Instant {
+ return atZone(ZoneId.systemDefault()).toInstant()
+}
diff --git a/apk/src/com/android/healthconnect/controller/utils/logging/HealthConnectLogger.kt b/apk/src/com/android/healthconnect/controller/utils/logging/HealthConnectLogger.kt
index 711b1733..370f6efd 100644
--- a/apk/src/com/android/healthconnect/controller/utils/logging/HealthConnectLogger.kt
+++ b/apk/src/com/android/healthconnect/controller/utils/logging/HealthConnectLogger.kt
@@ -135,9 +135,14 @@ enum class PageName(val impressionId: Int, val interactionId: Int) {
HEALTH_CONNECT_UI_IMPRESSION__PAGE__MIGRATION_PAUSED_PAGE,
HEALTH_CONNECT_UI_INTERACTION__PAGE__MIGRATION_PAUSED_PAGE),
MANAGE_DATA_PAGE(
- HEALTH_CONNECT_UI_IMPRESSION__PAGE__MANAGE_DATA_PAGE,
- HEALTH_CONNECT_UI_INTERACTION__PAGE__MANAGE_DATA_PAGE
- ),
+ HEALTH_CONNECT_UI_IMPRESSION__PAGE__MANAGE_DATA_PAGE,
+ HEALTH_CONNECT_UI_INTERACTION__PAGE__MANAGE_DATA_PAGE),
+ DATA_SOURCES_PAGE(
+ HEALTH_CONNECT_UI_IMPRESSION__PAGE__DATA_SOURCES_PAGE,
+ HEALTH_CONNECT_UI_INTERACTION__PAGE__DATA_SOURCES_PAGE),
+ ADD_AN_APP_PAGE(
+ HEALTH_CONNECT_UI_IMPRESSION__PAGE__ADD_AN_APP_PAGE,
+ HEALTH_CONNECT_UI_INTERACTION__PAGE__ADD_AN_APP_PAGE),
UNKNOWN_PAGE(
HEALTH_CONNECT_UI_IMPRESSION__PAGE__PAGE_UNKNOWN,
HEALTH_CONNECT_UI_INTERACTION__PAGE__PAGE_UNKNOWN)
@@ -162,8 +167,8 @@ enum class HomePageElement(override val impressionId: Int, override val interact
HEALTH_CONNECT_UI_IMPRESSION__ELEMENT__SEE_ALL_RECENT_ACCESS_BUTTON,
HEALTH_CONNECT_UI_INTERACTION__ELEMENT__SEE_ALL_RECENT_ACCESS_BUTTON),
MANAGE_DATA_BUTTON(
- HEALTH_CONNECT_UI_IMPRESSION__ELEMENT__MANAGE_DATA_BUTTON,
- HEALTH_CONNECT_UI_INTERACTION__ELEMENT__MANAGE_DATA_BUTTON),
+ HEALTH_CONNECT_UI_IMPRESSION__ELEMENT__MANAGE_DATA_BUTTON,
+ HEALTH_CONNECT_UI_INTERACTION__ELEMENT__MANAGE_DATA_BUTTON),
}
/** Loggable elements in the Onboarding page. */
@@ -188,24 +193,21 @@ enum class RecentAccessElement(override val impressionId: Int, override val inte
HEALTH_CONNECT_UI_INTERACTION__ELEMENT__MANAGE_PERMISSIONS_FLOATING_BUTTON),
}
+/** Loggable elements in the Manage Data page. */
enum class ManageDataElement(override val impressionId: Int, override val interactionId: Int) :
ElementName {
AUTO_DELETE_BUTTON(
- HEALTH_CONNECT_UI_IMPRESSION__ELEMENT__AUTO_DELETE_BUTTON,
- HEALTH_CONNECT_UI_INTERACTION__ELEMENT__AUTO_DELETE_BUTTON
- ),
+ HEALTH_CONNECT_UI_IMPRESSION__ELEMENT__AUTO_DELETE_BUTTON,
+ HEALTH_CONNECT_UI_INTERACTION__ELEMENT__AUTO_DELETE_BUTTON),
BACKUP_BUTTON(
- HEALTH_CONNECT_UI_IMPRESSION__ELEMENT__BACKUP_DATA_BUTTON,
- HEALTH_CONNECT_UI_INTERACTION__ELEMENT__BACKUP_DATA_BUTTON
- ),
+ HEALTH_CONNECT_UI_IMPRESSION__ELEMENT__BACKUP_DATA_BUTTON,
+ HEALTH_CONNECT_UI_INTERACTION__ELEMENT__BACKUP_DATA_BUTTON),
DATA_SOURCES_AND_PRIORITY_BUTTON(
- HEALTH_CONNECT_UI_IMPRESSION__ELEMENT__DATA_SOURCES_AND_PRIORITY_BUTTON,
- HEALTH_CONNECT_UI_INTERACTION__ELEMENT__DATA_SOURCES_AND_PRIORITY_BUTTON
- ),
+ HEALTH_CONNECT_UI_IMPRESSION__ELEMENT__DATA_SOURCES_AND_PRIORITY_BUTTON,
+ HEALTH_CONNECT_UI_INTERACTION__ELEMENT__DATA_SOURCES_AND_PRIORITY_BUTTON),
SET_UNITS_BUTTON(
- HEALTH_CONNECT_UI_IMPRESSION__ELEMENT__SET_UNITS_BUTTON,
- HEALTH_CONNECT_UI_INTERACTION__ELEMENT__SET_UNITS_BUTTON
- )
+ HEALTH_CONNECT_UI_IMPRESSION__ELEMENT__SET_UNITS_BUTTON,
+ HEALTH_CONNECT_UI_INTERACTION__ELEMENT__SET_UNITS_BUTTON)
}
/** Loggable elements in the Category and All categories pages. */
@@ -307,6 +309,11 @@ enum class PermissionTypesElement(override val impressionId: Int, override val i
SET_APP_PRIORITY_DIALOG_SAVE_BUTTON(
HEALTH_CONNECT_UI_IMPRESSION__ELEMENT__ELEMENT_UNKNOWN,
HEALTH_CONNECT_UI_INTERACTION__ELEMENT__ELEMENT_UNKNOWN),
+
+ // New app priority
+ DATA_SOURCES_AND_PRIORITY_BUTTON(
+ HEALTH_CONNECT_UI_IMPRESSION__ELEMENT__DATA_SOURCES_AND_PRIORITY_BUTTON,
+ HEALTH_CONNECT_UI_INTERACTION__ELEMENT__DATA_SOURCES_AND_PRIORITY_BUTTON),
}
/** Loggable elements in the Data access page. */
@@ -679,6 +686,40 @@ enum class MigrationElement(override val impressionId: Int, override val interac
HEALTH_CONNECT_UI_INTERACTION__ELEMENT__MIGRATION_APP_UPDATE_BUTTON)
}
+/** Loggable elements in the Data sources page. */
+enum class DataSourcesElement(override val impressionId: Int, override val interactionId: Int) :
+ ElementName {
+ DATA_TYPE_SPINNER(
+ HEALTH_CONNECT_UI_IMPRESSION__ELEMENT__DATA_TYPE_SPINNER_BUTTON,
+ HEALTH_CONNECT_UI_INTERACTION__ELEMENT__DATA_TYPE_SPINNER_BUTTON),
+ DATA_TOTALS_CARD(
+ HEALTH_CONNECT_UI_IMPRESSION__ELEMENT__DATA_TOTALS_CARD,
+ HEALTH_CONNECT_UI_INTERACTION__ELEMENT__DATA_TOTALS_CARD),
+ APP_SOURCE_BUTTON(
+ HEALTH_CONNECT_UI_IMPRESSION__ELEMENT__APP_SOURCE_BUTTON,
+ HEALTH_CONNECT_UI_INTERACTION__ELEMENT__APP_SOURCE_BUTTON),
+ ADD_AN_APP_BUTTON(
+ HEALTH_CONNECT_UI_IMPRESSION__ELEMENT__ADD_AN_APP_BUTTON,
+ HEALTH_CONNECT_UI_INTERACTION__ELEMENT__ADD_AN_APP_BUTTON),
+ EDIT_SOURCE_LIST_BUTTON(
+ HEALTH_CONNECT_UI_IMPRESSION__ELEMENT__EDIT_SOURCE_LIST_BUTTON,
+ HEALTH_CONNECT_UI_INTERACTION__ELEMENT__EDIT_SOURCE_LIST_BUTTON),
+ REORDER_APP_SOURCE_BUTTON(
+ HEALTH_CONNECT_UI_IMPRESSION__ELEMENT__REORDER_APP_SOURCE_BUTTON,
+ HEALTH_CONNECT_UI_INTERACTION__ELEMENT__REORDER_APP_SOURCE_BUTTON),
+ REMOVE_APP_SOURCE_BUTTON(
+ HEALTH_CONNECT_UI_IMPRESSION__ELEMENT__REMOVE_APP_SOURCE_BUTTON,
+ HEALTH_CONNECT_UI_INTERACTION__ELEMENT__REMOVE_APP_SOURCE_BUTTON)
+}
+
+/** Loggable elements in the Add an app page. */
+enum class AddAnAppElement(override val impressionId: Int, override val interactionId: Int) :
+ ElementName {
+ POTENTIAL_PRIORITY_APP_BUTTON(
+ HEALTH_CONNECT_UI_IMPRESSION__ELEMENT__POTENTIAL_PRIORITY_APP_BUTTON,
+ HEALTH_CONNECT_UI_INTERACTION__ELEMENT__POTENTIAL_PRIORITY_APP_BUTTON)
+}
+
/** Loggable elements belonging to the error page, and the unknown element. */
enum class ErrorPageElement(override val impressionId: Int, override val interactionId: Int) :
ElementName {
diff --git a/apk/tests/Android.bp b/apk/tests/Android.bp
index 805fc047..ba4d2f71 100644
--- a/apk/tests/Android.bp
+++ b/apk/tests/Android.bp
@@ -78,9 +78,11 @@ android_test {
"kotlinx_coroutines_test",
// test dependencies
"androidx.test.espresso.contrib",
+ "androidx.test.espresso.intents",
"androidx.test.ext.junit",
"androidx.test.ext.truth",
"androidx.test.rules",
+ "mockito-kotlin2"
],
resource_dirs: ["main_res"],
libs: [
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/MainActivityTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/MainActivityTest.kt
index 2e5c4345..2124238b 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/MainActivityTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/MainActivityTest.kt
@@ -64,7 +64,7 @@ class MainActivityTest {
}
@Test
- fun homeSettingsIntent_onboardingNotDone_redirectToOnboarding() = runTest {
+ fun homeSettingsIntent_onboardingNotDone_redirectsToOnboarding() = runTest {
showOnboarding(context, true)
whenever(viewModel.getCurrentMigrationUiState()).then { MigrationState.COMPLETE_IDLE }
whenever(viewModel.migrationState).then {
@@ -83,7 +83,7 @@ class MainActivityTest {
}
@Test
- fun homeSettingsIntent_migrationInProgress_redirectToMigrationScreen() = runTest {
+ fun homeSettingsIntent_migrationInProgress_redirectsToMigrationScreen() = runTest {
showOnboarding(context, false)
whenever(viewModel.getCurrentMigrationUiState()).then { MigrationState.IN_PROGRESS }
whenever(viewModel.migrationState).then {
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/autodelete/api/LoadAutoDeleteUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/autodelete/api/LoadAutoDeleteUseCaseTest.kt
new file mode 100644
index 00000000..355c1091
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/autodelete/api/LoadAutoDeleteUseCaseTest.kt
@@ -0,0 +1,89 @@
+/**
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * ```
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * ```
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.android.healthconnect.controller.tests.autodelete.api
+
+import android.health.connect.HealthConnectException
+import android.health.connect.HealthConnectManager
+import com.android.healthconnect.controller.autodelete.api.LoadAutoDeleteUseCase
+import com.android.healthconnect.controller.shared.usecase.UseCaseResults
+import com.google.common.truth.Truth.assertThat
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito
+import org.mockito.kotlin.whenever
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@HiltAndroidTest
+class LoadAutoDeleteUseCaseTest {
+
+ @get:Rule val hiltRule = HiltAndroidRule(this)
+
+ private val healthConnectManager: HealthConnectManager =
+ Mockito.mock(HealthConnectManager::class.java)
+
+ private lateinit var loadAutoDeleteUseCase: LoadAutoDeleteUseCase
+
+ @Before
+ fun setup() {
+ hiltRule.inject()
+ loadAutoDeleteUseCase = LoadAutoDeleteUseCase(healthConnectManager, Dispatchers.Main)
+ }
+
+ @Test
+ fun loadAutoDeleteUseCase_whenRecordRetention90days_returns3months() = runTest {
+ whenever(healthConnectManager.recordRetentionPeriodInDays).thenReturn(90)
+
+ val result = loadAutoDeleteUseCase.invoke()
+ assertThat(result is UseCaseResults.Success).isTrue()
+ assertThat((result as UseCaseResults.Success).data).isEqualTo(3)
+ }
+
+ @Test
+ fun loadAutoDeleteUseCase_whenRecordRetention540days_returns18months() = runTest {
+ whenever(healthConnectManager.recordRetentionPeriodInDays).thenReturn(540)
+
+ val result = loadAutoDeleteUseCase.invoke()
+ assertThat(result is UseCaseResults.Success).isTrue()
+ assertThat((result as UseCaseResults.Success).data).isEqualTo(18)
+ }
+
+ @Test
+ fun loadAutoDeleteUseCase_whenRecordRetention0days_returns0months() = runTest {
+ whenever(healthConnectManager.recordRetentionPeriodInDays).thenReturn(0)
+
+ val result = loadAutoDeleteUseCase.invoke()
+ assertThat(result is UseCaseResults.Success).isTrue()
+ assertThat((result as UseCaseResults.Success).data).isEqualTo(0)
+ }
+
+ @Test
+ fun loadAutoDeleteUseCase_whenRecordRetentionFails_returnsFailure() = runTest {
+ whenever(healthConnectManager.recordRetentionPeriodInDays)
+ .thenThrow(HealthConnectException(HealthConnectException.ERROR_UNKNOWN))
+
+ val result = loadAutoDeleteUseCase.invoke()
+ assertThat(result is UseCaseResults.Failed).isTrue()
+ assertThat((result as UseCaseResults.Failed).exception is HealthConnectException).isTrue()
+ assertThat((result.exception as HealthConnectException).errorCode)
+ .isEqualTo(HealthConnectException.ERROR_UNKNOWN)
+ }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/autodelete/api/UpdateAutoDeleteUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/autodelete/api/UpdateAutoDeleteUseCaseTest.kt
new file mode 100644
index 00000000..203096b4
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/autodelete/api/UpdateAutoDeleteUseCaseTest.kt
@@ -0,0 +1,126 @@
+/**
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * ```
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * ```
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.android.healthconnect.controller.tests.autodelete.api
+
+import android.health.connect.HealthConnectException
+import android.health.connect.HealthConnectManager
+import android.os.OutcomeReceiver
+import com.android.healthconnect.controller.autodelete.api.UpdateAutoDeleteUseCase
+import com.android.healthconnect.controller.shared.usecase.UseCaseResults
+import com.android.healthconnect.controller.tests.utils.whenever
+import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+import org.mockito.invocation.InvocationOnMock
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@HiltAndroidTest
+class UpdateAutoDeleteUseCaseTest {
+
+ @get:Rule val hiltRule = HiltAndroidRule(this)
+
+ private val healthConnectManager: HealthConnectManager =
+ Mockito.mock(HealthConnectManager::class.java)
+
+ private lateinit var updateAutoDeleteUseCase: UpdateAutoDeleteUseCase
+
+ @Captor lateinit var captor: ArgumentCaptor<Int>
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ hiltRule.inject()
+ updateAutoDeleteUseCase = UpdateAutoDeleteUseCase(healthConnectManager, Dispatchers.Main)
+ }
+
+ @Test
+ fun updateAutoDeleteUseCase_3months_callsManagerWithCorrectArgs() = runTest {
+ doAnswer(prepareAnswer())
+ .`when`(healthConnectManager)
+ .setRecordRetentionPeriodInDays(any(), any(), any())
+
+ val result = updateAutoDeleteUseCase.invoke(3)
+
+ verify(healthConnectManager, times(1))
+ .setRecordRetentionPeriodInDays(captor.capture(), any(), any())
+ assertThat(captor.value).isEqualTo(90)
+ assertThat(result is UseCaseResults.Success)
+ }
+
+ @Test
+ fun updateAutoDeleteUseCase_18months_callsManagerWithCorrectArgs() = runTest {
+ doAnswer(prepareAnswer())
+ .`when`(healthConnectManager)
+ .setRecordRetentionPeriodInDays(any(), any(), any())
+
+ val result = updateAutoDeleteUseCase.invoke(18)
+
+ verify(healthConnectManager, times(1))
+ .setRecordRetentionPeriodInDays(captor.capture(), any(), any())
+ assertThat(captor.value).isEqualTo(540)
+ assertThat(result is UseCaseResults.Success)
+ }
+
+ @Test
+ fun updateAutoDeleteUseCase_0months_callsManagerWithCorrectArgs() = runTest {
+ doAnswer(prepareAnswer())
+ .`when`(healthConnectManager)
+ .setRecordRetentionPeriodInDays(any(), any(), any())
+
+ val result = updateAutoDeleteUseCase.invoke(0)
+
+ verify(healthConnectManager, times(1))
+ .setRecordRetentionPeriodInDays(captor.capture(), any(), any())
+ assertThat(captor.value).isEqualTo(0)
+ assertThat(result is UseCaseResults.Success)
+ }
+
+ @Test
+ fun updateAutoDeleteUseCase_whenSetRecordRetentionFails_returnsFailure() = runTest {
+ whenever(healthConnectManager.setRecordRetentionPeriodInDays(any(), any(), any()))
+ .thenThrow(HealthConnectException(HealthConnectException.ERROR_UNKNOWN))
+
+ val result = updateAutoDeleteUseCase.invoke(1)
+ assertThat(result is UseCaseResults.Failed).isTrue()
+ assertThat((result as UseCaseResults.Failed).exception is HealthConnectException).isTrue()
+ Truth.assertThat((result.exception as HealthConnectException).errorCode)
+ .isEqualTo(HealthConnectException.ERROR_UNKNOWN)
+ }
+
+ private fun prepareAnswer(): (InvocationOnMock) -> Nothing? {
+ val answer = { args: InvocationOnMock ->
+ val receiver = args.arguments[2] as OutcomeReceiver<*, *>
+ receiver.onResult(null)
+ null
+ }
+ return answer
+ }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/data/DataManagementActivityTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/data/DataManagementActivityTest.kt
new file mode 100644
index 00000000..14ac4ad1
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/data/DataManagementActivityTest.kt
@@ -0,0 +1,145 @@
+/**
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * ```
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * ```
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.android.healthconnect.controller.tests.data
+
+import android.content.Context
+import android.content.Intent
+import androidx.lifecycle.MutableLiveData
+import androidx.test.core.app.ActivityScenario.launch
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.healthconnect.controller.autodelete.AutoDeleteRange
+import com.android.healthconnect.controller.autodelete.AutoDeleteViewModel
+import com.android.healthconnect.controller.categories.HealthDataCategoryViewModel
+import com.android.healthconnect.controller.data.DataManagementActivity
+import com.android.healthconnect.controller.migration.MigrationViewModel
+import com.android.healthconnect.controller.migration.api.MigrationState
+import com.android.healthconnect.controller.tests.utils.di.FakeFeatureUtils
+import com.android.healthconnect.controller.tests.utils.showOnboarding
+import com.android.healthconnect.controller.tests.utils.whenever
+import com.android.healthconnect.controller.utils.FeatureUtils
+import dagger.hilt.android.testing.BindValue
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito
+
+@HiltAndroidTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class DataManagementActivityTest {
+ @get:Rule val hiltRule = HiltAndroidRule(this)
+
+ @BindValue
+ val migrationViewModel: MigrationViewModel = Mockito.mock(MigrationViewModel::class.java)
+
+ @BindValue
+ val categoryViewModel: HealthDataCategoryViewModel =
+ Mockito.mock(HealthDataCategoryViewModel::class.java)
+
+ @BindValue
+ val autoDeleteViewModel: AutoDeleteViewModel = Mockito.mock(AutoDeleteViewModel::class.java)
+
+ @Inject lateinit var fakeFeatureUtils: FeatureUtils
+
+ private lateinit var context: Context
+
+ @Before
+ fun setup() {
+ hiltRule.inject()
+ context = InstrumentationRegistry.getInstrumentation().context
+ (fakeFeatureUtils as FakeFeatureUtils).setIsNewInformationArchitectureEnabled(false)
+
+ showOnboarding(context, show = false)
+ whenever(autoDeleteViewModel.storedAutoDeleteRange).then {
+ MutableLiveData(
+ AutoDeleteViewModel.AutoDeleteState.WithData(
+ AutoDeleteRange.AUTO_DELETE_RANGE_NEVER))
+ }
+ whenever(categoryViewModel.categoriesData).then {
+ MutableLiveData<HealthDataCategoryViewModel.CategoriesFragmentState>(
+ HealthDataCategoryViewModel.CategoriesFragmentState.WithData(emptyList()))
+ }
+ }
+
+ @Test
+ fun manageDataIntent_onboardingDone_launchesDataManagementActivity() = runTest {
+ whenever(migrationViewModel.getCurrentMigrationUiState()).then {
+ MigrationState.COMPLETE_IDLE
+ }
+ whenever(migrationViewModel.migrationState).then {
+ MutableLiveData(
+ MigrationViewModel.MigrationFragmentState.WithData(MigrationState.COMPLETE_IDLE))
+ }
+
+ val startActivityIntent = Intent(context, DataManagementActivity::class.java)
+
+ launch<DataManagementActivity>(startActivityIntent)
+ onView(withText("Browse data")).check(matches(isDisplayed()))
+ }
+
+ @Test
+ fun manageDataIntent_onboardingNotDone_redirectsToOnboarding() = runTest {
+ showOnboarding(context, true)
+ whenever(migrationViewModel.getCurrentMigrationUiState()).then {
+ MigrationState.COMPLETE_IDLE
+ }
+ whenever(migrationViewModel.migrationState).then {
+ MutableLiveData(
+ MigrationViewModel.MigrationFragmentState.WithData(MigrationState.COMPLETE_IDLE))
+ }
+
+ val startActivityIntent = Intent(context, DataManagementActivity::class.java)
+
+ launch<DataManagementActivity>(startActivityIntent)
+
+ onView(withText("Share data with your apps"))
+ .perform(ViewActions.scrollTo())
+ .check(matches(isDisplayed()))
+ }
+
+ @Test
+ fun manageDataIntent_migrationInProgress_redirectsToMigrationScreen() = runTest {
+ showOnboarding(context, false)
+ whenever(migrationViewModel.getCurrentMigrationUiState()).then {
+ MigrationState.IN_PROGRESS
+ }
+ whenever(migrationViewModel.migrationState).then {
+ MutableLiveData(
+ MigrationViewModel.MigrationFragmentState.WithData(MigrationState.IN_PROGRESS))
+ }
+
+ val startActivityIntent = Intent(context, DataManagementActivity::class.java)
+
+ launch<DataManagementActivity>(startActivityIntent)
+
+ onView(withText("Integration in progress")).check(matches(isDisplayed()))
+ }
+
+ @After
+ fun tearDown() {
+ showOnboarding(context, false)
+ }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/data/access/AccessViewModelTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/data/access/AccessViewModelTest.kt
new file mode 100644
index 00000000..606e10cc
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/data/access/AccessViewModelTest.kt
@@ -0,0 +1,74 @@
+package com.android.healthconnect.controller.tests.data.access
+
+import com.android.healthconnect.controller.data.access.AccessViewModel
+import com.android.healthconnect.controller.data.access.AppAccessState
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType
+import com.android.healthconnect.controller.shared.app.AppInfoReader
+import com.android.healthconnect.controller.tests.utils.InstantTaskExecutorRule
+import com.android.healthconnect.controller.tests.utils.TEST_APP
+import com.android.healthconnect.controller.tests.utils.TEST_APP_2
+import com.android.healthconnect.controller.tests.utils.TEST_APP_3
+import com.android.healthconnect.controller.tests.utils.TestObserver
+import com.android.healthconnect.controller.tests.utils.di.FakeLoadAccessUseCase
+import com.google.common.truth.Truth.assertThat
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import javax.inject.Inject
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineDispatcher
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.MockitoAnnotations
+
+@ExperimentalCoroutinesApi
+@HiltAndroidTest
+class AccessViewModelTest {
+
+ @get:Rule val hiltRule = HiltAndroidRule(this)
+ @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ private lateinit var viewModel: AccessViewModel
+ private val fakeLoadAccessUseCase = FakeLoadAccessUseCase()
+ private val testDispatcher = TestCoroutineDispatcher()
+
+ @Inject lateinit var appInfoReader: AppInfoReader
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ Dispatchers.setMain(testDispatcher)
+ hiltRule.inject()
+ viewModel = AccessViewModel(fakeLoadAccessUseCase)
+ }
+
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
+ testDispatcher.cleanupTestCoroutines()
+ }
+
+ @Test
+ fun loadAppMetadataMap_returnsCorrectApps() = runTest {
+ val expected =
+ mapOf(
+ AppAccessState.Read to listOf(TEST_APP, TEST_APP_2),
+ AppAccessState.Write to listOf(TEST_APP_2),
+ AppAccessState.Inactive to listOf(TEST_APP_3))
+ fakeLoadAccessUseCase.updateMap(expected)
+
+ val testObserver = TestObserver<AccessViewModel.AccessScreenState>()
+ viewModel.appMetadataMap.observeForever(testObserver)
+ viewModel.loadAppMetaDataMap(HealthPermissionType.STEPS)
+ advanceUntilIdle()
+
+ assertThat(testObserver.getLastValue())
+ .isEqualTo(AccessViewModel.AccessScreenState.WithData(expected))
+ }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/data/access/LoadAccessUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/data/access/LoadAccessUseCaseTest.kt
new file mode 100644
index 00000000..7772ea99
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/data/access/LoadAccessUseCaseTest.kt
@@ -0,0 +1,90 @@
+package com.android.healthconnect.controller.tests.data.access
+
+import com.android.healthconnect.controller.data.access.AppAccessState
+import com.android.healthconnect.controller.data.access.ILoadAccessUseCase
+import com.android.healthconnect.controller.data.access.LoadAccessUseCase
+import com.android.healthconnect.controller.permissions.data.HealthPermission
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType
+import com.android.healthconnect.controller.permissions.data.PermissionsAccessType
+import com.android.healthconnect.controller.shared.HealthPermissionReader
+import com.android.healthconnect.controller.shared.app.AppInfoReader
+import com.android.healthconnect.controller.shared.usecase.UseCaseResults
+import com.android.healthconnect.controller.tests.utils.TEST_APP
+import com.android.healthconnect.controller.tests.utils.TEST_APP_2
+import com.android.healthconnect.controller.tests.utils.TEST_APP_NAME
+import com.android.healthconnect.controller.tests.utils.TEST_APP_NAME_2
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME_2
+import com.android.healthconnect.controller.tests.utils.di.FakeGetGrantedHealthPermissionsUseCase
+import com.android.healthconnect.controller.tests.utils.di.FakeLoadPermissionTypeContributorAppsUseCase
+import com.google.common.truth.Truth.assertThat
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import javax.inject.Inject
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.MockitoAnnotations
+
+@ExperimentalCoroutinesApi
+@HiltAndroidTest
+class LoadAccessUseCaseTest {
+
+ @get:Rule val hiltRule = HiltAndroidRule(this)
+ private lateinit var useCase: ILoadAccessUseCase
+ private val fakeLoadPermissionTypeContributorAppsUseCase =
+ FakeLoadPermissionTypeContributorAppsUseCase()
+ private val fakeFakeGetGrantedHealthPermissionsUseCase =
+ FakeGetGrantedHealthPermissionsUseCase()
+
+ @Inject lateinit var appInfoReader: AppInfoReader
+ @Inject lateinit var healthPermissionReader: HealthPermissionReader
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ hiltRule.inject()
+ useCase =
+ LoadAccessUseCase(
+ fakeLoadPermissionTypeContributorAppsUseCase,
+ fakeFakeGetGrantedHealthPermissionsUseCase,
+ healthPermissionReader,
+ appInfoReader,
+ Dispatchers.Main)
+ }
+
+ @Test
+ fun invoke_noDataNorPermission_returnsEmptyMap() = runTest {
+ val actual = (useCase.invoke(HealthPermissionType.STEPS) as UseCaseResults.Success).data
+
+ assertThat(actual[AppAccessState.Write]!!.size).isEqualTo(0)
+ assertThat(actual[AppAccessState.Read]!!.size).isEqualTo(0)
+ assertThat(actual[AppAccessState.Inactive]!!.size).isEqualTo(0)
+ }
+
+ @Test
+ fun invoke_returnsCorrectApps() = runTest {
+ fakeLoadPermissionTypeContributorAppsUseCase.updateList(listOf(TEST_APP, TEST_APP_2))
+ val writeSteps =
+ HealthPermission(HealthPermissionType.STEPS, PermissionsAccessType.WRITE).toString()
+ fakeFakeGetGrantedHealthPermissionsUseCase.updateData(
+ TEST_APP_PACKAGE_NAME, listOf(writeSteps))
+
+ val actual = (useCase.invoke(HealthPermissionType.STEPS) as UseCaseResults.Success).data
+
+ assertThat(actual[AppAccessState.Write]).isNotNull()
+ assertThat(actual[AppAccessState.Write]!!.size).isEqualTo(1)
+ assertThat(actual[AppAccessState.Write]!![0].packageName).isEqualTo(TEST_APP_PACKAGE_NAME)
+ assertThat(actual[AppAccessState.Write]!![0].appName).isEqualTo(TEST_APP_NAME)
+ assertThat(actual[AppAccessState.Read]).isNotNull()
+ assertThat(actual[AppAccessState.Read]!!.size).isEqualTo(0)
+ assertThat(actual[AppAccessState.Inactive]).isNotNull()
+ assertThat(actual[AppAccessState.Inactive]!!.size).isEqualTo(1)
+ assertThat(actual[AppAccessState.Inactive]!![0].packageName)
+ .isEqualTo(TEST_APP_PACKAGE_NAME_2)
+ assertThat(actual[AppAccessState.Inactive]!![0].appName).isEqualTo(TEST_APP_NAME_2)
+ }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/data/access/LoadPermissionTypeContributorAppsUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/data/access/LoadPermissionTypeContributorAppsUseCaseTest.kt
new file mode 100644
index 00000000..1db30e55
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/data/access/LoadPermissionTypeContributorAppsUseCaseTest.kt
@@ -0,0 +1,113 @@
+package com.android.healthconnect.controller.tests.data.access
+
+import android.content.Context
+import android.health.connect.HealthConnectManager
+import android.health.connect.HealthDataCategory
+import android.health.connect.HealthPermissionCategory
+import android.health.connect.RecordTypeInfoResponse
+import android.health.connect.datatypes.HeartRateRecord
+import android.health.connect.datatypes.Record
+import android.health.connect.datatypes.StepsRecord
+import android.health.connect.datatypes.WeightRecord
+import android.os.OutcomeReceiver
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.healthconnect.controller.data.access.LoadPermissionTypeContributorAppsUseCase
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType
+import com.android.healthconnect.controller.shared.app.AppInfoReader
+import com.android.healthconnect.controller.shared.app.AppMetadata
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME_2
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME_3
+import com.android.healthconnect.controller.tests.utils.getDataOrigin
+import com.google.common.truth.Truth.assertThat
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import javax.inject.Inject
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+import org.mockito.invocation.InvocationOnMock
+
+@HiltAndroidTest
+class LoadPermissionTypeContributorAppsUseCaseTest {
+
+ @get:Rule val hiltRule = HiltAndroidRule(this)
+
+ private lateinit var context: Context
+ private val healthConnectManager: HealthConnectManager =
+ Mockito.mock(HealthConnectManager::class.java)
+ private lateinit var loadPermissionTypeContributorAppsUseCase:
+ LoadPermissionTypeContributorAppsUseCase
+
+ @Inject lateinit var appInfoReader: AppInfoReader
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ context = InstrumentationRegistry.getInstrumentation().context
+ hiltRule.inject()
+ loadPermissionTypeContributorAppsUseCase =
+ LoadPermissionTypeContributorAppsUseCase(
+ appInfoReader, healthConnectManager, Dispatchers.Main)
+ }
+
+ @Test
+ fun loadPermissionTypeContributorAppsUseCase_noRecordsStored_returnsEmptyMap() = runTest {
+ Mockito.doAnswer(prepareAnswer(mapOf()))
+ .`when`(healthConnectManager)
+ .queryAllRecordTypesInfo(ArgumentMatchers.any(), ArgumentMatchers.any())
+
+ val result = loadPermissionTypeContributorAppsUseCase.invoke(HealthPermissionType.STEPS)
+ val expected = listOf<AppMetadata>()
+ assertThat(result).isEqualTo(expected)
+ }
+
+ @Test
+ fun loadPermissionTypeContributorAppsUseCase_returnsCorrectApps() = runTest {
+ val recordTypeInfoMap: Map<Class<out Record>, RecordTypeInfoResponse> =
+ mapOf(
+ StepsRecord::class.java to
+ RecordTypeInfoResponse(
+ HealthPermissionCategory.STEPS,
+ HealthDataCategory.ACTIVITY,
+ listOf(
+ getDataOrigin(TEST_APP_PACKAGE_NAME),
+ getDataOrigin(TEST_APP_PACKAGE_NAME_2))),
+ WeightRecord::class.java to
+ RecordTypeInfoResponse(
+ HealthPermissionCategory.WEIGHT,
+ HealthDataCategory.BODY_MEASUREMENTS,
+ listOf((getDataOrigin(TEST_APP_PACKAGE_NAME_2)))),
+ HeartRateRecord::class.java to
+ RecordTypeInfoResponse(
+ HealthPermissionCategory.HEART_RATE,
+ HealthDataCategory.VITALS,
+ listOf((getDataOrigin(TEST_APP_PACKAGE_NAME_3)))))
+ Mockito.doAnswer(prepareAnswer(recordTypeInfoMap))
+ .`when`(healthConnectManager)
+ .queryAllRecordTypesInfo(ArgumentMatchers.any(), ArgumentMatchers.any())
+
+ val result = loadPermissionTypeContributorAppsUseCase.invoke(HealthPermissionType.STEPS)
+ assertThat(result.size).isEqualTo(2)
+ assertThat(result[0].packageName).isEqualTo(TEST_APP_PACKAGE_NAME)
+ assertThat(result[1].packageName).isEqualTo(TEST_APP_PACKAGE_NAME_2)
+ }
+
+ private fun prepareAnswer(
+ map: Map<Class<out Record>, RecordTypeInfoResponse>
+ ): (InvocationOnMock) -> Map<Class<out Record>, RecordTypeInfoResponse> {
+ val answer = { args: InvocationOnMock ->
+ val receiver =
+ args.arguments[1]
+ as OutcomeReceiver<Map<Class<out Record>, RecordTypeInfoResponse>, *>
+ receiver.onResult(map)
+ map
+ }
+ return answer
+ }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/data/entries/api/LoadDataAggregationsUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/data/entries/api/LoadDataAggregationsUseCaseTest.kt
index e57d0194..06de631e 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/data/entries/api/LoadDataAggregationsUseCaseTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/data/entries/api/LoadDataAggregationsUseCaseTest.kt
@@ -32,6 +32,7 @@ import java.time.Instant
import java.util.Locale
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
@@ -41,6 +42,7 @@ import org.mockito.Mockito
import org.mockito.MockitoAnnotations
import org.mockito.invocation.InvocationOnMock
+@OptIn(ExperimentalCoroutinesApi::class)
@HiltAndroidTest
class LoadDataAggregationsUseCaseTest {
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/data/entries/api/LoadDataEntriesUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/data/entries/api/LoadDataEntriesUseCaseTest.kt
new file mode 100644
index 00000000..46f6b8c7
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/data/entries/api/LoadDataEntriesUseCaseTest.kt
@@ -0,0 +1,147 @@
+package com.android.healthconnect.controller.tests.data.entries.api
+
+import android.content.Context
+import android.health.connect.HealthConnectException
+import android.health.connect.HealthConnectManager
+import android.health.connect.ReadRecordsRequestUsingFilters
+import android.health.connect.ReadRecordsResponse
+import android.health.connect.datatypes.Record
+import android.health.connect.datatypes.StepsCadenceRecord
+import android.health.connect.datatypes.StepsRecord
+import android.os.OutcomeReceiver
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.healthconnect.controller.data.entries.api.LoadDataEntriesInput
+import com.android.healthconnect.controller.data.entries.api.LoadDataEntriesUseCase
+import com.android.healthconnect.controller.data.entries.api.LoadEntriesHelper
+import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod
+import com.android.healthconnect.controller.dataentries.formatters.shared.HealthDataEntryFormatter
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType
+import com.android.healthconnect.controller.shared.usecase.UseCaseResults
+import com.android.healthconnect.controller.tests.utils.forDataType
+import com.android.healthconnect.controller.tests.utils.getStepsRecord
+import com.android.healthconnect.controller.tests.utils.setLocale
+import com.android.healthconnect.controller.utils.randomInstant
+import com.android.healthconnect.controller.utils.toInstantAtStartOfDay
+import com.google.common.truth.Truth.assertThat
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import java.time.LocalDate
+import java.util.Locale
+import javax.inject.Inject
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+import org.mockito.invocation.InvocationOnMock
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@HiltAndroidTest
+class LoadDataEntriesUseCaseTest {
+
+ @get:Rule val hiltRule = HiltAndroidRule(this)
+ @Inject lateinit var healthDataEntryFormatter: HealthDataEntryFormatter
+ private val healthConnectManager: HealthConnectManager =
+ Mockito.mock(HealthConnectManager::class.java)
+
+ private lateinit var context: Context
+ private lateinit var loadEntriesHelper: LoadEntriesHelper
+ private lateinit var loadDataEntriesUseCase: LoadDataEntriesUseCase
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ context = InstrumentationRegistry.getInstrumentation().context
+ context.setLocale(Locale.US)
+ hiltRule.inject()
+ loadEntriesHelper =
+ LoadEntriesHelper(context, healthDataEntryFormatter, healthConnectManager)
+ loadDataEntriesUseCase = LoadDataEntriesUseCase(Dispatchers.Main, loadEntriesHelper)
+ }
+
+ @Test
+ fun invoke_returnsFormattedData() = runTest {
+ val stepsDate = LocalDate.of(2023, 4, 5)
+ val input =
+ LoadDataEntriesInput(
+ permissionType = HealthPermissionType.STEPS,
+ packageName = null,
+ displayedStartTime = stepsDate.toInstantAtStartOfDay(),
+ period = DateNavigationPeriod.PERIOD_DAY,
+ showDataOrigin = true)
+
+ val stepsRecord = getStepsRecord(100, stepsDate.randomInstant())
+
+ Mockito.doAnswer(prepareRecordsAnswer(listOf(stepsRecord)))
+ .`when`(healthConnectManager)
+ .readRecords(
+ ArgumentMatchers.argThat<ReadRecordsRequestUsingFilters<Record>> { request ->
+ request.forDataType(dataType = StepsRecord::class.java)
+ },
+ ArgumentMatchers.any(),
+ ArgumentMatchers.any())
+
+ Mockito.doAnswer(prepareRecordsAnswer(listOf()))
+ .`when`(healthConnectManager)
+ .readRecords(
+ ArgumentMatchers.argThat<ReadRecordsRequestUsingFilters<Record>> { request ->
+ request.forDataType(dataType = StepsCadenceRecord::class.java)
+ },
+ ArgumentMatchers.any(),
+ ArgumentMatchers.any())
+
+ val expectedFormattedEntry =
+ healthDataEntryFormatter.format(stepsRecord, showDataOrigin = true)
+ val result = loadDataEntriesUseCase.invoke(input)
+ assertThat(result is UseCaseResults.Success).isTrue()
+ assertThat((result as UseCaseResults.Success).data)
+ .containsExactlyElementsIn(listOf(expectedFormattedEntry))
+ }
+
+ @Test
+ fun invoke_whenLoadEntriesHelperUseCaseFails_returnsFailure() = runTest {
+ val sleepDate = LocalDate.of(2021, 9, 13)
+
+ Mockito.doAnswer(prepareFailureAnswer())
+ .`when`(healthConnectManager)
+ .readRecords<StepsRecord>(
+ ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any())
+
+ val input =
+ LoadDataEntriesInput(
+ permissionType = HealthPermissionType.SLEEP,
+ packageName = null,
+ displayedStartTime = sleepDate.toInstantAtStartOfDay(),
+ period = DateNavigationPeriod.PERIOD_DAY,
+ showDataOrigin = true)
+
+ val result = loadDataEntriesUseCase.invoke(input)
+ assertThat(result is UseCaseResults.Failed).isTrue()
+ assertThat((result as UseCaseResults.Failed).exception is HealthConnectException).isTrue()
+ assertThat((result.exception as HealthConnectException).errorCode)
+ .isEqualTo(HealthConnectException.ERROR_UNKNOWN)
+ }
+
+ private fun prepareRecordsAnswer(records: List<Record>): (InvocationOnMock) -> Nothing? {
+ val answer = { args: InvocationOnMock ->
+ val receiver = args.arguments[2] as OutcomeReceiver<ReadRecordsResponse<Record>, *>
+ receiver.onResult(ReadRecordsResponse(records, -1))
+ null
+ }
+ return answer
+ }
+
+ private fun prepareFailureAnswer(): (InvocationOnMock) -> Nothing? {
+ val answer = { args: InvocationOnMock ->
+ val receiver =
+ args.arguments[2] as OutcomeReceiver<List<LocalDate>, HealthConnectException>
+ receiver.onError(HealthConnectException(HealthConnectException.ERROR_UNKNOWN))
+ null
+ }
+ return answer
+ }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/data/entries/api/LoadSleepDataUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/data/entries/api/LoadEntriesHelperUseCaseTest.kt
index 1cf0f107..8ced864c 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/data/entries/api/LoadSleepDataUseCaseTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/data/entries/api/LoadEntriesHelperUseCaseTest.kt
@@ -1,3 +1,16 @@
+/**
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
package com.android.healthconnect.controller.tests.data.entries.api
import android.content.Context
@@ -5,19 +18,17 @@ import android.health.connect.HealthConnectManager
import android.health.connect.ReadRecordsRequestUsingFilters
import android.health.connect.ReadRecordsResponse
import android.health.connect.TimeInstantRangeFilter
-import android.health.connect.datatypes.Record
import android.health.connect.datatypes.SleepSessionRecord
import android.os.OutcomeReceiver
import androidx.test.platform.app.InstrumentationRegistry
import com.android.healthconnect.controller.data.entries.api.LoadDataEntriesInput
import com.android.healthconnect.controller.data.entries.api.LoadEntriesHelper
-import com.android.healthconnect.controller.data.entries.api.LoadSleepDataUseCase
import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod
import com.android.healthconnect.controller.dataentries.formatters.shared.HealthDataEntryFormatter
import com.android.healthconnect.controller.permissions.data.HealthPermissionType
-import com.android.healthconnect.controller.shared.usecase.UseCaseResults
import com.android.healthconnect.controller.tests.utils.getMetaData
import com.android.healthconnect.controller.tests.utils.setLocale
+import com.android.healthconnect.controller.tests.utils.verifySleepSessionListsEqual
import com.android.healthconnect.controller.utils.atStartOfDay
import com.google.common.truth.Truth.assertThat
import dagger.hilt.android.testing.HiltAndroidRule
@@ -27,7 +38,6 @@ import java.time.ZoneId
import java.util.Locale
import java.util.TimeZone
import javax.inject.Inject
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -37,23 +47,20 @@ import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers
import org.mockito.Captor
import org.mockito.Mockito
-import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import org.mockito.invocation.InvocationOnMock
@OptIn(ExperimentalCoroutinesApi::class)
@HiltAndroidTest
-class LoadSleepDataUseCaseTest {
+class LoadEntriesHelperUseCaseTest {
@get:Rule val hiltRule = HiltAndroidRule(this)
private val healthConnectManager: HealthConnectManager =
Mockito.mock(HealthConnectManager::class.java)
- private lateinit var context: Context
-
@Inject lateinit var healthDataEntryFormatter: HealthDataEntryFormatter
- private lateinit var loadSleepDataUseCase: LoadSleepDataUseCase
+
+ private lateinit var context: Context
private lateinit var loadEntriesHelper: LoadEntriesHelper
@Captor
@@ -67,65 +74,63 @@ class LoadSleepDataUseCaseTest {
hiltRule.inject()
loadEntriesHelper =
LoadEntriesHelper(context, healthDataEntryFormatter, healthConnectManager)
- loadSleepDataUseCase = LoadSleepDataUseCase(Dispatchers.Main, loadEntriesHelper)
}
- @Test
- fun loadSleepDataUseCase_withinDay_returnsListOfRecords_sortedByDescendingStartTime() =
- runTest {
- TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("UTC")))
-
- val startTime = Instant.parse("2023-06-12T22:30:00Z").atStartOfDay()
- val input =
- LoadDataEntriesInput(
- displayedStartTime = startTime,
- packageName = null,
- period = DateNavigationPeriod.PERIOD_DAY,
- showDataOrigin = true,
- permissionType = HealthPermissionType.SLEEP)
-
- val expectedTimeRangeFilter =
- loadEntriesHelper.getTimeFilter(startTime, DateNavigationPeriod.PERIOD_DAY, true)
-
- Mockito.doAnswer(prepareDaySleepAnswer())
- .`when`(healthConnectManager)
- .readRecords(
- ArgumentMatchers.any(ReadRecordsRequestUsingFilters::class.java),
- ArgumentMatchers.any(),
- ArgumentMatchers.any())
+ // TODO (b/309288325) add tests for other permission types
- val actual = loadSleepDataUseCase.invoke(input)
- val expected =
- listOf(
- SleepSessionRecord.Builder(
- getMetaData(),
- Instant.parse("2023-06-12T22:30:00Z"),
- Instant.parse("2023-06-13T07:45:00Z"))
- .build(),
- SleepSessionRecord.Builder(
- getMetaData(),
- Instant.parse("2023-06-12T21:00:00Z"),
- Instant.parse("2023-06-12T21:20:00Z"))
- .build(),
- SleepSessionRecord.Builder(
- getMetaData(),
- Instant.parse("2023-06-12T16:00:00Z"),
- Instant.parse("2023-06-12T17:45:00Z"))
- .build(),
- )
-
- verify(healthConnectManager, times(1))
- .readRecords(
- requestCaptor.capture(), ArgumentMatchers.any(), ArgumentMatchers.any())
- assertThat((requestCaptor.value.timeRangeFilter as TimeInstantRangeFilter).startTime)
- .isEqualTo(expectedTimeRangeFilter.startTime)
- assertThat((requestCaptor.value.timeRangeFilter as TimeInstantRangeFilter).endTime)
- .isEqualTo(expectedTimeRangeFilter.endTime)
- assertThat((requestCaptor.value.timeRangeFilter as TimeInstantRangeFilter).isBounded)
- .isEqualTo(expectedTimeRangeFilter.isBounded)
- assertThat(actual is UseCaseResults.Success).isTrue()
- verifySleepSessionListsEqual((actual as UseCaseResults.Success).data, expected)
- }
+ @Test
+ fun loadSleepData_withinDay_returnsListOfRecords_sortedByDescendingStartTime() = runTest {
+ TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("UTC")))
+
+ val startTime = Instant.parse("2023-06-12T22:30:00Z").atStartOfDay()
+ val input =
+ LoadDataEntriesInput(
+ displayedStartTime = startTime,
+ packageName = null,
+ period = DateNavigationPeriod.PERIOD_DAY,
+ showDataOrigin = true,
+ permissionType = HealthPermissionType.SLEEP)
+
+ val expectedTimeRangeFilter =
+ loadEntriesHelper.getTimeFilter(startTime, DateNavigationPeriod.PERIOD_DAY, true)
+
+ Mockito.doAnswer(prepareDaySleepAnswer())
+ .`when`(healthConnectManager)
+ .readRecords(
+ ArgumentMatchers.any(ReadRecordsRequestUsingFilters::class.java),
+ ArgumentMatchers.any(),
+ ArgumentMatchers.any())
+
+ val actual = loadEntriesHelper.readRecords(input)
+ val expected =
+ listOf(
+ SleepSessionRecord.Builder(
+ getMetaData(),
+ Instant.parse("2023-06-12T22:30:00Z"),
+ Instant.parse("2023-06-13T07:45:00Z"))
+ .build(),
+ SleepSessionRecord.Builder(
+ getMetaData(),
+ Instant.parse("2023-06-12T21:00:00Z"),
+ Instant.parse("2023-06-12T21:20:00Z"))
+ .build(),
+ SleepSessionRecord.Builder(
+ getMetaData(),
+ Instant.parse("2023-06-12T16:00:00Z"),
+ Instant.parse("2023-06-12T17:45:00Z"))
+ .build(),
+ )
+
+ Mockito.verify(healthConnectManager, Mockito.times(1))
+ .readRecords(requestCaptor.capture(), ArgumentMatchers.any(), ArgumentMatchers.any())
+ assertThat((requestCaptor.value.timeRangeFilter as TimeInstantRangeFilter).startTime)
+ .isEqualTo(expectedTimeRangeFilter.startTime)
+ assertThat((requestCaptor.value.timeRangeFilter as TimeInstantRangeFilter).endTime)
+ .isEqualTo(expectedTimeRangeFilter.endTime)
+ assertThat((requestCaptor.value.timeRangeFilter as TimeInstantRangeFilter).isBounded)
+ .isEqualTo(expectedTimeRangeFilter.isBounded)
+ verifySleepSessionListsEqual(actual, expected)
+ }
@Test
fun loadSleepDataUseCase_withinWeek_returnsListOfRecords_sortedByDescendingStartTime() =
@@ -151,7 +156,7 @@ class LoadSleepDataUseCaseTest {
ArgumentMatchers.any(),
ArgumentMatchers.any())
- val actual = loadSleepDataUseCase.invoke(input)
+ val actual = loadEntriesHelper.readRecords(input)
val expected =
listOf(
SleepSessionRecord.Builder(
@@ -180,7 +185,7 @@ class LoadSleepDataUseCaseTest {
Instant.parse("2023-06-13T07:45:00Z"))
.build())
- verify(healthConnectManager, times(1))
+ Mockito.verify(healthConnectManager, Mockito.times(1))
.readRecords(
requestCaptor.capture(), ArgumentMatchers.any(), ArgumentMatchers.any())
assertThat((requestCaptor.value.timeRangeFilter as TimeInstantRangeFilter).startTime)
@@ -189,8 +194,7 @@ class LoadSleepDataUseCaseTest {
.isEqualTo(expectedTimeRangeFilter.endTime)
assertThat((requestCaptor.value.timeRangeFilter as TimeInstantRangeFilter).isBounded)
.isEqualTo(expectedTimeRangeFilter.isBounded)
- assertThat(actual is UseCaseResults.Success).isTrue()
- verifySleepSessionListsEqual((actual as UseCaseResults.Success).data, expected)
+ verifySleepSessionListsEqual(actual, expected)
}
@Test
@@ -217,7 +221,7 @@ class LoadSleepDataUseCaseTest {
ArgumentMatchers.any(),
ArgumentMatchers.any())
- val actual = loadSleepDataUseCase.invoke(input)
+ val actual = loadEntriesHelper.readRecords(input)
val expected =
listOf(
SleepSessionRecord.Builder(
@@ -251,7 +255,7 @@ class LoadSleepDataUseCaseTest {
Instant.parse("2023-06-13T07:45:00Z"))
.build())
- verify(healthConnectManager, times(1))
+ Mockito.verify(healthConnectManager, Mockito.times(1))
.readRecords(
requestCaptor.capture(), ArgumentMatchers.any(), ArgumentMatchers.any())
assertThat((requestCaptor.value.timeRangeFilter as TimeInstantRangeFilter).startTime)
@@ -260,26 +264,8 @@ class LoadSleepDataUseCaseTest {
.isEqualTo(expectedTimeRangeFilter.endTime)
assertThat((requestCaptor.value.timeRangeFilter as TimeInstantRangeFilter).isBounded)
.isEqualTo(expectedTimeRangeFilter.isBounded)
- assertThat(actual is UseCaseResults.Success).isTrue()
- verifySleepSessionListsEqual((actual as UseCaseResults.Success).data, expected)
- }
-
- private fun verifySleepSessionListsEqual(
- actual: List<Record>,
- expected: List<SleepSessionRecord>
- ) {
- assertThat(actual.size).isEqualTo(expected.size)
- for ((index, element) in actual.withIndex()) {
- val expectedElement = expected[index]
- val actualElement = element as SleepSessionRecord
-
- assertThat(actualElement.startTime).isEqualTo(expectedElement.startTime)
- assertThat(actualElement.endTime).isEqualTo(expectedElement.endTime)
- assertThat(actualElement.notes).isEqualTo(expectedElement.notes)
- assertThat(actualElement.title).isEqualTo(expectedElement.title)
- assertThat(actualElement.stages).isEqualTo(expectedElement.stages)
+ verifySleepSessionListsEqual(actual, expected)
}
- }
private fun prepareDaySleepAnswer():
(InvocationOnMock) -> ReadRecordsResponse<SleepSessionRecord> {
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/datasources/DataSourcesFragmentTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/datasources/DataSourcesFragmentTest.kt
index 3b5f49ed..e1fb751d 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/datasources/DataSourcesFragmentTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/datasources/DataSourcesFragmentTest.kt
@@ -16,7 +16,7 @@
package com.android.healthconnect.controller.tests.datasources
import android.health.connect.HealthDataCategory
-import android.os.Bundle
+import androidx.core.os.bundleOf
import androidx.lifecycle.MutableLiveData
import androidx.test.espresso.Espresso.onIdle
import androidx.test.espresso.Espresso.onView
@@ -28,6 +28,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import com.android.healthconnect.controller.R
+import com.android.healthconnect.controller.categories.HealthDataCategoriesFragment.Companion.CATEGORY_KEY
import com.android.healthconnect.controller.data.entries.FormattedEntry
import com.android.healthconnect.controller.datasources.AggregationCardInfo
import com.android.healthconnect.controller.datasources.DataSourcesFragment
@@ -50,6 +51,8 @@ import com.android.healthconnect.controller.tests.utils.di.FakeAppUtils
import com.android.healthconnect.controller.tests.utils.launchFragment
import com.android.healthconnect.controller.tests.utils.setLocale
import com.android.healthconnect.controller.tests.utils.whenever
+import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
+import com.android.healthconnect.controller.utils.logging.PageName
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
@@ -65,6 +68,10 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito
+import org.mockito.kotlin.atLeast
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.reset
+import org.mockito.kotlin.verify
@UninstallModules(AppUtilsModule::class)
@HiltAndroidTest
@@ -75,6 +82,7 @@ class DataSourcesFragmentTest {
@BindValue
val dataSourcesViewModel: DataSourcesViewModel = Mockito.mock(DataSourcesViewModel::class.java)
@BindValue val appUtils: AppUtils = FakeAppUtils()
+ @BindValue val healthConnectLogger: HealthConnectLogger = mock<HealthConnectLogger>()
@Before
fun setup() {
@@ -88,6 +96,7 @@ class DataSourcesFragmentTest {
@After
fun tearDown() {
(appUtils as FakeAppUtils).reset()
+ reset(healthConnectLogger)
}
@Test
@@ -104,7 +113,7 @@ class DataSourcesFragmentTest {
whenever(dataSourcesViewModel.updatedAggregationCardsData).then {
MutableLiveData(AggregationCardsState.WithData(true, listOf()))
}
- launchFragment<DataSourcesFragment>(Bundle())
+ launchFragment<DataSourcesFragment>(bundleOf(CATEGORY_KEY to HealthDataCategory.ACTIVITY))
onIdle()
onView(withText("Activity")).check(matches(isDisplayed()))
@@ -133,6 +142,9 @@ class DataSourcesFragmentTest {
allOf(
hasDescendant(withText("2")),
hasDescendant(withText(TEST_APP_NAME_2))))))
+
+ verify(healthConnectLogger, atLeast(1)).setPageId(PageName.DATA_SOURCES_PAGE)
+ verify(healthConnectLogger, atLeast(1)).logPageImpression()
}
@Test
@@ -167,7 +179,7 @@ class DataSourcesFragmentTest {
"1234 steps", "1234 steps", "TestApp"),
Instant.parse("2022-10-19T07:06:05.432Z")))))
}
- launchFragment<DataSourcesFragment>(Bundle())
+ launchFragment<DataSourcesFragment>(bundleOf(CATEGORY_KEY to HealthDataCategory.ACTIVITY))
onView(withText("Activity")).check(matches(isDisplayed()))
onView(withText("Data totals")).check(matches(isDisplayed()))
@@ -231,7 +243,7 @@ class DataSourcesFragmentTest {
"1234 steps", "1234 steps", "TestApp"),
Instant.parse("2020-10-19T07:06:05.432Z")))))
}
- launchFragment<DataSourcesFragment>(Bundle())
+ launchFragment<DataSourcesFragment>(bundleOf(CATEGORY_KEY to HealthDataCategory.ACTIVITY))
onView(withText("Data totals")).check(matches(isDisplayed()))
onView(withText("1234 steps")).check(matches(isDisplayed()))
onView(withText("October 19, 2020")).check(matches(isDisplayed()))
@@ -272,7 +284,7 @@ class DataSourcesFragmentTest {
Instant.parse("2022-10-19T08:05:00.00Z")))))
}
- launchFragment<DataSourcesFragment>(Bundle())
+ launchFragment<DataSourcesFragment>(bundleOf(CATEGORY_KEY to HealthDataCategory.SLEEP))
onView(withText("Sleep")).check(matches(isDisplayed()))
onView(withText("Data totals")).check(matches(isDisplayed()))
@@ -339,7 +351,7 @@ class DataSourcesFragmentTest {
Instant.parse("2020-10-19T08:05:00.00Z")))))
}
- launchFragment<DataSourcesFragment>(Bundle())
+ launchFragment<DataSourcesFragment>(bundleOf(CATEGORY_KEY to HealthDataCategory.SLEEP))
onView(withText("Sleep")).check(matches(isDisplayed()))
onView(withText("Data totals")).check(matches(isDisplayed()))
@@ -406,7 +418,7 @@ class DataSourcesFragmentTest {
Instant.parse("2021-01-01T08:05:00.00Z")))))
}
- launchFragment<DataSourcesFragment>(Bundle())
+ launchFragment<DataSourcesFragment>(bundleOf(CATEGORY_KEY to HealthDataCategory.SLEEP))
onView(withText("Sleep")).check(matches(isDisplayed()))
onView(withText("Data totals")).check(matches(isDisplayed()))
@@ -450,7 +462,7 @@ class DataSourcesFragmentTest {
whenever(dataSourcesViewModel.updatedAggregationCardsData).then {
MutableLiveData(AggregationCardsState.WithData(true, listOf()))
}
- launchFragment<DataSourcesFragment>(Bundle())
+ launchFragment<DataSourcesFragment>(bundleOf(CATEGORY_KEY to HealthDataCategory.ACTIVITY))
onView(withText("Activity")).check(matches(isDisplayed()))
onView(withText("No app sources")).check(matches(isDisplayed()))
@@ -477,7 +489,7 @@ class DataSourcesFragmentTest {
whenever(dataSourcesViewModel.updatedAggregationCardsData).then {
MutableLiveData(AggregationCardsState.WithData(true, listOf()))
}
- launchFragment<DataSourcesFragment>(Bundle())
+ launchFragment<DataSourcesFragment>(bundleOf(CATEGORY_KEY to HealthDataCategory.ACTIVITY))
onIdle()
onView(withText("Activity")).check(matches(isDisplayed()))
@@ -523,7 +535,7 @@ class DataSourcesFragmentTest {
MutableLiveData(AggregationCardsState.WithData(true, listOf()))
}
(appUtils as FakeAppUtils).setDefaultApp(TEST_APP_PACKAGE_NAME)
- launchFragment<DataSourcesFragment>(Bundle())
+ launchFragment<DataSourcesFragment>(bundleOf(CATEGORY_KEY to HealthDataCategory.ACTIVITY))
onIdle()
onView(withText("Activity")).check(matches(isDisplayed()))
@@ -574,7 +586,7 @@ class DataSourcesFragmentTest {
whenever(dataSourcesViewModel.updatedAggregationCardsData).then {
MutableLiveData(AggregationCardsState.Loading(false))
}
- launchFragment<DataSourcesFragment>(Bundle())
+ launchFragment<DataSourcesFragment>(bundleOf(CATEGORY_KEY to HealthDataCategory.ACTIVITY))
onView(withId(R.id.progress_indicator)).check(matches(isDisplayed()))
}
@@ -594,7 +606,7 @@ class DataSourcesFragmentTest {
whenever(dataSourcesViewModel.updatedAggregationCardsData).then {
MutableLiveData(AggregationCardsState.WithData(true, listOf()))
}
- launchFragment<DataSourcesFragment>(Bundle())
+ launchFragment<DataSourcesFragment>(bundleOf(CATEGORY_KEY to HealthDataCategory.ACTIVITY))
onIdle()
onView(withId(R.id.error_view)).check(matches(isDisplayed()))
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/datasources/api/LoadLastDateWithPriorityDataUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/datasources/api/LoadLastDateWithPriorityDataUseCaseTest.kt
new file mode 100644
index 00000000..5395158d
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/datasources/api/LoadLastDateWithPriorityDataUseCaseTest.kt
@@ -0,0 +1,517 @@
+package com.android.healthconnect.controller.tests.datasources.api
+
+import android.content.Context
+import android.health.connect.HealthConnectException
+import android.health.connect.HealthConnectManager
+import android.health.connect.ReadRecordsRequestUsingFilters
+import android.health.connect.ReadRecordsResponse
+import android.health.connect.datatypes.Record
+import android.health.connect.datatypes.StepsRecord
+import android.os.OutcomeReceiver
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.healthconnect.controller.data.entries.api.LoadEntriesHelper
+import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod
+import com.android.healthconnect.controller.dataentries.formatters.shared.HealthDataEntryFormatter
+import com.android.healthconnect.controller.datasources.api.LoadLastDateWithPriorityDataUseCase
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType
+import com.android.healthconnect.controller.shared.HealthPermissionToDatatypeMapper
+import com.android.healthconnect.controller.shared.usecase.UseCaseResults
+import com.android.healthconnect.controller.tests.utils.CoroutineTestRule
+import com.android.healthconnect.controller.tests.utils.TEST_APP
+import com.android.healthconnect.controller.tests.utils.TEST_APP_2
+import com.android.healthconnect.controller.tests.utils.TEST_APP_3
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME_2
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME_3
+import com.android.healthconnect.controller.tests.utils.TestTimeSource
+import com.android.healthconnect.controller.tests.utils.di.FakeLoadPriorityListUseCase
+import com.android.healthconnect.controller.tests.utils.forDataType
+import com.android.healthconnect.controller.tests.utils.fromDataSource
+import com.android.healthconnect.controller.tests.utils.fromTimeRange
+import com.android.healthconnect.controller.tests.utils.getRandomRecord
+import com.android.healthconnect.controller.tests.utils.setLocale
+import com.android.healthconnect.controller.utils.toInstantAtStartOfDay
+import com.google.common.truth.Truth.assertThat
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import java.time.Instant
+import java.time.LocalDate
+import java.time.ZoneId
+import java.util.Locale
+import java.util.TimeZone
+import javax.inject.Inject
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito
+import org.mockito.invocation.InvocationOnMock
+import org.mockito.kotlin.any
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verifyZeroInteractions
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@HiltAndroidTest
+class LoadLastDateWithPriorityDataUseCaseTest {
+
+ @get:Rule val hiltRule = HiltAndroidRule(this)
+ @get:Rule val coroutineTestRule = CoroutineTestRule()
+
+ private lateinit var loadEntriesHelper: LoadEntriesHelper
+ private val loadPriorityListUseCase = FakeLoadPriorityListUseCase()
+ private val healthConnectManager = Mockito.mock(HealthConnectManager::class.java)
+
+ private lateinit var loadLastDateWithPriorityDataUseCase: LoadLastDateWithPriorityDataUseCase
+ private lateinit var context: Context
+ private val timeSource = TestTimeSource
+
+ @Inject lateinit var healthDataEntryFormatter: HealthDataEntryFormatter
+
+ @Before
+ fun setup() {
+ context = InstrumentationRegistry.getInstrumentation().context
+ context.setLocale(Locale.US)
+ TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("UTC")))
+ hiltRule.inject()
+ loadEntriesHelper =
+ LoadEntriesHelper(context, healthDataEntryFormatter, healthConnectManager)
+ loadLastDateWithPriorityDataUseCase =
+ LoadLastDateWithPriorityDataUseCase(
+ healthConnectManager,
+ loadEntriesHelper,
+ loadPriorityListUseCase,
+ timeSource,
+ Dispatchers.Main)
+ }
+
+ @After
+ fun tearDown() {
+ loadPriorityListUseCase.reset()
+ timeSource.reset()
+ }
+
+ @Test
+ fun emptyPriorityList_doesNotInvokeEntriesUseCase_returnsNull() = runTest {
+ loadPriorityListUseCase.updatePriorityList(listOf())
+
+ val result = loadLastDateWithPriorityDataUseCase.invoke(HealthPermissionType.STEPS)
+ assertThat(result is UseCaseResults.Success).isTrue()
+ assertThat((result as UseCaseResults.Success).data).isNull()
+ verifyZeroInteractions(healthConnectManager)
+ }
+
+ @Test
+ fun onePriorityApp_noActivityDates_returnsNull() = runTest {
+ loadPriorityListUseCase.updatePriorityList(listOf(TEST_APP))
+
+ mockQueryActivityDatesAnswer(listOf())
+
+ val result = loadLastDateWithPriorityDataUseCase.invoke(HealthPermissionType.STEPS)
+ assertThat(result is UseCaseResults.Success).isTrue()
+ assertThat((result as UseCaseResults.Success).data).isNull()
+ Mockito.verify(healthConnectManager, times(0)).readRecords<StepsRecord>(any(), any(), any())
+ }
+
+ @Test
+ fun onePriorityApp_noData_returnsNull() = runTest {
+ val now = Instant.parse("2023-10-20T12:00:00Z")
+ timeSource.setNow(now)
+
+ val dateWithData = LocalDate.of(2023, 10, 10)
+ loadPriorityListUseCase.updatePriorityList(listOf(TEST_APP))
+ mockQueryActivityDatesAnswer(listOf(dateWithData))
+
+ mockReadRecordsResult(
+ packageName = TEST_APP_PACKAGE_NAME,
+ healthPermissionType = HealthPermissionType.STEPS,
+ queryDate = dateWithData,
+ numRecords = 0)
+
+ val result = loadLastDateWithPriorityDataUseCase.invoke(HealthPermissionType.STEPS)
+ assertThat(result is UseCaseResults.Success).isTrue()
+ assertThat((result as UseCaseResults.Success).data).isNull()
+ }
+
+ @Test
+ fun onePriorityApp_onlyDataOlderThan1Month_returnsNull() = runTest {
+ val now = Instant.parse("2023-11-01T12:00:00Z")
+ timeSource.setNow(now)
+
+ val dateWithData = LocalDate.of(2023, 9, 10)
+ loadPriorityListUseCase.updatePriorityList(listOf(TEST_APP))
+ mockReadRecordsResult(
+ packageName = TEST_APP_PACKAGE_NAME,
+ healthPermissionType = HealthPermissionType.STEPS,
+ queryDate = dateWithData,
+ numRecords = 2)
+
+ mockQueryActivityDatesAnswer(listOf(dateWithData))
+
+ val result = loadLastDateWithPriorityDataUseCase.invoke(HealthPermissionType.STEPS)
+ assertThat(result is UseCaseResults.Success).isTrue()
+ assertThat((result as UseCaseResults.Success).data).isNull()
+ }
+
+ @Test
+ fun multiplePriorityApps_withData_returnsMostRecentDateWithPriorityData() = runTest {
+ val now = Instant.parse("2023-11-07T12:00:00Z")
+ timeSource.setNow(now)
+ loadPriorityListUseCase.updatePriorityList(listOf(TEST_APP, TEST_APP_2, TEST_APP_3))
+
+ // datesWithin1MonthOfToday = 2023-11-1, 2023-11-2
+ // min = 2023-11-1
+ val activityDates =
+ listOf(
+ // Too old
+ LocalDate.of(2023, 10, 1),
+ // Too old
+ LocalDate.of(2023, 7, 11),
+ LocalDate.of(2023, 11, 1),
+ LocalDate.of(2023, 11, 2),
+ // Valid date but none of the priority apps have data then
+ LocalDate.of(2023, 11, 4),
+ // Future date with data, not included because we only
+ // query for data within the last 30 days
+ LocalDate.of(2024, 11, 2))
+
+ mockQueryActivityDatesAnswer(activityDates)
+ val minDateWithin1Month = LocalDate.of(2023, 11, 1)
+
+ mockReadRecordsResult(
+ packageName = TEST_APP_PACKAGE_NAME,
+ healthPermissionType = HealthPermissionType.STEPS,
+ queryDate = LocalDate.of(2023, 10, 1),
+ recordDates = listOf(LocalDate.of(2023, 10, 1)))
+
+ mockReadRecordsResult(
+ packageName = TEST_APP_PACKAGE_NAME,
+ healthPermissionType = HealthPermissionType.STEPS,
+ queryDate = minDateWithin1Month,
+ numRecords = 2)
+
+ mockReadRecordsResult(
+ packageName = TEST_APP_PACKAGE_NAME_2,
+ healthPermissionType = HealthPermissionType.STEPS,
+ queryDate = minDateWithin1Month,
+ recordDates = listOf(LocalDate.of(2023, 11, 1)))
+
+ mockReadRecordsResult(
+ packageName = TEST_APP_PACKAGE_NAME_3,
+ healthPermissionType = HealthPermissionType.STEPS,
+ queryDate = minDateWithin1Month,
+ recordDates = listOf(LocalDate.of(2023, 11, 1), LocalDate.of(2023, 11, 2)))
+
+ val result = loadLastDateWithPriorityDataUseCase.invoke(HealthPermissionType.STEPS)
+ assertThat(result is UseCaseResults.Success).isTrue()
+ assertThat((result as UseCaseResults.Success).data).isEqualTo(LocalDate.of(2023, 11, 2))
+ }
+
+ @Test
+ fun multipleStepsPriorityApps_withDataAndWithout_returnsMostRecentDate() = runTest {
+ val now = Instant.parse("2023-11-07T12:00:00Z")
+ timeSource.setNow(now)
+ loadPriorityListUseCase.updatePriorityList(listOf(TEST_APP, TEST_APP_2, TEST_APP_3))
+
+ mockQueryActivityDatesAnswer(
+ listOf(
+ // Too old
+ LocalDate.of(2023, 10, 1),
+ // Too old
+ LocalDate.of(2023, 7, 11),
+ LocalDate.of(2023, 11, 1),
+ LocalDate.of(2023, 11, 2),
+ // In the future
+ LocalDate.of(2024, 11, 12)))
+ val minDateWithin1Month = LocalDate.of(2023, 11, 1)
+
+ mockReadRecordsResult(
+ packageName = TEST_APP_PACKAGE_NAME,
+ healthPermissionType = HealthPermissionType.STEPS,
+ queryDate = minDateWithin1Month,
+ numRecords = 0)
+ mockReadRecordsResult(
+ packageName = TEST_APP_PACKAGE_NAME_2,
+ healthPermissionType = HealthPermissionType.STEPS,
+ queryDate = minDateWithin1Month,
+ numRecords = 1)
+ mockReadRecordsResult(
+ packageName = TEST_APP_PACKAGE_NAME_3,
+ healthPermissionType = HealthPermissionType.STEPS,
+ queryDate = minDateWithin1Month,
+ recordDates = listOf(LocalDate.of(2023, 11, 1), LocalDate.of(2023, 11, 2)))
+
+ val result = loadLastDateWithPriorityDataUseCase.invoke(HealthPermissionType.STEPS)
+ assertThat(result is UseCaseResults.Success).isTrue()
+ assertThat((result as UseCaseResults.Success).data).isEqualTo(LocalDate.of(2023, 11, 2))
+ }
+
+ @Test
+ fun multipleDistancePriorityApps_withDataAndWithout_returnsMostRecentDate() = runTest {
+ val now = Instant.parse("2023-10-14T12:00:00Z")
+ timeSource.setNow(now)
+ val healthPermissionType = HealthPermissionType.DISTANCE
+ loadPriorityListUseCase.updatePriorityList(listOf(TEST_APP, TEST_APP_2, TEST_APP_3))
+
+ mockQueryActivityDatesAnswer(
+ listOf(
+ // Too old
+ LocalDate.of(2023, 8, 1),
+ // Too old
+ LocalDate.of(2023, 6, 11),
+ LocalDate.of(2023, 10, 1),
+ LocalDate.of(2023, 10, 2),
+ LocalDate.of(2023, 10, 12),
+ LocalDate.of(2023, 9, 26),
+ // Too old
+ LocalDate.of(2023, 3, 1),
+ // Too old
+ LocalDate.of(2021, 8, 12),
+ // Too old
+ LocalDate.of(2023, 7, 2)))
+
+ val minDateWithin1Month = LocalDate.of(2023, 9, 26)
+
+ mockReadRecordsResult(
+ packageName = TEST_APP_PACKAGE_NAME,
+ healthPermissionType = healthPermissionType,
+ queryDate = minDateWithin1Month,
+ numRecords = 0)
+ mockReadRecordsResult(
+ packageName = TEST_APP_PACKAGE_NAME_2,
+ healthPermissionType = healthPermissionType,
+ queryDate = minDateWithin1Month,
+ numRecords = 1)
+ mockReadRecordsResult(
+ packageName = TEST_APP_PACKAGE_NAME_3,
+ healthPermissionType = healthPermissionType,
+ queryDate = minDateWithin1Month,
+ recordDates = listOf(LocalDate.of(2023, 10, 12), minDateWithin1Month))
+
+ val result = loadLastDateWithPriorityDataUseCase.invoke(healthPermissionType)
+ assertThat(result is UseCaseResults.Success).isTrue()
+ assertThat((result as UseCaseResults.Success).data).isEqualTo(LocalDate.of(2023, 10, 12))
+ }
+
+ @Test
+ fun multipleCaloriesPriorityApps_withDataAndWithout_returnsMostRecentDate() = runTest {
+ val now = Instant.parse("2023-10-14T12:00:00Z")
+ timeSource.setNow(now)
+ val healthPermissionType = HealthPermissionType.TOTAL_CALORIES_BURNED
+ loadPriorityListUseCase.updatePriorityList(listOf(TEST_APP, TEST_APP_2, TEST_APP_3))
+
+ mockQueryActivityDatesAnswer(
+ listOf(
+ LocalDate.of(2023, 10, 1),
+ // in the future
+ LocalDate.of(2023, 7, 11),
+ LocalDate.of(2023, 10, 12),
+ LocalDate.of(2023, 10, 14),
+ // Too old
+ LocalDate.of(2023, 4, 1),
+ // Too old
+ LocalDate.of(2021, 9, 13),
+ // Too old
+ LocalDate.of(2023, 8, 2)))
+
+ val minDateWithin1Month = LocalDate.of(2023, 10, 1)
+
+ mockReadRecordsResult(
+ packageName = TEST_APP_PACKAGE_NAME,
+ healthPermissionType = healthPermissionType,
+ queryDate = minDateWithin1Month,
+ numRecords = 1)
+
+ mockReadRecordsResult(
+ packageName = TEST_APP_PACKAGE_NAME_2,
+ healthPermissionType = healthPermissionType,
+ queryDate = minDateWithin1Month,
+ recordDates =
+ listOf(LocalDate.of(2023, 10, 12), LocalDate.of(2023, 10, 14), minDateWithin1Month))
+
+ mockReadRecordsResult(
+ packageName = TEST_APP_PACKAGE_NAME_3,
+ healthPermissionType = healthPermissionType,
+ queryDate = minDateWithin1Month,
+ numRecords = 1)
+
+ val result = loadLastDateWithPriorityDataUseCase.invoke(healthPermissionType)
+ assertThat(result is UseCaseResults.Success).isTrue()
+ assertThat((result as UseCaseResults.Success).data).isEqualTo(LocalDate.of(2023, 10, 14))
+ }
+
+ @Test
+ fun multipleSleepPriorityApps_withDataAndWithout_returnsMostRecentDate() = runTest {
+ val now = Instant.parse("2023-10-14T12:00:00Z")
+ timeSource.setNow(now)
+ val healthPermissionType = HealthPermissionType.SLEEP
+ loadPriorityListUseCase.updatePriorityList(listOf(TEST_APP, TEST_APP_2, TEST_APP_3))
+
+ mockQueryActivityDatesAnswer(
+ listOf(
+ LocalDate.of(2023, 10, 1),
+ // in the future
+ LocalDate.of(2023, 7, 11),
+ LocalDate.of(2023, 10, 12),
+ LocalDate.of(2023, 10, 14),
+ // Too old
+ LocalDate.of(2023, 4, 1),
+ // Too old
+ LocalDate.of(2021, 9, 13),
+ // Too old
+ LocalDate.of(2023, 8, 2)))
+
+ val minDateWithin1Month = LocalDate.of(2023, 10, 1)
+
+ mockReadRecordsResult(
+ packageName = TEST_APP_PACKAGE_NAME,
+ healthPermissionType = healthPermissionType,
+ queryDate = minDateWithin1Month,
+ numRecords = 1)
+
+ mockReadRecordsResult(
+ packageName = TEST_APP_PACKAGE_NAME_2,
+ healthPermissionType = healthPermissionType,
+ queryDate = minDateWithin1Month,
+ recordDates =
+ listOf(LocalDate.of(2023, 10, 12), LocalDate.of(2023, 10, 14), minDateWithin1Month))
+
+ mockReadRecordsResult(
+ packageName = TEST_APP_PACKAGE_NAME_3,
+ healthPermissionType = healthPermissionType,
+ queryDate = minDateWithin1Month,
+ numRecords = 1)
+
+ val result = loadLastDateWithPriorityDataUseCase.invoke(healthPermissionType)
+ assertThat(result is UseCaseResults.Success).isTrue()
+ assertThat((result as UseCaseResults.Success).data).isEqualTo(LocalDate.of(2023, 10, 14))
+ }
+
+ @Test
+ fun whenLoadPriorityListFails_returnsFailure() = runTest {
+ loadPriorityListUseCase.setFailure("Exception")
+ val result = loadLastDateWithPriorityDataUseCase.invoke(HealthPermissionType.STEPS)
+
+ verifyZeroInteractions(healthConnectManager)
+ Mockito.verify(healthConnectManager, times(0)).readRecords<StepsRecord>(any(), any(), any())
+ assertThat(result is UseCaseResults.Failed).isTrue()
+ assertThat((result as UseCaseResults.Failed).exception.message).isEqualTo("Exception")
+ }
+
+ @Test
+ fun whenLoadEntriesHelperFails_returnsFailure() = runTest {
+ val now = Instant.parse("2023-10-14T12:00:00Z")
+ timeSource.setNow(now)
+ loadPriorityListUseCase.updatePriorityList(listOf(TEST_APP))
+ Mockito.doAnswer(prepareFailureAnswer())
+ .`when`(healthConnectManager)
+ .readRecords<StepsRecord>(any(), any(), any())
+ mockQueryActivityDatesAnswer(listOf(LocalDate.of(2023, 10, 5)))
+
+ val result = loadLastDateWithPriorityDataUseCase.invoke(HealthPermissionType.STEPS)
+
+ assertThat(result is UseCaseResults.Failed).isTrue()
+ assertThat((result as UseCaseResults.Failed).exception is HealthConnectException).isTrue()
+ assertThat((result.exception as HealthConnectException).errorCode)
+ .isEqualTo(HealthConnectException.ERROR_UNKNOWN)
+ }
+
+ @Test
+ fun whenQueryActivityDatesFails_returnsFailure() = runTest {
+ loadPriorityListUseCase.updatePriorityList(listOf(TEST_APP))
+ mockQueryActivityDatesError()
+
+ val result = loadLastDateWithPriorityDataUseCase.invoke(HealthPermissionType.STEPS)
+
+ assertThat(result is UseCaseResults.Failed).isTrue()
+ assertThat((result as UseCaseResults.Failed).exception is HealthConnectException).isTrue()
+ assertThat((result.exception as HealthConnectException).errorCode)
+ .isEqualTo(HealthConnectException.ERROR_UNKNOWN)
+ }
+
+ private fun mockReadRecordsResult(
+ packageName: String,
+ healthPermissionType: HealthPermissionType,
+ queryDate: LocalDate,
+ numRecords: Int
+ ) {
+ mockReadRecordsResult(
+ packageName, healthPermissionType, queryDate, List(numRecords) { queryDate })
+ }
+
+ private fun mockReadRecordsResult(
+ packageName: String,
+ healthPermissionType: HealthPermissionType,
+ queryDate: LocalDate,
+ recordDates: List<LocalDate>
+ ) {
+ val timeFilterRange =
+ loadEntriesHelper.getTimeFilter(
+ queryDate.toInstantAtStartOfDay(),
+ DateNavigationPeriod.PERIOD_MONTH,
+ endTimeExclusive = true)
+ val dataTypes = HealthPermissionToDatatypeMapper.getDataTypes(healthPermissionType)
+ val records =
+ recordDates.map { date -> getRandomRecord(healthPermissionType, date) }.toList()
+
+ dataTypes.map { dataType ->
+ Mockito.doAnswer(prepareRecordsAnswer(records))
+ .`when`(healthConnectManager)
+ .readRecords(
+ ArgumentMatchers.argThat<ReadRecordsRequestUsingFilters<Record>> { request ->
+ request.fromDataSource(packageName) &&
+ request.fromTimeRange(timeFilterRange) &&
+ request.forDataType(dataType)
+ },
+ ArgumentMatchers.any(),
+ ArgumentMatchers.any())
+ }
+ }
+
+ private fun mockQueryActivityDatesAnswer(datesList: List<LocalDate>) {
+ Mockito.doAnswer(prepareActivityDatesAnswer(datesList))
+ .`when`(healthConnectManager)
+ .queryActivityDates(
+ ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any())
+ }
+
+ private fun mockQueryActivityDatesError() {
+ Mockito.doAnswer(prepareFailureAnswer())
+ .`when`(healthConnectManager)
+ .queryActivityDates(
+ ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any())
+ }
+
+ private fun prepareActivityDatesAnswer(
+ datesList: List<LocalDate>
+ ): (InvocationOnMock) -> Nothing? {
+ val answer = { args: InvocationOnMock ->
+ val receiver = args.arguments[2] as OutcomeReceiver<List<LocalDate>, *>
+ receiver.onResult(datesList)
+ null
+ }
+ return answer
+ }
+
+ private fun prepareFailureAnswer(): (InvocationOnMock) -> Nothing? {
+ val answer = { args: InvocationOnMock ->
+ val receiver =
+ args.arguments[2] as OutcomeReceiver<List<LocalDate>, HealthConnectException>
+ receiver.onError(HealthConnectException(HealthConnectException.ERROR_UNKNOWN))
+ null
+ }
+ return answer
+ }
+
+ private fun prepareRecordsAnswer(records: List<Record>): (InvocationOnMock) -> Nothing? {
+ val answer = { args: InvocationOnMock ->
+ val receiver = args.arguments[2] as OutcomeReceiver<ReadRecordsResponse<Record>, *>
+ receiver.onResult(ReadRecordsResponse(records, -1))
+ null
+ }
+ return answer
+ }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/datasources/api/LoadMostRecentAggregationsUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/datasources/api/LoadMostRecentAggregationsUseCaseTest.kt
index 3f6c58a5..6cf9625b 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/datasources/api/LoadMostRecentAggregationsUseCaseTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/datasources/api/LoadMostRecentAggregationsUseCaseTest.kt
@@ -1,29 +1,23 @@
package com.android.healthconnect.controller.tests.datasources.api
import android.content.Context
-import android.health.connect.HealthConnectException
-import android.health.connect.HealthConnectManager
import android.health.connect.HealthDataCategory
-import android.health.connect.datatypes.Record
-import android.health.connect.datatypes.SleepSessionRecord
-import android.os.OutcomeReceiver
import androidx.test.platform.app.InstrumentationRegistry
import com.android.healthconnect.controller.data.entries.FormattedEntry
import com.android.healthconnect.controller.datasources.AggregationCardInfo
import com.android.healthconnect.controller.datasources.api.LoadMostRecentAggregationsUseCase
import com.android.healthconnect.controller.permissions.data.HealthPermissionType
-import com.android.healthconnect.controller.shared.HealthPermissionToDatatypeMapper
import com.android.healthconnect.controller.shared.usecase.UseCaseResults
import com.android.healthconnect.controller.tests.utils.di.FakeLoadDataAggregationsUseCase
-import com.android.healthconnect.controller.tests.utils.di.FakeLoadSleepDataUseCase
-import com.android.healthconnect.controller.tests.utils.getMetaData
+import com.android.healthconnect.controller.tests.utils.di.FakeLoadLastDateWithPriorityDataUseCase
+import com.android.healthconnect.controller.tests.utils.di.FakeSleepSessionHelper
import com.android.healthconnect.controller.tests.utils.setLocale
-import com.android.healthconnect.controller.utils.atStartOfDay
+import com.android.healthconnect.controller.utils.randomInstant
+import com.android.healthconnect.controller.utils.toInstantAtStartOfDay
import com.android.healthconnect.controller.utils.toLocalDate
import com.google.common.truth.Truth.assertThat
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
-import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.util.Locale
@@ -35,11 +29,7 @@ import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
-import org.mockito.ArgumentMatchers.any
-import org.mockito.ArgumentMatchers.eq
-import org.mockito.Mockito
import org.mockito.MockitoAnnotations
-import org.mockito.invocation.InvocationOnMock
@OptIn(ExperimentalCoroutinesApi::class)
@HiltAndroidTest
@@ -58,80 +48,48 @@ class LoadMostRecentAggregationsUseCaseTest {
private lateinit var context: Context
private lateinit var loadMostRecentAggregationsUseCase: LoadMostRecentAggregationsUseCase
- private val healthConnectManager: HealthConnectManager =
- Mockito.mock(HealthConnectManager::class.java)
private val loadDataAggregationsUseCase = FakeLoadDataAggregationsUseCase()
- private val loadSleepDataUseCase = FakeLoadSleepDataUseCase()
-
- private val STEPS_DATE_1 = Instant.parse("2022-10-24T18:40:13.00Z")
- private val STEPS_DATE_2 = Instant.parse("2022-10-26T13:23:19.00Z")
- private val STEPS_DATE_3 = Instant.parse("2023-04-09T19:45:12.00Z")
-
- private val DISTANCE_DATE_1 = Instant.parse("2022-05-12T14:15:22.00Z")
- private val DISTANCE_DATE_2 = Instant.parse("2022-11-03T07:20:18.00Z")
- private val DISTANCE_DATE_3 = Instant.parse("2023-02-08T16:42:29.00Z")
-
- private val CALORIES_DATE_1 = Instant.parse("2022-07-26T11:33:10.00Z")
- private val CALORIES_DATE_2 = Instant.parse("2022-09-30T12:55:44.00Z")
- private val CALORIES_DATE_3 = Instant.parse("2023-04-19T20:25:37.00Z")
-
- private val SLEEP_DATE_1 = Instant.parse("2022-03-17T12:34:56.00Z")
- private val SLEEP_DATE_2 = Instant.parse("2022-09-21T14:45:37.00Z")
- private val SLEEP_DATE_3 = Instant.parse("2023-02-13T23:00:00.00Z")
+ private val loadLastDateWithPriorityDataUseCase = FakeLoadLastDateWithPriorityDataUseCase()
+ private val sleepSessionHelper = FakeSleepSessionHelper()
private val stepsAggregation = formattedAggregation("100 steps")
private val distanceAggregation = formattedAggregation("1.5 km")
private val caloriesAggregation = formattedAggregation("1590 kcal")
- private val sleepAggregation = formattedAggregation("11h 5m")
-
- private val stepsRecordTypes =
- HealthPermissionToDatatypeMapper.getDataTypes(HealthPermissionType.STEPS)
- private val distanceRecordTypes =
- HealthPermissionToDatatypeMapper.getDataTypes(HealthPermissionType.DISTANCE)
- private val caloriesRecordTypes =
- HealthPermissionToDatatypeMapper.getDataTypes(HealthPermissionType.TOTAL_CALORIES_BURNED)
- private val sleepRecordTypes =
- HealthPermissionToDatatypeMapper.getDataTypes(HealthPermissionType.SLEEP)
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
- context = InstrumentationRegistry.getInstrumentation().context
- context.setLocale(Locale.US)
hiltRule.inject()
context = InstrumentationRegistry.getInstrumentation().context
+ context.setLocale(Locale.US)
TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("UTC")))
loadMostRecentAggregationsUseCase =
LoadMostRecentAggregationsUseCase(
- healthConnectManager,
loadDataAggregationsUseCase,
- loadSleepDataUseCase,
+ loadLastDateWithPriorityDataUseCase,
+ sleepSessionHelper,
Dispatchers.Main)
}
@After
fun tearDown() {
loadDataAggregationsUseCase.reset()
- loadSleepDataUseCase.reset()
+ loadLastDateWithPriorityDataUseCase.reset()
+ sleepSessionHelper.reset()
}
@Test
- fun loadMostRecentAggregations_forActivity_returnsMostRecent_stepsDistanceCalories() = runTest {
- Mockito.doAnswer(prepareStepsAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(stepsRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareDistanceAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(distanceRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareCaloriesAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(caloriesRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(sleepRecordTypes), any(), any())
+ fun loadMostRecentAggregations_forActivity_returnsInOrder_stepsDistanceCalories() = runTest {
+ val stepsDate = LocalDate.of(2023, 4, 9)
+ val distanceDate = LocalDate.of(2023, 2, 8)
+ val caloriesDate = LocalDate.of(2023, 4, 19)
+
+ loadLastDateWithPriorityDataUseCase.setLastDateWithPriorityDataForHealthPermissionType(
+ HealthPermissionType.STEPS, stepsDate)
+ loadLastDateWithPriorityDataUseCase.setLastDateWithPriorityDataForHealthPermissionType(
+ HealthPermissionType.DISTANCE, distanceDate)
+ loadLastDateWithPriorityDataUseCase.setLastDateWithPriorityDataForHealthPermissionType(
+ HealthPermissionType.TOTAL_CALORIES_BURNED, caloriesDate)
loadDataAggregationsUseCase.updateAggregationResponses(
listOf(stepsAggregation, distanceAggregation, caloriesAggregation))
@@ -142,803 +100,134 @@ class LoadMostRecentAggregationsUseCaseTest {
.isEqualTo(
listOf(
AggregationCardInfo(
- HealthPermissionType.STEPS, stepsAggregation, STEPS_DATE_3.atStartOfDay()),
+ HealthPermissionType.STEPS,
+ stepsAggregation,
+ stepsDate.toInstantAtStartOfDay()),
AggregationCardInfo(
HealthPermissionType.DISTANCE,
distanceAggregation,
- DISTANCE_DATE_3.atStartOfDay()),
+ distanceDate.toInstantAtStartOfDay()),
AggregationCardInfo(
HealthPermissionType.TOTAL_CALORIES_BURNED,
caloriesAggregation,
- CALORIES_DATE_3.atStartOfDay()),
+ caloriesDate.toInstantAtStartOfDay()),
))
}
- // Case 1 - start and end times on same day
- @Test
- fun loadMostRecentAggregations_forSleep_allSessionStartAndEndOnSameDay() = runTest {
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(stepsRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(distanceRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(caloriesRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareSleepAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(sleepRecordTypes), any(), any())
-
- // lastDayWithSleepData = 2023-02-13
-
- // 2h
- val SLEEP_SESSION_1_START_DATE = Instant.parse("2023-02-13T16:00:00.00Z")
- val SLEEP_SESSION_1_END_DATE = Instant.parse("2023-02-13T18:00:00.00Z")
-
- // 5h 45m
- val SLEEP_SESSION_2_START_DATE = Instant.parse("2023-02-13T17:30:00.00Z")
- val SLEEP_SESSION_2_END_DATE = Instant.parse("2023-02-13T23:15:00.00Z")
-
- // 7h 20m
- val SLEEP_SESSION_3_START_DATE = Instant.parse("2023-02-13T01:00:00.00Z")
- val SLEEP_SESSION_3_END_DATE = Instant.parse("2023-02-13T08:20:00.00Z")
-
- loadSleepDataUseCase.updateSleepData(
- SLEEP_SESSION_1_START_DATE.toLocalDate(),
- getSleepSessionRecords(
- listOf(
- Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE),
- Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE),
- Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_3_END_DATE))))
-
- val expectedSleepAggregation = formattedAggregation("14h 5m")
- loadDataAggregationsUseCase.updateAggregationResponses(listOf(expectedSleepAggregation))
-
- val result = loadMostRecentAggregationsUseCase.invoke(HealthDataCategory.SLEEP)
- assertThat(result is UseCaseResults.Success).isTrue()
- assertThat((result as UseCaseResults.Success).data)
- .isEqualTo(
- listOf(
- AggregationCardInfo(
- HealthPermissionType.SLEEP,
- expectedSleepAggregation,
- SLEEP_SESSION_1_START_DATE.atStartOfDay(),
- SLEEP_SESSION_1_END_DATE.atStartOfDay())))
- }
-
- // Case 1 - start and end times on same day
- // Edge case - additional sleep session starts on past date (not day before)
- // And finishes on last day with data
@Test
- fun loadMostRecentAggregations_forSleep_allSessionStartAndEndOnSameDay_withSessionUnknownStart() =
+ fun loadMostRecentAggregations_forActivity_whenNoStepsData_returnsInOrder_DistanceCalories() =
runTest {
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(stepsRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(distanceRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(caloriesRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareSleepAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(sleepRecordTypes), any(), any())
+ val stepsDate = null
+ val distanceDate = LocalDate.of(2023, 2, 8)
+ val caloriesDate = LocalDate.of(2023, 4, 19)
- // lastDayWithSleepData = 2023-02-13
+ loadLastDateWithPriorityDataUseCase.setLastDateWithPriorityDataForHealthPermissionType(
+ HealthPermissionType.STEPS, stepsDate)
+ loadLastDateWithPriorityDataUseCase.setLastDateWithPriorityDataForHealthPermissionType(
+ HealthPermissionType.DISTANCE, distanceDate)
+ loadLastDateWithPriorityDataUseCase.setLastDateWithPriorityDataForHealthPermissionType(
+ HealthPermissionType.TOTAL_CALORIES_BURNED, caloriesDate)
- // 2h
- val SLEEP_SESSION_1_START_DATE = Instant.parse("2023-02-13T16:00:00.00Z")
- val SLEEP_SESSION_1_END_DATE = Instant.parse("2023-02-13T18:00:00.00Z")
+ loadDataAggregationsUseCase.updateAggregationResponses(
+ listOf(distanceAggregation, caloriesAggregation))
- // 5h 45m
- val SLEEP_SESSION_2_START_DATE = Instant.parse("2023-02-13T17:30:00.00Z")
- val SLEEP_SESSION_2_END_DATE = Instant.parse("2023-02-13T23:15:00.00Z")
-
- // 7h 20m
- val SLEEP_SESSION_3_START_DATE = Instant.parse("2023-02-13T01:00:00.00Z")
- val SLEEP_SESSION_3_END_DATE = Instant.parse("2023-02-13T08:20:00.00Z")
-
- // Past sleep session ending on lastDayWithData, overlaps with above data by 1 hour
- // 3d 7h 20m
- val SLEEP_SESSION_4_START_DATE = Instant.parse("2023-02-10T01:00:00.00Z")
- val SLEEP_SESSION_4_END_DATE = Instant.parse("2023-02-13T08:20:00.00Z")
-
- loadSleepDataUseCase.updateSleepData(
- SLEEP_SESSION_1_START_DATE.toLocalDate(),
- getSleepSessionRecords(
- listOf(
- Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE),
- Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE),
- Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_3_END_DATE))))
-
- loadSleepDataUseCase.updateSleepData(
- SLEEP_SESSION_4_START_DATE.toLocalDate(),
- getSleepSessionRecords(
- listOf(Pair(SLEEP_SESSION_4_START_DATE, SLEEP_SESSION_4_END_DATE))))
-
- val expectedSleepAggregation = formattedAggregation("15h 5m")
- loadDataAggregationsUseCase.updateAggregationResponses(listOf(expectedSleepAggregation))
-
- val result = loadMostRecentAggregationsUseCase.invoke(HealthDataCategory.SLEEP)
+ val result = loadMostRecentAggregationsUseCase.invoke(HealthDataCategory.ACTIVITY)
assertThat(result is UseCaseResults.Success).isTrue()
assertThat((result as UseCaseResults.Success).data)
.isEqualTo(
listOf(
AggregationCardInfo(
- HealthPermissionType.SLEEP,
- expectedSleepAggregation,
- SLEEP_SESSION_1_START_DATE.atStartOfDay(),
- SLEEP_SESSION_1_END_DATE.atStartOfDay())))
- }
-
- // Case 1 - start and end times on same day
- // Edge case - additional sleep session starts on past date (not day before)
- // And finishes on future date
- @Test
- fun loadMostRecentAggregations_forSleep_allSessionStartAndEndOnSameDay_withSessionUnknownStartEnd() =
- runTest {
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(stepsRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(distanceRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(caloriesRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareSleepAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(sleepRecordTypes), any(), any())
-
- // lastDayWithSleepData = 2023-02-13
-
- // 2h
- val SLEEP_SESSION_1_START_DATE = Instant.parse("2023-02-13T16:00:00.00Z")
- val SLEEP_SESSION_1_END_DATE = Instant.parse("2023-02-13T18:00:00.00Z")
-
- // 5h 45m
- val SLEEP_SESSION_2_START_DATE = Instant.parse("2023-02-13T17:30:00.00Z")
- val SLEEP_SESSION_2_END_DATE = Instant.parse("2023-02-13T23:15:00.00Z")
-
- // 7h 20m
- val SLEEP_SESSION_3_START_DATE = Instant.parse("2023-02-13T01:00:00.00Z")
- val SLEEP_SESSION_3_END_DATE = Instant.parse("2023-02-13T08:20:00.00Z")
-
- // Past sleep session, overlaps completely with above data
- // 5d 7h 20m
- val SLEEP_SESSION_4_START_DATE = Instant.parse("2023-02-10T01:00:00.00Z")
- val SLEEP_SESSION_4_END_DATE = Instant.parse("2023-02-15T08:20:00.00Z")
-
- loadSleepDataUseCase.updateSleepData(
- SLEEP_SESSION_1_START_DATE.toLocalDate(),
- getSleepSessionRecords(
- listOf(
- Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE),
- Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE),
- Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_3_END_DATE))))
-
- loadSleepDataUseCase.updateSleepData(
- SLEEP_SESSION_4_START_DATE.toLocalDate(),
- getSleepSessionRecords(
- listOf(Pair(SLEEP_SESSION_4_START_DATE, SLEEP_SESSION_4_END_DATE))))
-
- // minStartTime = SLEEP_SESSION_3_START_DATE
- // maxEndTime = SLEEP_SESSION_2_END_DATE
- // Total time = 1am to 23:15 = 22h 15m
- val expectedSleepAggregation = formattedAggregation("22h 15m")
- loadDataAggregationsUseCase.updateAggregationResponses(listOf(expectedSleepAggregation))
-
- val result = loadMostRecentAggregationsUseCase.invoke(HealthDataCategory.SLEEP)
- assertThat(result is UseCaseResults.Success).isTrue()
- assertThat((result as UseCaseResults.Success).data)
- .isEqualTo(
- listOf(
- AggregationCardInfo(
- HealthPermissionType.SLEEP,
- expectedSleepAggregation,
- SLEEP_SESSION_1_START_DATE.atStartOfDay(),
- SLEEP_SESSION_1_END_DATE.atStartOfDay())))
- }
-
- // Case 2 - At least one session starts on Day 1 and finishes on Day 2 or later
- @Test
- fun loadMostRecentAggregations_forSleep_atLeastOneSessionStartsYesterdayAndEndsToday() =
- runTest {
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(stepsRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(distanceRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(caloriesRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareSleepAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(sleepRecordTypes), any(), any())
-
- // lastDayWithSleepData = 2023-02-13
-
- // 2h
- val SLEEP_SESSION_1_START_DATE = Instant.parse("2023-02-13T16:00:00.00Z")
- val SLEEP_SESSION_1_END_DATE = Instant.parse("2023-02-13T18:00:00.00Z")
-
- // 5h 45m
- val SLEEP_SESSION_2_START_DATE = Instant.parse("2023-02-13T17:30:00.00Z")
- val SLEEP_SESSION_2_END_DATE = Instant.parse("2023-02-13T23:15:00.00Z")
-
- // 9h 20m
- val SLEEP_SESSION_3_START_DATE = Instant.parse("2023-02-12T23:00:00.00Z")
- val SLEEP_SESSION_3_END_DATE = Instant.parse("2023-02-13T08:20:00.00Z")
-
- // Should be partially included in aggregation
- val SLEEP_SESSION_4_START_DATE = Instant.parse("2023-02-12T16:00:00.00Z")
- val SLEEP_SESSION_4_END_DATE = Instant.parse("2023-02-12T23:20:00.00Z")
-
- // Should not be included in aggregation
- val SLEEP_SESSION_5_START_DATE = Instant.parse("2023-02-12T12:00:00.00Z")
- val SLEEP_SESSION_5_END_DATE = Instant.parse("2023-02-12T14:20:00.00Z")
-
- loadSleepDataUseCase.updateSleepData(
- SLEEP_SESSION_1_START_DATE.toLocalDate(),
- getSleepSessionRecords(
- listOf(
- Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE),
- Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE))))
-
- loadSleepDataUseCase.updateSleepData(
- SLEEP_SESSION_3_START_DATE.toLocalDate(),
- getSleepSessionRecords(
- listOf(
- Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_3_END_DATE),
- Pair(SLEEP_SESSION_4_START_DATE, SLEEP_SESSION_4_END_DATE),
- Pair(SLEEP_SESSION_5_START_DATE, SLEEP_SESSION_5_END_DATE))))
-
- // minStartTime = SLEEP_SESSION_3_START_DATE
- // maxEndTime = SLEEP_SESSION_2_END_DATE
- // Total time = 12 Feb, 23:00 - 13 Feb 23:15 = 24h 15m
- val expectedSleepAggregation = formattedAggregation("24h 15m")
- loadDataAggregationsUseCase.updateAggregationResponses(listOf(expectedSleepAggregation))
-
- val result = loadMostRecentAggregationsUseCase.invoke(HealthDataCategory.SLEEP)
- assertThat(result is UseCaseResults.Success).isTrue()
- assertThat((result as UseCaseResults.Success).data)
- .isEqualTo(
- listOf(
+ HealthPermissionType.DISTANCE,
+ distanceAggregation,
+ distanceDate.toInstantAtStartOfDay()),
AggregationCardInfo(
- HealthPermissionType.SLEEP,
- expectedSleepAggregation,
- SLEEP_SESSION_3_START_DATE.atStartOfDay(),
- SLEEP_SESSION_1_END_DATE.atStartOfDay())))
+ HealthPermissionType.TOTAL_CALORIES_BURNED,
+ caloriesAggregation,
+ caloriesDate.toInstantAtStartOfDay()),
+ ))
}
- // Case 2 - At least one session starts on Day 1 and finishes on Day 2 or later
- // with gaps
@Test
- fun loadMostRecentAggregations_forSleep_atLeastOneSessionStartsYesterdayAndEndsToday_withGaps() =
+ fun loadMostRecentAggregations_forActivity_whenNoDistanceData_returnsInOrder_StepsCalories() =
runTest {
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(stepsRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(distanceRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(caloriesRecordTypes), any(), any())
+ val stepsDate = LocalDate.of(2023, 4, 9)
+ val distanceDate = null
+ val caloriesDate = LocalDate.of(2023, 4, 19)
- Mockito.doAnswer(prepareSleepAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(sleepRecordTypes), any(), any())
+ loadLastDateWithPriorityDataUseCase.setLastDateWithPriorityDataForHealthPermissionType(
+ HealthPermissionType.STEPS, stepsDate)
+ loadLastDateWithPriorityDataUseCase.setLastDateWithPriorityDataForHealthPermissionType(
+ HealthPermissionType.DISTANCE, distanceDate)
+ loadLastDateWithPriorityDataUseCase.setLastDateWithPriorityDataForHealthPermissionType(
+ HealthPermissionType.TOTAL_CALORIES_BURNED, caloriesDate)
- // lastDayWithSleepData = 2023-02-13
+ loadDataAggregationsUseCase.updateAggregationResponses(
+ listOf(stepsAggregation, caloriesAggregation))
- // 2h
- val SLEEP_SESSION_1_START_DATE = Instant.parse("2023-02-13T18:00:00.00Z")
- val SLEEP_SESSION_1_END_DATE = Instant.parse("2023-02-13T20:00:00.00Z")
-
- // 2h 15m
- val SLEEP_SESSION_2_START_DATE = Instant.parse("2023-02-13T12:30:00.00Z")
- val SLEEP_SESSION_2_END_DATE = Instant.parse("2023-02-13T14:45:00.00Z")
-
- // 5h 20m
- val SLEEP_SESSION_3_START_DATE = Instant.parse("2023-02-12T20:00:00.00Z")
- val SLEEP_SESSION_3_END_DATE = Instant.parse("2023-02-13T01:20:00.00Z")
-
- // Should be partially included in aggregation
- val SLEEP_SESSION_4_START_DATE = Instant.parse("2023-02-12T16:00:00.00Z")
- val SLEEP_SESSION_4_END_DATE = Instant.parse("2023-02-12T23:20:00.00Z")
-
- loadSleepDataUseCase.updateSleepData(
- SLEEP_SESSION_1_START_DATE.toLocalDate(),
- getSleepSessionRecords(
- listOf(
- Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE),
- Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE))))
-
- loadSleepDataUseCase.updateSleepData(
- SLEEP_SESSION_3_START_DATE.toLocalDate(),
- getSleepSessionRecords(
- listOf(
- Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_3_END_DATE),
- Pair(SLEEP_SESSION_4_START_DATE, SLEEP_SESSION_4_END_DATE))))
-
- // minStartTime = SLEEP_SESSION_3_START_DATE
- // maxEndTime = SLEEP_SESSION_1_END_DATE
- // Total time = 2h + 2h 15m + 5h 20m = 9h 35m
- val expectedSleepAggregation = formattedAggregation("9h 35m")
- loadDataAggregationsUseCase.updateAggregationResponses(listOf(expectedSleepAggregation))
-
- val result = loadMostRecentAggregationsUseCase.invoke(HealthDataCategory.SLEEP)
+ val result = loadMostRecentAggregationsUseCase.invoke(HealthDataCategory.ACTIVITY)
assertThat(result is UseCaseResults.Success).isTrue()
assertThat((result as UseCaseResults.Success).data)
.isEqualTo(
listOf(
AggregationCardInfo(
- HealthPermissionType.SLEEP,
- expectedSleepAggregation,
- SLEEP_SESSION_3_START_DATE.atStartOfDay(),
- SLEEP_SESSION_1_END_DATE.atStartOfDay())))
- }
-
- // Case 2 - At least one session starts on Day 1 and finishes on Day 2 or later
- // Edge case - additional sleep session starts on past date
- // and finishes on lastDayWithData
- @Test
- fun loadMostRecentAggregations_forSleep_atLeastOneSessionStartsYesterdayAndEndsToday_withSessionUnknownStart() =
- runTest {
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(stepsRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(distanceRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(caloriesRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareSleepAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(sleepRecordTypes), any(), any())
-
- // secondToLastDayWithSleepData = 2023-02-12
- // lastDayWithSleepData = 2023-02-13
-
- // 2h
- val SLEEP_SESSION_1_START_DATE = Instant.parse("2023-02-13T16:00:00.00Z")
- val SLEEP_SESSION_1_END_DATE = Instant.parse("2023-02-13T18:00:00.00Z")
-
- // 5h 45m
- val SLEEP_SESSION_2_START_DATE = Instant.parse("2023-02-13T17:30:00.00Z")
- val SLEEP_SESSION_2_END_DATE = Instant.parse("2023-02-13T23:15:00.00Z")
-
- // 10h 20m
- val SLEEP_SESSION_3_START_DATE = Instant.parse("2023-02-12T22:00:00.00Z")
- val SLEEP_SESSION_3_END_DATE = Instant.parse("2023-02-13T08:20:00.00Z")
-
- // Should be partially included in aggregation
- val SLEEP_SESSION_4_START_DATE = Instant.parse("2023-02-12T16:00:00.00Z")
- val SLEEP_SESSION_4_END_DATE = Instant.parse("2023-02-12T23:20:00.00Z")
-
- // Should be partially included in aggregation
- val SLEEP_SESSION_5_START_DATE = Instant.parse("2023-02-10T12:00:00.00Z")
- val SLEEP_SESSION_5_END_DATE = Instant.parse("2023-02-13T14:20:00.00Z")
-
- loadSleepDataUseCase.updateSleepData(
- SLEEP_SESSION_1_START_DATE.toLocalDate(),
- getSleepSessionRecords(
- listOf(
- Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE),
- Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE))))
-
- loadSleepDataUseCase.updateSleepData(
- SLEEP_SESSION_3_START_DATE.toLocalDate(),
- getSleepSessionRecords(
- listOf(
- Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_3_END_DATE),
- Pair(SLEEP_SESSION_4_START_DATE, SLEEP_SESSION_4_END_DATE))))
-
- loadSleepDataUseCase.updateSleepData(
- SLEEP_SESSION_5_START_DATE.toLocalDate(),
- getSleepSessionRecords(
- listOf(Pair(SLEEP_SESSION_5_START_DATE, SLEEP_SESSION_5_END_DATE))))
-
- // minStartTime = SLEEP_SESSION_3_START_DATE
- // maxEndTime = SLEEP_SESSION_2_END_DATE
- // Total time = 12 Feb, 22:00 - 13 Feb 23:15 = 25h 15m
- val expectedSleepAggregation = formattedAggregation("25h 15m")
- loadDataAggregationsUseCase.updateAggregationResponses(listOf(expectedSleepAggregation))
-
- val result = loadMostRecentAggregationsUseCase.invoke(HealthDataCategory.SLEEP)
- assertThat(result is UseCaseResults.Success).isTrue()
- assertThat((result as UseCaseResults.Success).data)
- .isEqualTo(
- listOf(
+ HealthPermissionType.STEPS,
+ stepsAggregation,
+ stepsDate.toInstantAtStartOfDay()),
AggregationCardInfo(
- HealthPermissionType.SLEEP,
- expectedSleepAggregation,
- SLEEP_SESSION_3_START_DATE.atStartOfDay(),
- SLEEP_SESSION_2_END_DATE.atStartOfDay())))
+ HealthPermissionType.TOTAL_CALORIES_BURNED,
+ caloriesAggregation,
+ caloriesDate.toInstantAtStartOfDay()),
+ ))
}
- // Case 2 - At least one session starts on Day 1 and finishes on Day 2 or later
- // Edge case - additional sleep session starts on Day 1
- // and finishes on unknown date
@Test
- fun loadMostRecentAggregations_forSleep_atLeastOneSessionStartsYesterdayAndEndsToday_withSessionUnknownEnd() =
+ fun loadMostRecentAggregations_forActivity_whenNoCaloriesData_returnsInOrder_StepsDistance() =
runTest {
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(stepsRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(distanceRecordTypes), any(), any())
+ val stepsDate = LocalDate.of(2023, 4, 9)
+ val distanceDate = LocalDate.of(2023, 2, 8)
+ val caloriesDate = null
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(caloriesRecordTypes), any(), any())
+ loadLastDateWithPriorityDataUseCase.setLastDateWithPriorityDataForHealthPermissionType(
+ HealthPermissionType.STEPS, stepsDate)
+ loadLastDateWithPriorityDataUseCase.setLastDateWithPriorityDataForHealthPermissionType(
+ HealthPermissionType.DISTANCE, distanceDate)
+ loadLastDateWithPriorityDataUseCase.setLastDateWithPriorityDataForHealthPermissionType(
+ HealthPermissionType.TOTAL_CALORIES_BURNED, caloriesDate)
- Mockito.doAnswer(prepareSleepAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(sleepRecordTypes), any(), any())
+ loadDataAggregationsUseCase.updateAggregationResponses(
+ listOf(stepsAggregation, distanceAggregation))
- // secondToLastDayWithSleepData = 2023-02-12
- // lastDayWithSleepData = 2023-02-13
-
- // 2h
- val SLEEP_SESSION_1_START_DATE = Instant.parse("2023-02-13T16:00:00.00Z")
- val SLEEP_SESSION_1_END_DATE = Instant.parse("2023-02-13T18:00:00.00Z")
-
- // 5h 45m
- val SLEEP_SESSION_2_START_DATE = Instant.parse("2023-02-13T17:30:00.00Z")
- val SLEEP_SESSION_2_END_DATE = Instant.parse("2023-02-13T23:15:00.00Z")
-
- // 10h 20m
- val SLEEP_SESSION_3_START_DATE = Instant.parse("2023-02-12T22:00:00.00Z")
- val SLEEP_SESSION_3_END_DATE = Instant.parse("2023-02-13T08:20:00.00Z")
-
- // Should be partially included in aggregation
- val SLEEP_SESSION_4_START_DATE = Instant.parse("2023-02-12T16:00:00.00Z")
- val SLEEP_SESSION_4_END_DATE = Instant.parse("2023-02-12T23:20:00.00Z")
-
- // Should be partially included in aggregation up to 2023-02-14T00:00
- val SLEEP_SESSION_5_START_DATE = Instant.parse("2023-02-12T12:00:00.00Z")
- val SLEEP_SESSION_5_END_DATE = Instant.parse("2023-02-18T14:20:00.00Z")
-
- val maxDate = Instant.parse("2023-02-14T00:00:00.00Z")
-
- loadSleepDataUseCase.updateSleepData(
- SLEEP_SESSION_1_START_DATE.toLocalDate(),
- getSleepSessionRecords(
- listOf(
- Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE),
- Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE))))
-
- loadSleepDataUseCase.updateSleepData(
- SLEEP_SESSION_3_START_DATE.toLocalDate(),
- getSleepSessionRecords(
- listOf(
- Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_3_END_DATE),
- Pair(SLEEP_SESSION_4_START_DATE, SLEEP_SESSION_4_END_DATE),
- Pair(SLEEP_SESSION_5_START_DATE, SLEEP_SESSION_5_END_DATE))))
-
- // minStartTime = SLEEP_SESSION_5_START_DATE
- // maxEndTime = 2023-02-14T00:00
- // Total time = 12 Feb, 12:00 - 14 Feb 00:00 = 36h
- val expectedSleepAggregation = formattedAggregation("36h")
- loadDataAggregationsUseCase.updateAggregationResponses(listOf(expectedSleepAggregation))
-
- val result = loadMostRecentAggregationsUseCase.invoke(HealthDataCategory.SLEEP)
+ val result = loadMostRecentAggregationsUseCase.invoke(HealthDataCategory.ACTIVITY)
assertThat(result is UseCaseResults.Success).isTrue()
assertThat((result as UseCaseResults.Success).data)
.isEqualTo(
listOf(
AggregationCardInfo(
- HealthPermissionType.SLEEP,
- expectedSleepAggregation,
- SLEEP_SESSION_5_START_DATE.atStartOfDay(),
- maxDate.atStartOfDay())))
- }
-
- // Case 2 - At least one session starts on Day 1 and finishes on Day 2 or later
- // Edge case - additional sleep session starts and finishes on unknown date
- @Test
- fun loadMostRecentAggregations_forSleep_atLeastOneSessionStartsYesterdayAndEndsToday_withSessionUnknownStartAndEnd() =
- runTest {
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(stepsRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(distanceRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(caloriesRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareSleepAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(sleepRecordTypes), any(), any())
-
- // secondToLastDayWithSleepData = 2023-02-12
- // lastDayWithSleepData = 2023-02-13
-
- // 2h
- val SLEEP_SESSION_1_START_DATE = Instant.parse("2023-02-13T18:00:00.00Z")
- val SLEEP_SESSION_1_END_DATE = Instant.parse("2023-02-13T20:00:00.00Z")
-
- // 2h 15m
- val SLEEP_SESSION_2_START_DATE = Instant.parse("2023-02-13T12:30:00.00Z")
- val SLEEP_SESSION_2_END_DATE = Instant.parse("2023-02-13T14:45:00.00Z")
-
- // 5h 20m
- val SLEEP_SESSION_3_START_DATE = Instant.parse("2023-02-12T20:00:00.00Z")
- val SLEEP_SESSION_3_END_DATE = Instant.parse("2023-02-13T01:20:00.00Z")
-
- // Should be partially included in aggregation
- val SLEEP_SESSION_4_START_DATE = Instant.parse("2023-02-10T16:00:00.00Z")
- val SLEEP_SESSION_4_END_DATE = Instant.parse("2023-02-20T23:20:00.00Z")
-
- loadSleepDataUseCase.updateSleepData(
- SLEEP_SESSION_1_START_DATE.toLocalDate(),
- getSleepSessionRecords(
- listOf(
- Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE),
- Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE))))
-
- loadSleepDataUseCase.updateSleepData(
- SLEEP_SESSION_3_START_DATE.toLocalDate(),
- getSleepSessionRecords(
- listOf(Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_3_END_DATE))))
-
- loadSleepDataUseCase.updateSleepData(
- SLEEP_SESSION_4_START_DATE.toLocalDate(),
- getSleepSessionRecords(
- listOf(Pair(SLEEP_SESSION_4_START_DATE, SLEEP_SESSION_4_END_DATE))))
-
- // minStartTime = SLEEP_SESSION_3_START_DATE
- // maxEndTime = SLEEP_SESSION_1_END_DATE
- // Total time = 12 Oct 20:00 - 13 Oct 20:00 = 24h
- val expectedSleepAggregation = formattedAggregation("24h")
- loadDataAggregationsUseCase.updateAggregationResponses(listOf(expectedSleepAggregation))
-
- val result = loadMostRecentAggregationsUseCase.invoke(HealthDataCategory.SLEEP)
- assertThat(result is UseCaseResults.Success).isTrue()
- assertThat((result as UseCaseResults.Success).data)
- .isEqualTo(
- listOf(
+ HealthPermissionType.STEPS,
+ stepsAggregation,
+ stepsDate.toInstantAtStartOfDay()),
AggregationCardInfo(
- HealthPermissionType.SLEEP,
- expectedSleepAggregation,
- SLEEP_SESSION_3_START_DATE.atStartOfDay(),
- SLEEP_SESSION_1_END_DATE.atStartOfDay())))
+ HealthPermissionType.DISTANCE,
+ distanceAggregation,
+ distanceDate.toInstantAtStartOfDay())))
}
- // Case 3 - The sessions from lastDayWithData cross midnight into the next day
@Test
- fun loadMostRecentAggregations_forSleep_atLeastOneSessionFinishesTomorrow() = runTest {
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(stepsRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(distanceRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(caloriesRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareSleepAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(sleepRecordTypes), any(), any())
-
- // lastDayWithSleepData = 2023-02-13
-
- // 2h
- val SLEEP_SESSION_1_START_DATE = Instant.parse("2023-02-13T18:00:00.00Z")
- val SLEEP_SESSION_1_END_DATE = Instant.parse("2023-02-13T20:00:00.00Z")
-
- // 10h 15m
- val SLEEP_SESSION_2_START_DATE = Instant.parse("2023-02-13T22:30:00.00Z")
- val SLEEP_SESSION_2_END_DATE = Instant.parse("2023-02-14T08:45:00.00Z")
-
- // 2h 20m
- val SLEEP_SESSION_3_START_DATE = Instant.parse("2023-02-13T16:00:00.00Z")
- val SLEEP_SESSION_3_END_DATE = Instant.parse("2023-02-13T18:20:00.00Z")
-
- // 10h
- val SLEEP_SESSION_4_START_DATE = Instant.parse("2023-02-13T22:00:00.00Z")
- val SLEEP_SESSION_4_END_DATE = Instant.parse("2023-02-14T08:00:00.00Z")
-
- loadSleepDataUseCase.updateSleepData(
- SLEEP_SESSION_1_START_DATE.toLocalDate(),
- getSleepSessionRecords(
- listOf(
- Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE),
- Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE))))
-
- loadSleepDataUseCase.updateSleepData(
- SLEEP_SESSION_3_START_DATE.toLocalDate(),
- getSleepSessionRecords(
- listOf(
- Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_3_END_DATE),
- Pair(SLEEP_SESSION_4_START_DATE, SLEEP_SESSION_4_END_DATE))))
-
- // minStartTime = SLEEP_SESSION_4_START_DATE
- // maxEndTime = SLEEP_SESSION_2_END_DATE
- // Total time = 13 Feb 22:00 - 14 Feb 08:45 = 10h 45m
- val expectedSleepAggregation = formattedAggregation("10h 45m")
- loadDataAggregationsUseCase.updateAggregationResponses(listOf(expectedSleepAggregation))
-
- val result = loadMostRecentAggregationsUseCase.invoke(HealthDataCategory.SLEEP)
+ fun loadMostRecentAggregations_ifNoActivityData_returnsEmptyList() = runTest {
+ val result = loadMostRecentAggregationsUseCase.invoke(HealthDataCategory.ACTIVITY)
assertThat(result is UseCaseResults.Success).isTrue()
- assertThat((result as UseCaseResults.Success).data)
- .isEqualTo(
- listOf(
- AggregationCardInfo(
- HealthPermissionType.SLEEP,
- expectedSleepAggregation,
- SLEEP_SESSION_4_START_DATE.atStartOfDay(),
- SLEEP_SESSION_2_END_DATE.atStartOfDay())))
+ assertThat((result as UseCaseResults.Success).data).isEmpty()
}
- // Case 3 - The sessions from lastDayWithData cross midnight into the next day
- // Edge case - additional sleep session starts on unknown date
- // and finishes within range
@Test
- fun loadMostRecentAggregations_forSleep_atLeastOneSessionFinishesTomorrow_withSessionUnknownStart() =
+ fun loadMostRecentAggregations_forSleep_sessionsSpanOneDay_returnsAggregationInfoForOneDay() =
runTest {
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(stepsRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(distanceRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(caloriesRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareSleepAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(sleepRecordTypes), any(), any())
-
- // lastDayWithSleepData = 2023-02-13
-
- // 2h
- val SLEEP_SESSION_1_START_DATE = Instant.parse("2023-02-13T18:00:00.00Z")
- val SLEEP_SESSION_1_END_DATE = Instant.parse("2023-02-13T20:00:00.00Z")
-
- // 10h 15m
- val SLEEP_SESSION_2_START_DATE = Instant.parse("2023-02-13T22:30:00.00Z")
- val SLEEP_SESSION_2_END_DATE = Instant.parse("2023-02-14T08:45:00.00Z")
-
- // 2h 20m
- val SLEEP_SESSION_3_START_DATE = Instant.parse("2023-02-13T16:00:00.00Z")
- val SLEEP_SESSION_3_END_DATE = Instant.parse("2023-02-13T18:20:00.00Z")
-
- // 10h
- val SLEEP_SESSION_4_START_DATE = Instant.parse("2023-02-13T22:00:00.00Z")
- val SLEEP_SESSION_4_END_DATE = Instant.parse("2023-02-14T08:00:00.00Z")
-
- // 2d 11h - should not have an effect on the aggregation
- val SLEEP_SESSION_5_START_DATE = Instant.parse("2023-02-11T22:00:00.00Z")
- val SLEEP_SESSION_5_END_DATE = Instant.parse("2023-02-14T09:00:00.00Z")
-
- loadSleepDataUseCase.updateSleepData(
- SLEEP_SESSION_1_START_DATE.toLocalDate(),
- getSleepSessionRecords(
- listOf(
- Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE),
- Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE),
- Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_3_END_DATE),
- Pair(SLEEP_SESSION_4_START_DATE, SLEEP_SESSION_4_END_DATE))))
-
- loadSleepDataUseCase.updateSleepData(
- SLEEP_SESSION_5_START_DATE.toLocalDate(),
- getSleepSessionRecords(
- listOf(Pair(SLEEP_SESSION_5_START_DATE, SLEEP_SESSION_5_END_DATE))))
-
- // minStartTime = SLEEP_SESSION_4_START_DATE
- // maxEndTime = SLEEP_SESSION_2_END_DATE
- // Total time = 13 Feb 22:00 - 14 Feb 08:45 = 10h 45m
- val expectedSleepAggregation = formattedAggregation("10h 45m")
- loadDataAggregationsUseCase.updateAggregationResponses(listOf(expectedSleepAggregation))
-
- val result = loadMostRecentAggregationsUseCase.invoke(HealthDataCategory.SLEEP)
- assertThat(result is UseCaseResults.Success).isTrue()
- assertThat((result as UseCaseResults.Success).data)
- .isEqualTo(
- listOf(
- AggregationCardInfo(
- HealthPermissionType.SLEEP,
- expectedSleepAggregation,
- SLEEP_SESSION_4_START_DATE.atStartOfDay(),
- SLEEP_SESSION_2_END_DATE.atStartOfDay())))
- }
-
- // Case 3 - The sessions from lastDayWithData cross midnight into the next day
- // Edge case - additional sleep session starts on last day with data
- // and finishes in the future
- @Test
- fun loadMostRecentAggregations_forSleep_atLeastOneSessionFinishesTomorrow_withSessionUnknownEnd() =
- runTest {
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(stepsRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(distanceRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(caloriesRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareSleepAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(sleepRecordTypes), any(), any())
-
- // lastDayWithSleepData = 2023-02-13
-
- // 2h
- val SLEEP_SESSION_1_START_DATE = Instant.parse("2023-02-13T18:00:00.00Z")
- val SLEEP_SESSION_1_END_DATE = Instant.parse("2023-02-13T20:00:00.00Z")
-
- // 10h 15m
- val SLEEP_SESSION_2_START_DATE = Instant.parse("2023-02-13T22:30:00.00Z")
- val SLEEP_SESSION_2_END_DATE = Instant.parse("2023-02-14T08:45:00.00Z")
-
- // 2h 20m
- val SLEEP_SESSION_3_START_DATE = Instant.parse("2023-02-13T16:00:00.00Z")
- val SLEEP_SESSION_3_END_DATE = Instant.parse("2023-02-13T18:20:00.00Z")
-
- // 10h
- val SLEEP_SESSION_4_START_DATE = Instant.parse("2023-02-13T22:00:00.00Z")
- val SLEEP_SESSION_4_END_DATE = Instant.parse("2023-02-14T08:00:00.00Z")
-
- // 3d 11h - determines maxEndTime
- val SLEEP_SESSION_5_START_DATE = Instant.parse("2023-02-13T22:00:00.00Z")
- val SLEEP_SESSION_5_END_DATE = Instant.parse("2023-02-16T09:00:00.00Z")
-
- val maxEndTime = Instant.parse("2023-02-15T00:00:00.00Z")
-
- loadSleepDataUseCase.updateSleepData(
- SLEEP_SESSION_1_START_DATE.toLocalDate(),
- getSleepSessionRecords(
- listOf(
- Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE),
- Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE),
- Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_3_END_DATE),
- Pair(SLEEP_SESSION_4_START_DATE, SLEEP_SESSION_4_END_DATE),
- Pair(SLEEP_SESSION_5_START_DATE, SLEEP_SESSION_5_END_DATE))))
-
- // minStartTime = SLEEP_SESSION_4_START_DATE
- // maxEndTime = 15 Feb 00:00
- // Total time = 13 Feb 22:00 - 15 Feb 00:00 = 26h
- val expectedSleepAggregation = formattedAggregation("26h")
+ val startDate = LocalDate.of(2023, 4, 5).randomInstant()
+ val endDate = LocalDate.of(2023, 4, 5).randomInstant()
+ loadLastDateWithPriorityDataUseCase.setLastDateWithPriorityDataForHealthPermissionType(
+ HealthPermissionType.SLEEP, startDate.toLocalDate())
+ sleepSessionHelper.setDatePair(startDate, endDate)
+ val expectedSleepAggregation = formattedAggregation("14h 5m")
loadDataAggregationsUseCase.updateAggregationResponses(listOf(expectedSleepAggregation))
val result = loadMostRecentAggregationsUseCase.invoke(HealthDataCategory.SLEEP)
@@ -947,73 +236,24 @@ class LoadMostRecentAggregationsUseCaseTest {
.isEqualTo(
listOf(
AggregationCardInfo(
- HealthPermissionType.SLEEP,
- expectedSleepAggregation,
- SLEEP_SESSION_4_START_DATE.atStartOfDay(),
- maxEndTime.atStartOfDay())))
+ healthPermissionType = HealthPermissionType.SLEEP,
+ aggregation = expectedSleepAggregation,
+ startDate = startDate,
+ endDate = endDate)))
}
- // Case 3 - The sessions from lastDayWithData cross midnight into the next day
- // Edge case - additional sleep session starts and ends on unknown date
@Test
- fun loadMostRecentAggregations_forSleep_atLeastOneSessionFinishesTomorrow_withSessionUnknownStartAndEnd() =
+ fun loadMostRecentAggregations_forSleep_sessionsSpanTwoDays_returnsAggregationInfoWithStartAndEndTime() =
runTest {
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(stepsRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(distanceRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(caloriesRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareSleepAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(sleepRecordTypes), any(), any())
-
- // lastDayWithSleepData = 2023-02-13
-
- // 2h
- val SLEEP_SESSION_1_START_DATE = Instant.parse("2023-02-13T18:00:00.00Z")
- val SLEEP_SESSION_1_END_DATE = Instant.parse("2023-02-13T20:00:00.00Z")
-
- // 10h 15m
- val SLEEP_SESSION_2_START_DATE = Instant.parse("2023-02-13T22:30:00.00Z")
- val SLEEP_SESSION_2_END_DATE = Instant.parse("2023-02-14T08:45:00.00Z")
+ val startDate = LocalDate.of(2023, 4, 5).randomInstant()
+ val endDate = LocalDate.of(2023, 4, 7).randomInstant()
- // 2h 20m
- val SLEEP_SESSION_3_START_DATE = Instant.parse("2023-02-13T16:00:00.00Z")
- val SLEEP_SESSION_3_END_DATE = Instant.parse("2023-02-13T18:20:00.00Z")
+ loadLastDateWithPriorityDataUseCase.setLastDateWithPriorityDataForHealthPermissionType(
+ HealthPermissionType.SLEEP, startDate.toLocalDate())
- // 10h
- val SLEEP_SESSION_4_START_DATE = Instant.parse("2023-02-13T22:00:00.00Z")
- val SLEEP_SESSION_4_END_DATE = Instant.parse("2023-02-14T08:00:00.00Z")
+ sleepSessionHelper.setDatePair(startDate, endDate)
- // 5d 11h - Should not affect aggregation
- val SLEEP_SESSION_5_START_DATE = Instant.parse("2023-02-11T22:00:00.00Z")
- val SLEEP_SESSION_5_END_DATE = Instant.parse("2023-02-16T09:00:00.00Z")
-
- loadSleepDataUseCase.updateSleepData(
- SLEEP_SESSION_1_START_DATE.toLocalDate(),
- getSleepSessionRecords(
- listOf(
- Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE),
- Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE),
- Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_3_END_DATE),
- Pair(SLEEP_SESSION_4_START_DATE, SLEEP_SESSION_4_END_DATE))))
-
- loadSleepDataUseCase.updateSleepData(
- SLEEP_SESSION_5_START_DATE.toLocalDate(),
- getSleepSessionRecords(
- listOf(Pair(SLEEP_SESSION_5_START_DATE, SLEEP_SESSION_5_END_DATE))))
-
- // minStartTime = SLEEP_SESSION_4_START_DATE
- // maxEndTime = SLEEP_SESSION_2_END_DATE
- // Total time = 13 Feb 22:00 - 14 Feb 08:45 = 10h 45m
- val expectedSleepAggregation = formattedAggregation("10h 45m")
+ val expectedSleepAggregation = formattedAggregation("36h 5m")
loadDataAggregationsUseCase.updateAggregationResponses(listOf(expectedSleepAggregation))
val result = loadMostRecentAggregationsUseCase.invoke(HealthDataCategory.SLEEP)
@@ -1022,215 +262,67 @@ class LoadMostRecentAggregationsUseCaseTest {
.isEqualTo(
listOf(
AggregationCardInfo(
- HealthPermissionType.SLEEP,
- expectedSleepAggregation,
- SLEEP_SESSION_4_START_DATE.atStartOfDay(),
- SLEEP_SESSION_2_END_DATE.atStartOfDay())))
+ healthPermissionType = HealthPermissionType.SLEEP,
+ aggregation = expectedSleepAggregation,
+ startDate = startDate,
+ endDate = endDate)))
}
@Test
- fun loadMostRecentAggregations_forSleep_returnsMostRecent_sleepSessions() = runTest {
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(stepsRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(distanceRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(caloriesRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareSleepAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(sleepRecordTypes), any(), any())
-
- // 2h
- val SLEEP_SESSION_1_START_DATE = Instant.parse("2023-02-13T16:00:00.00Z")
- val SLEEP_SESSION_1_END_DATE = Instant.parse("2023-02-13T18:00:00.00Z")
-
- // 10h 45m
- val SLEEP_SESSION_2_START_DATE = Instant.parse("2023-02-13T22:30:00.00Z")
- val SLEEP_SESSION_2_END_DATE = Instant.parse("2023-02-14T08:15:00.00Z")
-
- // 9h 20m
- val SLEEP_SESSION_3_START_DATE = Instant.parse("2023-02-13T23:00:00.00Z")
- val SLEEP_SESSION_3_END_DATE = Instant.parse("2023-02-14T08:20:00.00Z")
-
- loadDataAggregationsUseCase.updateAggregationResponses(listOf(sleepAggregation))
- loadSleepDataUseCase.updateSleepData(
- SLEEP_SESSION_1_START_DATE.toLocalDate(),
- getSleepSessionRecords(
- listOf(
- Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE),
- Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE),
- Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_3_END_DATE))))
-
+ fun loadMostRecentAggregations_ifNoSleepData_returnsEmptyList() = runTest {
val result = loadMostRecentAggregationsUseCase.invoke(HealthDataCategory.SLEEP)
assertThat(result is UseCaseResults.Success).isTrue()
- assertThat((result as UseCaseResults.Success).data)
- .isEqualTo(
- listOf(
- AggregationCardInfo(
- HealthPermissionType.SLEEP,
- sleepAggregation,
- SLEEP_SESSION_1_START_DATE.atStartOfDay(),
- SLEEP_SESSION_3_END_DATE.atStartOfDay())))
+ assertThat((result as UseCaseResults.Success).data).isEmpty()
}
@Test
- fun loadMostRecentAggregations_ifQueryActivityDatesFails_returnsFailure() = runTest {
- Mockito.doAnswer(prepareStepsAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(stepsRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareFailedDistanceAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(distanceRecordTypes), any(), any())
+ fun loadMostRecentAggregations_whenLoadLastDateWithPriorityDataFails_returnsFailure() =
+ runTest {
+ loadLastDateWithPriorityDataUseCase.setFailure("Exception")
- Mockito.doAnswer(prepareCaloriesAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(caloriesRecordTypes), any(), any())
+ val result = loadMostRecentAggregationsUseCase.invoke(HealthDataCategory.ACTIVITY)
+ assertThat(result is UseCaseResults.Failed).isTrue()
+ assertThat((result as UseCaseResults.Failed).exception.message).isEqualTo("Exception")
+ assertThat(loadDataAggregationsUseCase.invocationCount).isEqualTo(0)
+ }
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(sleepRecordTypes), any(), any())
+ @Test
+ fun loadMostRecentAggregations_ifActivityAggregationRequestFails_returnsFailure() = runTest {
+ val stepsDate = LocalDate.of(2023, 2, 13)
- loadDataAggregationsUseCase.updateAggregationResponses(
- listOf(stepsAggregation, distanceAggregation, caloriesAggregation))
+ loadLastDateWithPriorityDataUseCase.setLastDateWithPriorityDataForHealthPermissionType(
+ HealthPermissionType.STEPS, stepsDate)
+ loadDataAggregationsUseCase.setFailure("Exception")
val result = loadMostRecentAggregationsUseCase.invoke(HealthDataCategory.ACTIVITY)
assertThat(result is UseCaseResults.Failed).isTrue()
+ assertThat((result as UseCaseResults.Failed).exception.message).isEqualTo("Exception")
}
@Test
- fun loadMostRecentAggregations_ifAggregationRequestFails_returnsEmptyList() = runTest {
- Mockito.doAnswer(prepareStepsAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(stepsRecordTypes), any(), any())
+ fun loadMostRecentAggregations_ifSleepAggregationRequestFails_returnsFailure() = runTest {
+ val sleepDate = LocalDate.of(2023, 2, 13)
- Mockito.doAnswer(prepareDistanceAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(distanceRecordTypes), any(), any())
+ loadLastDateWithPriorityDataUseCase.setLastDateWithPriorityDataForHealthPermissionType(
+ HealthPermissionType.SLEEP, sleepDate)
+ loadDataAggregationsUseCase.setFailure("Exception")
- Mockito.doAnswer(prepareCaloriesAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(caloriesRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(sleepRecordTypes), any(), any())
-
- loadDataAggregationsUseCase.updateErrorResponse()
-
- val result = loadMostRecentAggregationsUseCase.invoke(HealthDataCategory.ACTIVITY)
- assertThat(result is UseCaseResults.Success).isTrue()
- assertThat((result as UseCaseResults.Success).data).isEmpty()
+ val result = loadMostRecentAggregationsUseCase.invoke(HealthDataCategory.SLEEP)
+ assertThat(result is UseCaseResults.Failed).isTrue()
+ assertThat((result as UseCaseResults.Failed).exception.message).isEqualTo("Exception")
}
@Test
- fun loadMostRecentAggregations_ifNoData_returnsEmptyList() = runTest {
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(stepsRecordTypes), any(), any())
-
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(distanceRecordTypes), any(), any())
+ fun loadMostRecentAggregations_ifSleepSessionHelperFails_returnsFailure() = runTest {
+ val sleepDate = LocalDate.of(2023, 2, 13)
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(caloriesRecordTypes), any(), any())
+ loadLastDateWithPriorityDataUseCase.setLastDateWithPriorityDataForHealthPermissionType(
+ HealthPermissionType.SLEEP, sleepDate)
+ sleepSessionHelper.setFailure("Exception")
- Mockito.doAnswer(prepareEmptyAnswer())
- .`when`(healthConnectManager)
- .queryActivityDates(eq(sleepRecordTypes), any(), any())
-
- val result = loadMostRecentAggregationsUseCase.invoke(HealthDataCategory.ACTIVITY)
- assertThat(result is UseCaseResults.Success).isTrue()
- assertThat((result as UseCaseResults.Success).data).isEmpty()
- }
-
- private fun prepareStepsAnswer(): (InvocationOnMock) -> Nothing? {
- val answer = { args: InvocationOnMock ->
- val receiver = args.arguments[2] as OutcomeReceiver<List<LocalDate>, *>
- receiver.onResult(getStepsDates())
- null
- }
- return answer
- }
-
- private fun prepareDistanceAnswer(): (InvocationOnMock) -> Nothing? {
- val answer = { args: InvocationOnMock ->
- val receiver = args.arguments[2] as OutcomeReceiver<List<LocalDate>, *>
- receiver.onResult(getDistanceDates())
- null
- }
- return answer
- }
-
- private fun prepareFailedDistanceAnswer(): (InvocationOnMock) -> Nothing? {
- val answer = { args: InvocationOnMock ->
- val receiver =
- args.arguments[2] as OutcomeReceiver<List<LocalDate>, HealthConnectException>
- receiver.onError(HealthConnectException(HealthConnectException.ERROR_INTERNAL))
- null
- }
- return answer
- }
-
- private fun prepareCaloriesAnswer(): (InvocationOnMock) -> Nothing? {
- val answer = { args: InvocationOnMock ->
- val receiver = args.arguments[2] as OutcomeReceiver<List<LocalDate>, *>
- receiver.onResult(getCaloriesDates())
- null
- }
- return answer
- }
-
- private fun prepareSleepAnswer(): (InvocationOnMock) -> Nothing? {
- val answer = { args: InvocationOnMock ->
- val receiver = args.arguments[2] as OutcomeReceiver<List<LocalDate>, *>
- receiver.onResult(getSleepDates())
- null
- }
- return answer
- }
-
- private fun prepareEmptyAnswer(): (InvocationOnMock) -> Nothing? {
- val answer = { args: InvocationOnMock ->
- val receiver = args.arguments[2] as OutcomeReceiver<List<LocalDate>, *>
- receiver.onResult(listOf())
- null
- }
- return answer
- }
-
- private fun getStepsDates(): List<LocalDate> =
- listOf(STEPS_DATE_1.toLocalDate(), STEPS_DATE_2.toLocalDate(), STEPS_DATE_3.toLocalDate())
-
- private fun getDistanceDates(): List<LocalDate> =
- listOf(
- DISTANCE_DATE_1.toLocalDate(),
- DISTANCE_DATE_2.toLocalDate(),
- DISTANCE_DATE_3.toLocalDate())
-
- private fun getCaloriesDates(): List<LocalDate> =
- listOf(
- CALORIES_DATE_1.toLocalDate(),
- CALORIES_DATE_2.toLocalDate(),
- CALORIES_DATE_3.toLocalDate())
-
- private fun getSleepDates(): List<LocalDate> =
- listOf(SLEEP_DATE_1.toLocalDate(), SLEEP_DATE_2.toLocalDate(), SLEEP_DATE_3.toLocalDate())
-
- private fun getSleepSessionRecords(inputDates: List<Pair<Instant, Instant>>): List<Record> {
- val result = arrayListOf<Record>()
- inputDates.forEach { (startTime, endTime) ->
- result.add(SleepSessionRecord.Builder(getMetaData(), startTime, endTime).build())
- }
-
- return result
+ val result = loadMostRecentAggregationsUseCase.invoke(HealthDataCategory.SLEEP)
+ assertThat(loadDataAggregationsUseCase.invocationCount).isEqualTo(0)
+ assertThat(result is UseCaseResults.Failed).isTrue()
+ assertThat((result as UseCaseResults.Failed).exception.message).isEqualTo("Exception")
}
}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/datasources/api/LoadPriorityEntriesUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/datasources/api/LoadPriorityEntriesUseCaseTest.kt
new file mode 100644
index 00000000..3a73968c
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/datasources/api/LoadPriorityEntriesUseCaseTest.kt
@@ -0,0 +1,375 @@
+package com.android.healthconnect.controller.tests.datasources.api
+
+import android.content.Context
+import android.health.connect.HealthConnectException
+import android.health.connect.HealthConnectManager
+import android.health.connect.ReadRecordsRequestUsingFilters
+import android.health.connect.ReadRecordsResponse
+import android.health.connect.datatypes.Record
+import android.health.connect.datatypes.SleepSessionRecord
+import android.os.OutcomeReceiver
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.healthconnect.controller.data.entries.api.LoadEntriesHelper
+import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod
+import com.android.healthconnect.controller.dataentries.formatters.shared.HealthDataEntryFormatter
+import com.android.healthconnect.controller.datasources.api.LoadPriorityEntriesUseCase
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType
+import com.android.healthconnect.controller.shared.HealthPermissionToDatatypeMapper
+import com.android.healthconnect.controller.shared.usecase.UseCaseResults
+import com.android.healthconnect.controller.tests.utils.TEST_APP
+import com.android.healthconnect.controller.tests.utils.TEST_APP_2
+import com.android.healthconnect.controller.tests.utils.TEST_APP_3
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME_2
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME_3
+import com.android.healthconnect.controller.tests.utils.di.FakeLoadPriorityListUseCase
+import com.android.healthconnect.controller.tests.utils.forDataType
+import com.android.healthconnect.controller.tests.utils.fromDataSource
+import com.android.healthconnect.controller.tests.utils.fromTimeRange
+import com.android.healthconnect.controller.tests.utils.getSleepSessionRecords
+import com.android.healthconnect.controller.tests.utils.setLocale
+import com.android.healthconnect.controller.tests.utils.verifySleepSessionListsEqual
+import com.android.healthconnect.controller.utils.toInstantAtStartOfDay
+import com.google.common.truth.Truth.assertThat
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import java.time.Instant
+import java.time.LocalDate
+import java.time.ZoneId
+import java.util.Locale
+import java.util.TimeZone
+import javax.inject.Inject
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+import org.mockito.invocation.InvocationOnMock
+import org.mockito.kotlin.any
+import org.mockito.kotlin.times
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@HiltAndroidTest
+class LoadPriorityEntriesUseCaseTest {
+
+ @get:Rule val hiltRule = HiltAndroidRule(this)
+ private lateinit var context: Context
+
+ private val loadPriorityListUseCase = FakeLoadPriorityListUseCase()
+ private lateinit var loadEntriesHelper: LoadEntriesHelper
+ private val healthConnectManager = Mockito.mock(HealthConnectManager::class.java)
+
+ private lateinit var loadPriorityEntriesUseCase: LoadPriorityEntriesUseCase
+ @Inject lateinit var healthDataEntryFormatter: HealthDataEntryFormatter
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ hiltRule.inject()
+ context = InstrumentationRegistry.getInstrumentation().context
+ context.setLocale(Locale.US)
+ TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("UTC")))
+ loadEntriesHelper =
+ LoadEntriesHelper(context, healthDataEntryFormatter, healthConnectManager)
+ loadPriorityEntriesUseCase =
+ LoadPriorityEntriesUseCase(loadEntriesHelper, loadPriorityListUseCase, Dispatchers.Main)
+ }
+
+ @Test
+ fun invoke_onePriorityApp_doesNotIncludeNonPriorityData() = runTest {
+ val sleepDate = LocalDate.of(2023, 2, 13)
+ loadPriorityListUseCase.updatePriorityList(listOf(TEST_APP))
+
+ // 2h
+ val SLEEP_SESSION_1_START_DATE = Instant.parse("2023-02-13T16:00:00.00Z")
+ val SLEEP_SESSION_1_END_DATE = Instant.parse("2023-02-13T18:00:00.00Z")
+
+ // 5h 45m
+ val SLEEP_SESSION_2_START_DATE = Instant.parse("2023-02-13T17:30:00.00Z")
+ val SLEEP_SESSION_2_END_DATE = Instant.parse("2023-02-13T23:15:00.00Z")
+
+ // 7h 20m - not on the priority list
+ val SLEEP_SESSION_3_START_DATE = Instant.parse("2023-02-13T01:00:00.00Z")
+ val SLEEP_SESSION_3_END_DATE = Instant.parse("2023-02-13T23:20:00.00Z")
+
+ mockEntriesResult(
+ packageName = TEST_APP_PACKAGE_NAME,
+ healthPermissionType = HealthPermissionType.SLEEP,
+ queryDate = sleepDate,
+ records =
+ getSleepSessionRecords(
+ listOf(
+ Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE),
+ Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE))))
+
+ mockEntriesResult(
+ packageName = TEST_APP_PACKAGE_NAME_2,
+ healthPermissionType = HealthPermissionType.SLEEP,
+ queryDate = sleepDate,
+ records =
+ getSleepSessionRecords(
+ listOf(Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_3_END_DATE))))
+
+ val result = loadPriorityEntriesUseCase.invoke(HealthPermissionType.SLEEP, sleepDate)
+ assertThat(result is UseCaseResults.Success).isTrue()
+ verifySleepSessionListsEqual(
+ actual = (result as UseCaseResults.Success).data,
+ expected =
+ getSleepSessionRecords(
+ listOf(
+ Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE),
+ Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE))))
+ }
+
+ @Test
+ fun invoke_twoPriorityApps_doesNotIncludeNonPriorityData() = runTest {
+ val sleepDate = LocalDate.of(2023, 2, 13)
+ val pastSleepDate = LocalDate.of(2023, 2, 12)
+
+ loadPriorityListUseCase.updatePriorityList(listOf(TEST_APP, TEST_APP_2))
+
+ // 2h
+ val SLEEP_SESSION_1_START_DATE = Instant.parse("2023-02-13T16:00:00.00Z")
+ val SLEEP_SESSION_1_END_DATE = Instant.parse("2023-02-13T18:00:00.00Z")
+
+ // 5h 45m
+ val SLEEP_SESSION_2_START_DATE = Instant.parse("2023-02-13T17:30:00.00Z")
+ val SLEEP_SESSION_2_END_DATE = Instant.parse("2023-02-13T23:15:00.00Z")
+
+ // 7h 20m
+ val SLEEP_SESSION_3_START_DATE = Instant.parse("2023-02-13T01:00:00.00Z")
+ val SLEEP_SESSION_3_END_DATE = Instant.parse("2023-02-13T23:20:00.00Z")
+
+ // Should be partially included in aggregation
+ val SLEEP_SESSION_4_START_DATE = Instant.parse("2023-02-12T16:00:00.00Z")
+ val SLEEP_SESSION_4_END_DATE = Instant.parse("2023-02-12T23:20:00.00Z")
+
+ // Should not be included in aggregation
+ val SLEEP_SESSION_5_START_DATE = Instant.parse("2023-02-12T12:00:00.00Z")
+ val SLEEP_SESSION_5_END_DATE = Instant.parse("2023-02-12T14:20:00.00Z")
+
+ // Non priority session
+ val SLEEP_SESSION_6_START_DATE = Instant.parse("2023-02-13T00:10:00.00Z")
+ val SLEEP_SESSION_6_END_DATE = Instant.parse("2023-02-13T23:20:00.00Z")
+
+ mockEntriesResult(
+ packageName = TEST_APP_PACKAGE_NAME,
+ healthPermissionType = HealthPermissionType.SLEEP,
+ queryDate = sleepDate,
+ records =
+ getSleepSessionRecords(
+ listOf(Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE))))
+
+ mockEntriesResult(
+ packageName = TEST_APP_PACKAGE_NAME_2,
+ healthPermissionType = HealthPermissionType.SLEEP,
+ queryDate = sleepDate,
+ records =
+ getSleepSessionRecords(
+ listOf(Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE))))
+
+ mockEntriesResult(
+ packageName = TEST_APP_PACKAGE_NAME,
+ healthPermissionType = HealthPermissionType.SLEEP,
+ queryDate = pastSleepDate,
+ records =
+ getSleepSessionRecords(
+ listOf(Pair(SLEEP_SESSION_4_START_DATE, SLEEP_SESSION_4_END_DATE))))
+
+ mockEntriesResult(
+ packageName = TEST_APP_PACKAGE_NAME_2,
+ healthPermissionType = HealthPermissionType.SLEEP,
+ queryDate = pastSleepDate,
+ records =
+ getSleepSessionRecords(
+ listOf(
+ Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_3_END_DATE),
+ Pair(SLEEP_SESSION_5_START_DATE, SLEEP_SESSION_5_END_DATE))))
+
+ mockEntriesResult(
+ packageName = TEST_APP_PACKAGE_NAME_3,
+ healthPermissionType = HealthPermissionType.SLEEP,
+ queryDate = sleepDate,
+ records =
+ getSleepSessionRecords(
+ listOf(Pair(SLEEP_SESSION_6_START_DATE, SLEEP_SESSION_6_END_DATE))))
+
+ val result = loadPriorityEntriesUseCase.invoke(HealthPermissionType.SLEEP, sleepDate)
+ assertThat(result is UseCaseResults.Success).isTrue()
+ verifySleepSessionListsEqual(
+ actual = (result as UseCaseResults.Success).data,
+ expected =
+ getSleepSessionRecords(
+ listOf(
+ Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE),
+ Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE))))
+ }
+
+ @Test
+ fun invoke_twoPriorityApps_noData_returnsEmptyList() = runTest {
+ // No priority sessions on this day
+ val noDataDate = LocalDate.of(2023, 2, 14)
+ val sleepDate = LocalDate.of(2023, 2, 13)
+ val pastSleepDate = LocalDate.of(2023, 2, 12)
+ loadPriorityListUseCase.updatePriorityList(listOf(TEST_APP, TEST_APP_2))
+
+ // 2h
+ val SLEEP_SESSION_1_START_DATE = Instant.parse("2023-02-13T16:00:00.00Z")
+ val SLEEP_SESSION_1_END_DATE = Instant.parse("2023-02-13T18:00:00.00Z")
+
+ // 5h 45m
+ val SLEEP_SESSION_2_START_DATE = Instant.parse("2023-02-13T17:30:00.00Z")
+ val SLEEP_SESSION_2_END_DATE = Instant.parse("2023-02-13T23:15:00.00Z")
+
+ // 7h 20m
+ val SLEEP_SESSION_3_START_DATE = Instant.parse("2023-02-13T01:00:00.00Z")
+ val SLEEP_SESSION_3_END_DATE = Instant.parse("2023-02-13T23:20:00.00Z")
+
+ // Should be partially included in aggregation
+ val SLEEP_SESSION_4_START_DATE = Instant.parse("2023-02-12T16:00:00.00Z")
+ val SLEEP_SESSION_4_END_DATE = Instant.parse("2023-02-12T23:20:00.00Z")
+
+ // Should not be included in aggregation
+ val SLEEP_SESSION_5_START_DATE = Instant.parse("2023-02-12T12:00:00.00Z")
+ val SLEEP_SESSION_5_END_DATE = Instant.parse("2023-02-12T14:20:00.00Z")
+
+ // Non priority session
+ val SLEEP_SESSION_6_START_DATE = Instant.parse("2023-02-14T00:10:00.00Z")
+ val SLEEP_SESSION_6_END_DATE = Instant.parse("2023-02-14T23:20:00.00Z")
+
+ mockEntriesResult(
+ packageName = TEST_APP_PACKAGE_NAME,
+ healthPermissionType = HealthPermissionType.SLEEP,
+ queryDate = noDataDate,
+ records = listOf())
+
+ mockEntriesResult(
+ packageName = TEST_APP_PACKAGE_NAME_2,
+ healthPermissionType = HealthPermissionType.SLEEP,
+ queryDate = noDataDate,
+ records = listOf())
+
+ mockEntriesResult(
+ packageName = TEST_APP_PACKAGE_NAME,
+ healthPermissionType = HealthPermissionType.SLEEP,
+ queryDate = sleepDate,
+ records =
+ getSleepSessionRecords(
+ listOf(Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE))))
+
+ mockEntriesResult(
+ packageName = TEST_APP_PACKAGE_NAME_2,
+ healthPermissionType = HealthPermissionType.SLEEP,
+ queryDate = sleepDate,
+ records =
+ getSleepSessionRecords(
+ listOf(Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE))))
+
+ mockEntriesResult(
+ packageName = TEST_APP_PACKAGE_NAME,
+ healthPermissionType = HealthPermissionType.SLEEP,
+ queryDate = pastSleepDate,
+ records =
+ getSleepSessionRecords(
+ listOf(Pair(SLEEP_SESSION_4_START_DATE, SLEEP_SESSION_4_END_DATE))))
+
+ mockEntriesResult(
+ packageName = TEST_APP_PACKAGE_NAME_2,
+ healthPermissionType = HealthPermissionType.SLEEP,
+ queryDate = pastSleepDate,
+ records =
+ getSleepSessionRecords(
+ listOf(
+ Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_3_END_DATE),
+ Pair(SLEEP_SESSION_5_START_DATE, SLEEP_SESSION_5_END_DATE))))
+
+ mockEntriesResult(
+ packageName = TEST_APP_PACKAGE_NAME_3,
+ healthPermissionType = HealthPermissionType.SLEEP,
+ queryDate = sleepDate,
+ records =
+ getSleepSessionRecords(
+ listOf(Pair(SLEEP_SESSION_6_START_DATE, SLEEP_SESSION_6_END_DATE))))
+
+ val result = loadPriorityEntriesUseCase.invoke(HealthPermissionType.SLEEP, noDataDate)
+ assertThat(result is UseCaseResults.Success).isTrue()
+ assertThat((result as UseCaseResults.Success).data).isEmpty()
+ }
+
+ @Test
+ fun invoke_whenPriorityFails_returnsFailure() = runTest {
+ val queryDate = LocalDate.of(2023, 1, 4)
+ loadPriorityListUseCase.setFailure("Exception")
+
+ val result = loadPriorityEntriesUseCase.invoke(HealthPermissionType.SLEEP, queryDate)
+ assertThat(result is UseCaseResults.Failed).isTrue()
+ assertThat((result as UseCaseResults.Failed).exception.message).isEqualTo("Exception")
+ Mockito.verify(healthConnectManager, times(0))
+ .readRecords<SleepSessionRecord>(any(), any(), any())
+ }
+
+ @Test
+ fun invoke_whenLoadEntriesHelperFails_returnsFailure() = runTest {
+ val queryDate = LocalDate.of(2023, 1, 4)
+ loadPriorityListUseCase.updatePriorityList(listOf(TEST_APP_2, TEST_APP_3))
+ Mockito.doAnswer(prepareFailureAnswer())
+ .`when`(healthConnectManager)
+ .readRecords<SleepSessionRecord>(any(), any(), any())
+
+ val result = loadPriorityEntriesUseCase.invoke(HealthPermissionType.SLEEP, queryDate)
+ assertThat(result is UseCaseResults.Failed).isTrue()
+ assertThat((result as UseCaseResults.Failed).exception is HealthConnectException).isTrue()
+ assertThat((result.exception as HealthConnectException).errorCode)
+ .isEqualTo(HealthConnectException.ERROR_UNKNOWN)
+ }
+
+ private fun mockEntriesResult(
+ packageName: String,
+ healthPermissionType: HealthPermissionType,
+ queryDate: LocalDate,
+ records: List<Record>
+ ) {
+ val timeFilterRange =
+ loadEntriesHelper.getTimeFilter(
+ queryDate.toInstantAtStartOfDay(),
+ DateNavigationPeriod.PERIOD_DAY,
+ endTimeExclusive = true)
+ val dataTypes = HealthPermissionToDatatypeMapper.getDataTypes(healthPermissionType)
+
+ dataTypes.map { dataType ->
+ Mockito.doAnswer(prepareRecordsAnswer(records))
+ .`when`(healthConnectManager)
+ .readRecords(
+ ArgumentMatchers.argThat<ReadRecordsRequestUsingFilters<Record>> { request ->
+ request.fromDataSource(packageName) &&
+ request.fromTimeRange(timeFilterRange) &&
+ request.forDataType(dataType)
+ },
+ ArgumentMatchers.any(),
+ ArgumentMatchers.any())
+ }
+ }
+
+ private fun prepareRecordsAnswer(records: List<Record>): (InvocationOnMock) -> Nothing? {
+ val answer = { args: InvocationOnMock ->
+ val receiver = args.arguments[2] as OutcomeReceiver<ReadRecordsResponse<Record>, *>
+ receiver.onResult(ReadRecordsResponse(records, -1))
+ null
+ }
+ return answer
+ }
+
+ private fun prepareFailureAnswer(): (InvocationOnMock) -> Nothing? {
+ val answer = { args: InvocationOnMock ->
+ val receiver =
+ args.arguments[2] as OutcomeReceiver<List<LocalDate>, HealthConnectException>
+ receiver.onError(HealthConnectException(HealthConnectException.ERROR_UNKNOWN))
+ null
+ }
+ return answer
+ }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/datasources/api/SleepSessionHelperTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/datasources/api/SleepSessionHelperTest.kt
new file mode 100644
index 00000000..e7ba932c
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/datasources/api/SleepSessionHelperTest.kt
@@ -0,0 +1,676 @@
+package com.android.healthconnect.controller.tests.datasources.api
+
+import android.content.Context
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.healthconnect.controller.datasources.api.SleepSessionHelper
+import com.android.healthconnect.controller.shared.usecase.UseCaseResults
+import com.android.healthconnect.controller.tests.utils.di.FakeLoadPriorityEntriesUseCase
+import com.android.healthconnect.controller.tests.utils.getSleepSessionRecords
+import com.android.healthconnect.controller.tests.utils.setLocale
+import com.google.common.truth.Truth.assertThat
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import java.time.Instant
+import java.time.LocalDate
+import java.time.ZoneId
+import java.util.Locale
+import java.util.TimeZone
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@HiltAndroidTest
+class SleepSessionHelperTest {
+
+ @get:Rule val hiltRule = HiltAndroidRule(this)
+ private lateinit var context: Context
+
+ private val loadPriorityEntriesUseCase = FakeLoadPriorityEntriesUseCase()
+
+ private lateinit var sleepSessionHelper: SleepSessionHelper
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ hiltRule.inject()
+ context = InstrumentationRegistry.getInstrumentation().context
+ context.setLocale(Locale.US)
+ TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("UTC")))
+ sleepSessionHelper = SleepSessionHelper(loadPriorityEntriesUseCase, Dispatchers.Main)
+ }
+
+ @After
+ fun tearDown() {
+ loadPriorityEntriesUseCase.reset()
+ }
+
+ // Case 1 - start and end times on same day
+ @Test
+ fun clusterSessions_allSessionsStartAndEndOnSameDay_returnsMinAndMaxOfAllSessions() = runTest {
+ val sleepDate = LocalDate.of(2023, 2, 13)
+
+ // lastDayWithSleepData = 2023-02-13
+
+ // 2h
+ val SLEEP_SESSION_1_START_DATE = Instant.parse("2023-02-13T16:00:00.00Z")
+ val SLEEP_SESSION_1_END_DATE = Instant.parse("2023-02-13T18:00:00.00Z")
+
+ // 5h 45m
+ val SLEEP_SESSION_2_START_DATE = Instant.parse("2023-02-13T17:30:00.00Z")
+ val SLEEP_SESSION_2_END_DATE = Instant.parse("2023-02-13T23:15:00.00Z")
+
+ // 7h 20m
+ val SLEEP_SESSION_3_START_DATE = Instant.parse("2023-02-13T01:00:00.00Z")
+ val SLEEP_SESSION_3_END_DATE = Instant.parse("2023-02-13T08:20:00.00Z")
+
+ loadPriorityEntriesUseCase.setEntriesList(
+ sleepDate,
+ getSleepSessionRecords(
+ listOf(
+ Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE),
+ Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE),
+ Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_3_END_DATE))))
+
+ val result = sleepSessionHelper.clusterSleepSessions(sleepDate)
+ assertThat(result is UseCaseResults.Success).isTrue()
+ assertThat((result as UseCaseResults.Success).data)
+ .isEqualTo(Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_2_END_DATE))
+ }
+
+ // Case 1 - start and end times on same day
+ // Edge case - additional sleep session starts on past date (not day before)
+ // And finishes on last day with data
+ @Test
+ fun clusterSessions_allSessionStartAndEndOnSameDay_withSessionUnknownStart_doesNotIncludeUnknownSessionInMinAndMax() =
+ runTest {
+ val sleepDate = LocalDate.of(2023, 2, 13)
+
+ // lastDayWithSleepData = 2023-02-13
+
+ // 2h
+ val SLEEP_SESSION_1_START_DATE = Instant.parse("2023-02-13T16:00:00.00Z")
+ val SLEEP_SESSION_1_END_DATE = Instant.parse("2023-02-13T18:00:00.00Z")
+
+ // 5h 45m
+ val SLEEP_SESSION_2_START_DATE = Instant.parse("2023-02-13T17:30:00.00Z")
+ val SLEEP_SESSION_2_END_DATE = Instant.parse("2023-02-13T23:15:00.00Z")
+
+ // 7h 20m
+ val SLEEP_SESSION_3_START_DATE = Instant.parse("2023-02-13T01:00:00.00Z")
+ val SLEEP_SESSION_3_END_DATE = Instant.parse("2023-02-13T08:20:00.00Z")
+
+ // Past sleep session ending on lastDayWithData
+ // 3d 7h 20m
+ val pastSleepDate = LocalDate.of(2023, 2, 10)
+ val SLEEP_SESSION_4_START_DATE = Instant.parse("2023-02-10T01:00:00.00Z")
+ val SLEEP_SESSION_4_END_DATE = Instant.parse("2023-02-13T08:20:00.00Z")
+
+ loadPriorityEntriesUseCase.setEntriesList(
+ sleepDate,
+ getSleepSessionRecords(
+ listOf(
+ Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE),
+ Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE),
+ Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_3_END_DATE),
+ )))
+
+ loadPriorityEntriesUseCase.setEntriesList(
+ pastSleepDate,
+ getSleepSessionRecords(
+ listOf(Pair(SLEEP_SESSION_4_START_DATE, SLEEP_SESSION_4_END_DATE))))
+
+ val result = sleepSessionHelper.clusterSleepSessions(sleepDate)
+ assertThat(result is UseCaseResults.Success).isTrue()
+ assertThat((result as UseCaseResults.Success).data)
+ .isEqualTo(Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_2_END_DATE))
+ }
+
+ // Case 1 - start and end times on same day
+ // Edge case - additional sleep session starts on past date (not day before)
+ // And finishes on future date
+ @Test
+ fun clusterSessions_allSessionStartAndEndOnSameDay_withSessionUnknownStartEnd_doesNotIncludeUnknownSessionInMinAndMax() =
+ runTest {
+ val sleepDate = LocalDate.of(2023, 2, 13)
+
+ // lastDayWithSleepData = 2023-02-13
+
+ // 2h
+ val SLEEP_SESSION_1_START_DATE = Instant.parse("2023-02-13T16:00:00.00Z")
+ val SLEEP_SESSION_1_END_DATE = Instant.parse("2023-02-13T18:00:00.00Z")
+
+ // 5h 45m
+ val SLEEP_SESSION_2_START_DATE = Instant.parse("2023-02-13T17:30:00.00Z")
+ val SLEEP_SESSION_2_END_DATE = Instant.parse("2023-02-13T23:15:00.00Z")
+
+ // 7h 20m
+ val SLEEP_SESSION_3_START_DATE = Instant.parse("2023-02-13T01:00:00.00Z")
+ val SLEEP_SESSION_3_END_DATE = Instant.parse("2023-02-13T08:20:00.00Z")
+
+ // Past sleep session, overlaps completely with above data
+ // 5d 7h 20m
+ val pastSleepSessionStartDate = LocalDate.of(2023, 2, 10)
+ val SLEEP_SESSION_4_START_DATE = Instant.parse("2023-02-10T01:00:00.00Z")
+ val SLEEP_SESSION_4_END_DATE = Instant.parse("2023-02-15T08:20:00.00Z")
+
+ loadPriorityEntriesUseCase.setEntriesList(
+ sleepDate,
+ getSleepSessionRecords(
+ listOf(
+ Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE),
+ Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE),
+ Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_3_END_DATE),
+ )))
+
+ loadPriorityEntriesUseCase.setEntriesList(
+ pastSleepSessionStartDate,
+ getSleepSessionRecords(
+ listOf(Pair(SLEEP_SESSION_4_START_DATE, SLEEP_SESSION_4_END_DATE))))
+
+ val result = sleepSessionHelper.clusterSleepSessions(sleepDate)
+ assertThat(result is UseCaseResults.Success).isTrue()
+ assertThat((result as UseCaseResults.Success).data)
+ .isEqualTo(Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_2_END_DATE))
+ }
+
+ // Case 2 - At least one session starts on Day 1 and finishes on Day 2 or later
+ @Test
+ fun clusterSessions_atLeastOneSessionStartsYesterdayAndEndsToday_includesCrossingSessionInMinAndMax() =
+ runTest {
+ val lastDateWithSleepData = LocalDate.of(2023, 2, 13)
+ val secondToLastDateWithData = LocalDate.of(2023, 2, 12)
+
+ // lastDayWithSleepData = 2023-02-13
+
+ // 2h
+ val SLEEP_SESSION_1_START_DATE = Instant.parse("2023-02-13T16:00:00.00Z")
+ val SLEEP_SESSION_1_END_DATE = Instant.parse("2023-02-13T18:00:00.00Z")
+
+ // 5h 45m
+ val SLEEP_SESSION_2_START_DATE = Instant.parse("2023-02-13T17:30:00.00Z")
+ val SLEEP_SESSION_2_END_DATE = Instant.parse("2023-02-13T23:15:00.00Z")
+
+ // 9h 20m
+ val SLEEP_SESSION_3_START_DATE = Instant.parse("2023-02-12T23:00:00.00Z")
+ val SLEEP_SESSION_3_END_DATE = Instant.parse("2023-02-13T08:20:00.00Z")
+
+ // Should be partially included in aggregation
+ val SLEEP_SESSION_4_START_DATE = Instant.parse("2023-02-12T16:00:00.00Z")
+ val SLEEP_SESSION_4_END_DATE = Instant.parse("2023-02-12T23:20:00.00Z")
+
+ // Should not be included in aggregation
+ val SLEEP_SESSION_5_START_DATE = Instant.parse("2023-02-12T12:00:00.00Z")
+ val SLEEP_SESSION_5_END_DATE = Instant.parse("2023-02-12T14:20:00.00Z")
+
+ loadPriorityEntriesUseCase.setEntriesList(
+ lastDateWithSleepData,
+ getSleepSessionRecords(
+ listOf(
+ Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE),
+ Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE))))
+
+ loadPriorityEntriesUseCase.setEntriesList(
+ secondToLastDateWithData,
+ getSleepSessionRecords(
+ listOf(
+ Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_3_END_DATE),
+ Pair(SLEEP_SESSION_4_START_DATE, SLEEP_SESSION_4_END_DATE),
+ Pair(SLEEP_SESSION_5_START_DATE, SLEEP_SESSION_5_END_DATE))))
+
+ // minStartTime = SLEEP_SESSION_3_START_DATE
+ // maxEndTime = SLEEP_SESSION_2_END_DATE
+ // Total time = 12 Feb, 23:00 - 13 Feb 23:15 = 24h 15m
+ val result = sleepSessionHelper.clusterSleepSessions(lastDateWithSleepData)
+ assertThat(result is UseCaseResults.Success).isTrue()
+ assertThat((result as UseCaseResults.Success).data)
+ .isEqualTo(Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_2_END_DATE))
+ }
+
+ // Case 2 - At least one session starts on Day 1 and finishes on Day 2 or later
+ // with gaps
+ @Test
+ fun clusterSessions_atLeastOneSessionStartsYesterdayAndEndsToday_withGaps_returnsCorrectMinAndMax() =
+ runTest {
+ val lastDateWithSleepData = LocalDate.of(2023, 2, 13)
+ val secondToLastDateWithSleepData = LocalDate.of(2023, 2, 12)
+
+ // lastDayWithSleepData = 2023-02-13
+
+ // 2h
+ val SLEEP_SESSION_1_START_DATE = Instant.parse("2023-02-13T18:00:00.00Z")
+ val SLEEP_SESSION_1_END_DATE = Instant.parse("2023-02-13T20:00:00.00Z")
+
+ // 2h 15m
+ val SLEEP_SESSION_2_START_DATE = Instant.parse("2023-02-13T12:30:00.00Z")
+ val SLEEP_SESSION_2_END_DATE = Instant.parse("2023-02-13T14:45:00.00Z")
+
+ // 5h 20m
+ val SLEEP_SESSION_3_START_DATE = Instant.parse("2023-02-12T20:00:00.00Z")
+ val SLEEP_SESSION_3_END_DATE = Instant.parse("2023-02-13T01:20:00.00Z")
+
+ // Should be partially included in aggregation
+ val SLEEP_SESSION_4_START_DATE = Instant.parse("2023-02-12T16:00:00.00Z")
+ val SLEEP_SESSION_4_END_DATE = Instant.parse("2023-02-12T23:20:00.00Z")
+
+ loadPriorityEntriesUseCase.setEntriesList(
+ lastDateWithSleepData,
+ getSleepSessionRecords(
+ listOf(
+ Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE),
+ Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE))))
+
+ loadPriorityEntriesUseCase.setEntriesList(
+ secondToLastDateWithSleepData,
+ getSleepSessionRecords(
+ listOf(
+ Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_3_END_DATE),
+ Pair(SLEEP_SESSION_4_START_DATE, SLEEP_SESSION_4_END_DATE))))
+
+ // minStartTime = SLEEP_SESSION_3_START_DATE
+ // maxEndTime = SLEEP_SESSION_1_END_DATE
+ // Total time = 2h + 2h 15m + 5h 20m = 9h 35m
+ val result = sleepSessionHelper.clusterSleepSessions(lastDateWithSleepData)
+ assertThat(result is UseCaseResults.Success).isTrue()
+ assertThat((result as UseCaseResults.Success).data)
+ .isEqualTo(Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_1_END_DATE))
+ }
+
+ // Case 2 - At least one session starts on Day 1 and finishes on Day 2 or later
+ // Edge case - additional sleep session starts on past date
+ // and finishes on lastDayWithData
+ @Test
+ fun clusterSessions_atLeastOneSessionStartsYesterdayAndEndsToday_withSessionUnknownStart_doesNotIncludeUnknownSessionInMinAndMax() =
+ runTest {
+ val lastDateWithSleepData = LocalDate.of(2023, 2, 13)
+ val secondToLastDateWithSleepData = LocalDate.of(2023, 2, 12)
+
+ // secondToLastDayWithSleepData = 2023-02-12
+ // lastDayWithSleepData = 2023-02-13
+
+ // 2h
+ val SLEEP_SESSION_1_START_DATE = Instant.parse("2023-02-13T16:00:00.00Z")
+ val SLEEP_SESSION_1_END_DATE = Instant.parse("2023-02-13T18:00:00.00Z")
+
+ // 5h 45m
+ val SLEEP_SESSION_2_START_DATE = Instant.parse("2023-02-13T17:30:00.00Z")
+ val SLEEP_SESSION_2_END_DATE = Instant.parse("2023-02-13T23:15:00.00Z")
+
+ // 10h 20m
+ val SLEEP_SESSION_3_START_DATE = Instant.parse("2023-02-12T22:00:00.00Z")
+ val SLEEP_SESSION_3_END_DATE = Instant.parse("2023-02-13T08:20:00.00Z")
+
+ // Should not be included in calculation
+ val SLEEP_SESSION_4_START_DATE = Instant.parse("2023-02-12T16:00:00.00Z")
+ val SLEEP_SESSION_4_END_DATE = Instant.parse("2023-02-12T23:20:00.00Z")
+
+ // Should not be included in calculation
+ val pastDateWithSleepData = LocalDate.of(2023, 2, 10)
+ val SLEEP_SESSION_5_START_DATE = Instant.parse("2023-02-10T12:00:00.00Z")
+ val SLEEP_SESSION_5_END_DATE = Instant.parse("2023-02-13T14:20:00.00Z")
+
+ loadPriorityEntriesUseCase.setEntriesList(
+ lastDateWithSleepData,
+ getSleepSessionRecords(
+ listOf(
+ Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE),
+ Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE))))
+
+ loadPriorityEntriesUseCase.setEntriesList(
+ secondToLastDateWithSleepData,
+ getSleepSessionRecords(
+ listOf(
+ Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_3_END_DATE),
+ Pair(SLEEP_SESSION_4_START_DATE, SLEEP_SESSION_4_END_DATE),
+ )))
+
+ loadPriorityEntriesUseCase.setEntriesList(
+ pastDateWithSleepData,
+ getSleepSessionRecords(
+ listOf(Pair(SLEEP_SESSION_5_START_DATE, SLEEP_SESSION_5_END_DATE))))
+
+ // minStartTime = SLEEP_SESSION_3_START_DATE
+ // maxEndTime = SLEEP_SESSION_2_END_DATE
+ // Total time = 2h + 2h 15m + 5h 20m = 9h 35m
+ val result = sleepSessionHelper.clusterSleepSessions(lastDateWithSleepData)
+ assertThat(result is UseCaseResults.Success).isTrue()
+ assertThat((result as UseCaseResults.Success).data)
+ .isEqualTo(Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_2_END_DATE))
+ }
+
+ // Case 2 - At least one session starts on Day 1 and finishes on Day 2 or later
+ // Edge case - additional sleep session starts on Day 1
+ // and finishes on unknown date
+ // Then the maxEndDate should be forced at Day 3 midnight
+ @Test
+ fun clusterSessions_atLeastOneSessionStartsYesterdayAndEndsToday_withSessionUnknownEnd_returnsForcedMax() =
+ runTest {
+ val lastDateWithSleepData = LocalDate.of(2023, 2, 13)
+ val secondToLastDateWithSleepData = LocalDate.of(2023, 2, 12)
+
+ // secondToLastDayWithSleepData = 2023-02-12
+ // lastDayWithSleepData = 2023-02-13
+
+ // 2h
+ val SLEEP_SESSION_1_START_DATE = Instant.parse("2023-02-13T16:00:00.00Z")
+ val SLEEP_SESSION_1_END_DATE = Instant.parse("2023-02-13T18:00:00.00Z")
+
+ // 5h 45m
+ val SLEEP_SESSION_2_START_DATE = Instant.parse("2023-02-13T17:30:00.00Z")
+ val SLEEP_SESSION_2_END_DATE = Instant.parse("2023-02-13T23:15:00.00Z")
+
+ // 10h 20m
+ val SLEEP_SESSION_3_START_DATE = Instant.parse("2023-02-12T22:00:00.00Z")
+ val SLEEP_SESSION_3_END_DATE = Instant.parse("2023-02-13T08:20:00.00Z")
+
+ // Should be partially included in aggregation
+ val SLEEP_SESSION_4_START_DATE = Instant.parse("2023-02-12T16:00:00.00Z")
+ val SLEEP_SESSION_4_END_DATE = Instant.parse("2023-02-12T23:20:00.00Z")
+
+ // Should be partially included in aggregation up to 2023-02-14T00:00
+ val SLEEP_SESSION_5_START_DATE = Instant.parse("2023-02-12T12:00:00.00Z")
+ val SLEEP_SESSION_5_END_DATE = Instant.parse("2023-02-18T14:20:00.00Z")
+
+ val maxDate = Instant.parse("2023-02-14T00:00:00.00Z")
+
+ loadPriorityEntriesUseCase.setEntriesList(
+ lastDateWithSleepData,
+ getSleepSessionRecords(
+ listOf(
+ Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE),
+ Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE))))
+
+ loadPriorityEntriesUseCase.setEntriesList(
+ secondToLastDateWithSleepData,
+ getSleepSessionRecords(
+ listOf(
+ Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_3_END_DATE),
+ Pair(SLEEP_SESSION_4_START_DATE, SLEEP_SESSION_4_END_DATE),
+ Pair(SLEEP_SESSION_5_START_DATE, SLEEP_SESSION_5_END_DATE))))
+
+ // minStartTime = SLEEP_SESSION_5_START_DATE
+ // maxEndTime = 2023-02-14T00:00
+ // Total time = 12 Feb, 12:00 - 14 Feb 00:00 = 36h
+ val result = sleepSessionHelper.clusterSleepSessions(lastDateWithSleepData)
+ assertThat(result is UseCaseResults.Success).isTrue()
+ assertThat((result as UseCaseResults.Success).data)
+ .isEqualTo(Pair(SLEEP_SESSION_5_START_DATE, maxDate))
+ }
+
+ // Case 2 - At least one session starts on Day 1 and finishes on Day 2 or later
+ // Edge case - additional sleep session starts and finishes on unknown date
+ @Test
+ fun clusterSessions_atLeastOneSessionStartsYesterdayAndEndsToday_withSessionUnknownStartAndEnd_doesNotIncludeUnknownSessionInMinAndMax() =
+ runTest {
+ val lastDateWithSleepData = LocalDate.of(2023, 2, 13)
+ val secondToLastDateWithSleepData = LocalDate.of(2023, 2, 12)
+
+ // secondToLastDayWithSleepData = 2023-02-12
+ // lastDayWithSleepData = 2023-02-13
+
+ // 2h
+ val SLEEP_SESSION_1_START_DATE = Instant.parse("2023-02-13T18:00:00.00Z")
+ val SLEEP_SESSION_1_END_DATE = Instant.parse("2023-02-13T20:00:00.00Z")
+
+ // 2h 15m
+ val SLEEP_SESSION_2_START_DATE = Instant.parse("2023-02-13T12:30:00.00Z")
+ val SLEEP_SESSION_2_END_DATE = Instant.parse("2023-02-13T14:45:00.00Z")
+
+ // 5h 20m
+ val SLEEP_SESSION_3_START_DATE = Instant.parse("2023-02-12T20:00:00.00Z")
+ val SLEEP_SESSION_3_END_DATE = Instant.parse("2023-02-13T01:20:00.00Z")
+
+ // Should be partially included in aggregation
+ val pastDateWithSleepData = LocalDate.of(2023, 2, 10)
+ val SLEEP_SESSION_4_START_DATE = Instant.parse("2023-02-10T16:00:00.00Z")
+ val SLEEP_SESSION_4_END_DATE = Instant.parse("2023-02-20T23:20:00.00Z")
+
+ loadPriorityEntriesUseCase.setEntriesList(
+ lastDateWithSleepData,
+ getSleepSessionRecords(
+ listOf(
+ Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE),
+ Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE))))
+
+ loadPriorityEntriesUseCase.setEntriesList(
+ secondToLastDateWithSleepData,
+ getSleepSessionRecords(
+ listOf(Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_3_END_DATE))))
+
+ loadPriorityEntriesUseCase.setEntriesList(
+ pastDateWithSleepData,
+ getSleepSessionRecords(
+ listOf(Pair(SLEEP_SESSION_4_START_DATE, SLEEP_SESSION_4_END_DATE))))
+
+ // minStartTime = SLEEP_SESSION_3_START_DATE
+ // maxEndTime = SLEEP_SESSION_1_END_DATE
+ // Total time = 12 Oct 20:00 - 13 Oct 20:00 = 24h
+ val result = sleepSessionHelper.clusterSleepSessions(lastDateWithSleepData)
+ assertThat(result is UseCaseResults.Success).isTrue()
+ assertThat((result as UseCaseResults.Success).data)
+ .isEqualTo(Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_1_END_DATE))
+ }
+
+ // Case 3 - The sessions from lastDayWithData cross midnight into the next day
+ @Test
+ fun clusterSessions_atLeastOneSessionFinishesTomorrow_returnsMaxFromTomorrow() = runTest {
+ val lastDateWithSleepData = LocalDate.of(2023, 2, 13)
+
+ // lastDayWithSleepData = 2023-02-13
+
+ // 2h
+ val SLEEP_SESSION_1_START_DATE = Instant.parse("2023-02-13T18:00:00.00Z")
+ val SLEEP_SESSION_1_END_DATE = Instant.parse("2023-02-13T20:00:00.00Z")
+
+ // 10h 15m
+ val SLEEP_SESSION_2_START_DATE = Instant.parse("2023-02-13T22:30:00.00Z")
+ val SLEEP_SESSION_2_END_DATE = Instant.parse("2023-02-14T08:45:00.00Z")
+
+ // 2h 20m
+ val SLEEP_SESSION_3_START_DATE = Instant.parse("2023-02-13T16:00:00.00Z")
+ val SLEEP_SESSION_3_END_DATE = Instant.parse("2023-02-13T18:20:00.00Z")
+
+ // 10h
+ val SLEEP_SESSION_4_START_DATE = Instant.parse("2023-02-13T22:00:00.00Z")
+ val SLEEP_SESSION_4_END_DATE = Instant.parse("2023-02-14T08:00:00.00Z")
+
+ loadPriorityEntriesUseCase.setEntriesList(
+ lastDateWithSleepData,
+ getSleepSessionRecords(
+ listOf(
+ Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE),
+ Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE),
+ Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_3_END_DATE),
+ Pair(SLEEP_SESSION_4_START_DATE, SLEEP_SESSION_4_END_DATE))))
+
+ // minStartTime = SLEEP_SESSION_4_START_DATE
+ // maxEndTime = SLEEP_SESSION_2_END_DATE
+ // Total time = 13 Feb 22:00 - 14 Feb 08:45 = 10h 45m
+ val result = sleepSessionHelper.clusterSleepSessions(lastDateWithSleepData)
+ assertThat(result is UseCaseResults.Success).isTrue()
+ assertThat((result as UseCaseResults.Success).data)
+ .isEqualTo(Pair(SLEEP_SESSION_4_START_DATE, SLEEP_SESSION_2_END_DATE))
+ }
+
+ // Case 3 - The sessions from lastDayWithData cross midnight into the next day
+ // Edge case - additional sleep session starts on unknown date
+ // and finishes within range
+ @Test
+ fun clusterSessions_atLeastOneSessionFinishesTomorrow_withSessionUnknownStart_doesNotIncludeUnknownSessionInMinAndMax() =
+ runTest {
+ val lastDateWithSleepData = LocalDate.of(2023, 2, 13)
+
+ // lastDayWithSleepData = 2023-02-13
+
+ // 2h
+ val SLEEP_SESSION_1_START_DATE = Instant.parse("2023-02-13T18:00:00.00Z")
+ val SLEEP_SESSION_1_END_DATE = Instant.parse("2023-02-13T20:00:00.00Z")
+
+ // 10h 15m
+ val SLEEP_SESSION_2_START_DATE = Instant.parse("2023-02-13T22:30:00.00Z")
+ val SLEEP_SESSION_2_END_DATE = Instant.parse("2023-02-14T08:45:00.00Z")
+
+ // 2h 20m
+ val SLEEP_SESSION_3_START_DATE = Instant.parse("2023-02-13T16:00:00.00Z")
+ val SLEEP_SESSION_3_END_DATE = Instant.parse("2023-02-13T18:20:00.00Z")
+
+ // 10h
+ val SLEEP_SESSION_4_START_DATE = Instant.parse("2023-02-13T22:00:00.00Z")
+ val SLEEP_SESSION_4_END_DATE = Instant.parse("2023-02-14T08:00:00.00Z")
+
+ // 2d 11h - should not have an effect on the aggregation
+ val pastDateWithSleepData = LocalDate.of(2023, 2, 11)
+ val SLEEP_SESSION_5_START_DATE = Instant.parse("2023-02-11T22:00:00.00Z")
+ val SLEEP_SESSION_5_END_DATE = Instant.parse("2023-02-14T09:00:00.00Z")
+
+ loadPriorityEntriesUseCase.setEntriesList(
+ lastDateWithSleepData,
+ getSleepSessionRecords(
+ listOf(
+ Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE),
+ Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE),
+ Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_3_END_DATE),
+ Pair(SLEEP_SESSION_4_START_DATE, SLEEP_SESSION_4_END_DATE))))
+
+ loadPriorityEntriesUseCase.setEntriesList(
+ pastDateWithSleepData,
+ getSleepSessionRecords(
+ listOf(Pair(SLEEP_SESSION_5_START_DATE, SLEEP_SESSION_5_END_DATE))))
+
+ // minStartTime = SLEEP_SESSION_4_START_DATE
+ // maxEndTime = SLEEP_SESSION_2_END_DATE
+ // Total time = 13 Feb 22:00 - 14 Feb 08:45 = 10h 45m
+ val result = sleepSessionHelper.clusterSleepSessions(lastDateWithSleepData)
+ assertThat(result is UseCaseResults.Success).isTrue()
+ assertThat((result as UseCaseResults.Success).data)
+ .isEqualTo(Pair(SLEEP_SESSION_4_START_DATE, SLEEP_SESSION_2_END_DATE))
+ }
+
+ // Case 3 - The sessions from lastDayWithData cross midnight into the next day
+ // Edge case - additional sleep session starts on last day with data
+ // and finishes in the future
+ @Test
+ fun clusterSessions_atLeastOneSessionFinishesTomorrow_withSessionUnknownEnd_returnsForcedMax() =
+ runTest {
+ val lastDateWithSleepData = LocalDate.of(2023, 2, 13)
+
+ // lastDayWithSleepData = 2023-02-13
+
+ // 2h
+ val SLEEP_SESSION_1_START_DATE = Instant.parse("2023-02-13T18:00:00.00Z")
+ val SLEEP_SESSION_1_END_DATE = Instant.parse("2023-02-13T20:00:00.00Z")
+
+ // 10h 15m
+ val SLEEP_SESSION_2_START_DATE = Instant.parse("2023-02-13T22:30:00.00Z")
+ val SLEEP_SESSION_2_END_DATE = Instant.parse("2023-02-14T08:45:00.00Z")
+
+ // 2h 20m
+ val SLEEP_SESSION_3_START_DATE = Instant.parse("2023-02-13T16:00:00.00Z")
+ val SLEEP_SESSION_3_END_DATE = Instant.parse("2023-02-13T18:20:00.00Z")
+
+ // 10h
+ val SLEEP_SESSION_4_START_DATE = Instant.parse("2023-02-13T22:00:00.00Z")
+ val SLEEP_SESSION_4_END_DATE = Instant.parse("2023-02-14T08:00:00.00Z")
+
+ // 3d 11h - determines maxEndTime
+ val SLEEP_SESSION_5_START_DATE = Instant.parse("2023-02-13T22:00:00.00Z")
+ val SLEEP_SESSION_5_END_DATE = Instant.parse("2023-02-16T09:00:00.00Z")
+
+ val maxEndTime = Instant.parse("2023-02-15T00:00:00.00Z")
+
+ loadPriorityEntriesUseCase.setEntriesList(
+ lastDateWithSleepData,
+ getSleepSessionRecords(
+ listOf(
+ Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE),
+ Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE),
+ Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_3_END_DATE),
+ Pair(SLEEP_SESSION_4_START_DATE, SLEEP_SESSION_4_END_DATE),
+ Pair(SLEEP_SESSION_5_START_DATE, SLEEP_SESSION_5_END_DATE))))
+
+ // minStartTime = SLEEP_SESSION_4_START_DATE
+ // maxEndTime = 15 Feb 00:00
+ // Total time = 13 Feb 22:00 - 15 Feb 00:00 = 26h
+ val result = sleepSessionHelper.clusterSleepSessions(lastDateWithSleepData)
+ assertThat(result is UseCaseResults.Success).isTrue()
+ assertThat((result as UseCaseResults.Success).data)
+ .isEqualTo(Pair(SLEEP_SESSION_4_START_DATE, maxEndTime))
+ }
+
+ // Case 3 - The sessions from lastDayWithData cross midnight into the next day
+ // Edge case - additional sleep session starts and ends on unknown date
+ @Test
+ fun clusterSessions_atLeastOneSessionFinishesTomorrow_withSessionUnknownStartAndEnd_doesNotIncludeUnknownSessionInMinAndMax() =
+ runTest {
+ val lastDateWithSleepData = LocalDate.of(2023, 2, 13)
+
+ // lastDayWithSleepData = 2023-02-13
+
+ // 2h
+ val SLEEP_SESSION_1_START_DATE = Instant.parse("2023-02-13T18:00:00.00Z")
+ val SLEEP_SESSION_1_END_DATE = Instant.parse("2023-02-13T20:00:00.00Z")
+
+ // 10h 15m
+ val SLEEP_SESSION_2_START_DATE = Instant.parse("2023-02-13T22:30:00.00Z")
+ val SLEEP_SESSION_2_END_DATE = Instant.parse("2023-02-14T08:45:00.00Z")
+
+ // 2h 20m
+ val SLEEP_SESSION_3_START_DATE = Instant.parse("2023-02-13T16:00:00.00Z")
+ val SLEEP_SESSION_3_END_DATE = Instant.parse("2023-02-13T18:20:00.00Z")
+
+ // 10h
+ val SLEEP_SESSION_4_START_DATE = Instant.parse("2023-02-13T22:00:00.00Z")
+ val SLEEP_SESSION_4_END_DATE = Instant.parse("2023-02-14T08:00:00.00Z")
+
+ // 5d 11h - Should not affect aggregation
+ val pastDateWithSleepData = LocalDate.of(2023, 2, 11)
+ val SLEEP_SESSION_5_START_DATE = Instant.parse("2023-02-11T22:00:00.00Z")
+ val SLEEP_SESSION_5_END_DATE = Instant.parse("2023-02-16T09:00:00.00Z")
+
+ loadPriorityEntriesUseCase.setEntriesList(
+ lastDateWithSleepData,
+ getSleepSessionRecords(
+ listOf(
+ Pair(SLEEP_SESSION_1_START_DATE, SLEEP_SESSION_1_END_DATE),
+ Pair(SLEEP_SESSION_2_START_DATE, SLEEP_SESSION_2_END_DATE),
+ Pair(SLEEP_SESSION_3_START_DATE, SLEEP_SESSION_3_END_DATE),
+ Pair(SLEEP_SESSION_4_START_DATE, SLEEP_SESSION_4_END_DATE))))
+
+ loadPriorityEntriesUseCase.setEntriesList(
+ pastDateWithSleepData,
+ getSleepSessionRecords(
+ listOf(Pair(SLEEP_SESSION_5_START_DATE, SLEEP_SESSION_5_END_DATE))))
+
+ // minStartTime = SLEEP_SESSION_4_START_DATE
+ // maxEndTime = SLEEP_SESSION_2_END_DATE
+ // Total time = 13 Feb 22:00 - 14 Feb 08:45 = 10h 45m
+ val result = sleepSessionHelper.clusterSleepSessions(lastDateWithSleepData)
+ assertThat(result is UseCaseResults.Success).isTrue()
+ assertThat((result as UseCaseResults.Success).data)
+ .isEqualTo(Pair(SLEEP_SESSION_4_START_DATE, SLEEP_SESSION_2_END_DATE))
+ }
+
+ @Test
+ fun clusterSessions_whenLoadPriorityEntriesFails_returnsFailure() = runTest {
+ val queryDate = LocalDate.of(2023, 1, 30)
+ loadPriorityEntriesUseCase.setFailure("Exception")
+
+ val result = sleepSessionHelper.clusterSleepSessions(queryDate)
+ assertThat(result is UseCaseResults.Failed).isTrue()
+ assertThat((result as UseCaseResults.Failed).exception.message).isEqualTo("Exception")
+ }
+
+ @Test
+ fun clusterSessions_whenNoData_returnsNull() = runTest {
+ val queryDate = LocalDate.of(2023, 1, 30)
+
+ val result = sleepSessionHelper.clusterSleepSessions(queryDate)
+ assertThat(result is UseCaseResults.Success).isTrue()
+ assertThat((result as UseCaseResults.Success).data).isNull()
+ }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/deletion/DeletionFragmentTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/deletion/DeletionFragmentTest.kt
index ef3c558c..29163821 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/deletion/DeletionFragmentTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/deletion/DeletionFragmentTest.kt
@@ -31,8 +31,10 @@ import com.android.healthconnect.controller.R
import com.android.healthconnect.controller.deletion.ChosenRange
import com.android.healthconnect.controller.deletion.DeletionConstants.DELETION_TYPE
import com.android.healthconnect.controller.deletion.DeletionConstants.START_DELETION_EVENT
+import com.android.healthconnect.controller.deletion.DeletionConstants.START_INACTIVE_APP_DELETION_EVENT
import com.android.healthconnect.controller.deletion.DeletionFragment
import com.android.healthconnect.controller.deletion.DeletionParameters
+import com.android.healthconnect.controller.deletion.DeletionState
import com.android.healthconnect.controller.deletion.DeletionType
import com.android.healthconnect.controller.deletion.DeletionViewModel
import com.android.healthconnect.controller.permissions.data.HealthPermissionType
@@ -213,6 +215,12 @@ class DeletionFragmentTest {
onView(withText("Permanently delete all data from the last 24 hours?"))
.inRoot(isDialog())
.check(matches(isDisplayed()))
+
+ onView(
+ withText(
+ "Connected apps will no longer be able to access this data from Health\u00A0Connect"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
}
@Test
@@ -240,6 +248,12 @@ class DeletionFragmentTest {
onView(withText("Permanently delete all data from the last 7 days?"))
.inRoot(isDialog())
.check(matches(isDisplayed()))
+
+ onView(
+ withText(
+ "Connected apps will no longer be able to access this data from Health\u00A0Connect"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
}
@Test
@@ -267,6 +281,12 @@ class DeletionFragmentTest {
onView(withText("Permanently delete all data from the last 30 days?"))
.inRoot(isDialog())
.check(matches(isDisplayed()))
+
+ onView(
+ withText(
+ "Connected apps will no longer be able to access this data from Health\u00A0Connect"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
}
@Test
@@ -294,6 +314,12 @@ class DeletionFragmentTest {
onView(withText("Permanently delete all data from all time?"))
.inRoot(isDialog())
.check(matches(isDisplayed()))
+
+ onView(
+ withText(
+ "Connected apps will no longer be able to access this data from Health\u00A0Connect"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
}
@Test
@@ -322,6 +348,12 @@ class DeletionFragmentTest {
onView(withText("Permanently delete activity data from the last 24 hours?"))
.inRoot(isDialog())
.check(matches(isDisplayed()))
+
+ onView(
+ withText(
+ "Connected apps will no longer be able to access this data from Health\u00A0Connect"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
}
@Test
@@ -349,6 +381,12 @@ class DeletionFragmentTest {
onView(withText("Permanently delete activity data from the last 7 days?"))
.inRoot(isDialog())
.check(matches(isDisplayed()))
+
+ onView(
+ withText(
+ "Connected apps will no longer be able to access this data from Health\u00A0Connect"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
}
@Test
@@ -377,6 +415,12 @@ class DeletionFragmentTest {
onView(withText("Permanently delete activity data from the last 30 days?"))
.inRoot(isDialog())
.check(matches(isDisplayed()))
+
+ onView(
+ withText(
+ "Connected apps will no longer be able to access this data from Health\u00A0Connect"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
}
@Test
@@ -404,6 +448,12 @@ class DeletionFragmentTest {
onView(withText("Permanently delete activity data from all time?"))
.inRoot(isDialog())
.check(matches(isDisplayed()))
+
+ onView(
+ withText(
+ "Connected apps will no longer be able to access this data from Health\u00A0Connect"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
}
@Test
@@ -434,6 +484,12 @@ class DeletionFragmentTest {
onView(withText("Permanently delete blood glucose data from the last 24 hours?"))
.inRoot(isDialog())
.check(matches(isDisplayed()))
+
+ onView(
+ withText(
+ "Connected apps will no longer be able to access this data from Health\u00A0Connect"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
}
@Test
@@ -463,6 +519,12 @@ class DeletionFragmentTest {
onView(withText("Permanently delete blood glucose data from the last 7 days?"))
.inRoot(isDialog())
.check(matches(isDisplayed()))
+
+ onView(
+ withText(
+ "Connected apps will no longer be able to access this data from Health\u00A0Connect"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
}
@Test
@@ -493,6 +555,12 @@ class DeletionFragmentTest {
onView(withText("Permanently delete blood glucose data from the last 30 days?"))
.inRoot(isDialog())
.check(matches(isDisplayed()))
+
+ onView(
+ withText(
+ "Connected apps will no longer be able to access this data from Health\u00A0Connect"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
}
@Test
@@ -522,6 +590,12 @@ class DeletionFragmentTest {
onView(withText("Permanently delete blood glucose data from all time?"))
.inRoot(isDialog())
.check(matches(isDisplayed()))
+
+ onView(
+ withText(
+ "Connected apps will no longer be able to access this data from Health\u00A0Connect"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
}
@Test
@@ -551,6 +625,12 @@ class DeletionFragmentTest {
onView(withText("Permanently delete $TEST_APP_NAME data from the last 24 hours?"))
.inRoot(isDialog())
.check(matches(isDisplayed()))
+
+ onView(
+ withText(
+ "Connected apps will no longer be able to access this data from Health\u00A0Connect"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
}
@Test
@@ -579,6 +659,12 @@ class DeletionFragmentTest {
onView(withText("Permanently delete $TEST_APP_NAME data from the last 7 days?"))
.inRoot(isDialog())
.check(matches(isDisplayed()))
+
+ onView(
+ withText(
+ "Connected apps will no longer be able to access this data from Health\u00A0Connect"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
}
@Test
@@ -608,6 +694,12 @@ class DeletionFragmentTest {
onView(withText("Permanently delete $TEST_APP_NAME data from the last 30 days?"))
.inRoot(isDialog())
.check(matches(isDisplayed()))
+
+ onView(
+ withText(
+ "Connected apps will no longer be able to access this data from Health\u00A0Connect"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
}
@Test
@@ -636,6 +728,43 @@ class DeletionFragmentTest {
onView(withText("Permanently delete $TEST_APP_NAME data from all time?"))
.inRoot(isDialog())
.check(matches(isDisplayed()))
+
+ onView(
+ withText(
+ "Connected apps will no longer be able to access this data from Health\u00A0Connect"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
+ }
+
+ @Test
+ fun deleteInActiveAppData_confirmationDialog_showsCorrectText() {
+ val deletionTypeAppData =
+ DeletionType.DeletionTypeAppData(
+ packageName = TEST_APP_PACKAGE_NAME, appName = TEST_APP_NAME)
+ Mockito.`when`(viewModel.deletionParameters).then {
+ MutableLiveData(
+ DeletionParameters(
+ deletionType = deletionTypeAppData,
+ chosenRange = ChosenRange.DELETE_RANGE_ALL_DATA))
+ }
+
+ launchFragment<DeletionFragment>(Bundle()) {
+ (this as DeletionFragment)
+ .parentFragmentManager
+ .setFragmentResult(
+ START_INACTIVE_APP_DELETION_EVENT,
+ bundleOf(DELETION_TYPE to deletionTypeAppData))
+ }
+
+ onView(withText("Permanently delete $TEST_APP_NAME data from all time?"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
+
+ onView(
+ withText(
+ "Connected apps will no longer be able to access this data from Health\u00A0Connect"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
}
@Test
@@ -666,9 +795,27 @@ class DeletionFragmentTest {
.inRoot(isDialog())
.check(matches(isDisplayed()))
+ onView(
+ withText(
+ "Connected apps will no longer be able to access this data from Health\u00A0Connect"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
+
onView(withText("Go back")).inRoot(isDialog()).perform(click())
onView(withText("Choose data to delete")).inRoot(isDialog()).check(matches(isDisplayed()))
+ onView(
+ withText(
+ "This permanently deletes all data added to Health\u00A0Connect in the chosen" +
+ " time period"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
+ onView(withText("Delete last 24 hours")).inRoot(isDialog()).check(matches(isDisplayed()))
+ onView(withText("Delete last 7 days")).inRoot(isDialog()).check(matches(isDisplayed()))
+ onView(withText("Delete last 30 days")).inRoot(isDialog()).check(matches(isDisplayed()))
+ onView(withText("Delete all data")).inRoot(isDialog()).check(matches(isDisplayed()))
+ onView(withText("Cancel")).inRoot(isDialog()).check(matches(isDisplayed()))
+ onView(withText("Next")).inRoot(isDialog()).check(matches(isDisplayed()))
}
@Test
@@ -693,7 +840,13 @@ class DeletionFragmentTest {
onView(
withText(
- "Connected apps will no longer be able to access this data from Health Connect"))
+ "Connected apps will no longer be able to access this data from Health\u00A0Connect"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
+
+ onView(
+ withText(
+ "Connected apps will no longer be able to access this data from Health\u00A0Connect"))
.inRoot(isDialog())
.check(matches(isDisplayed()))
@@ -729,8 +882,174 @@ class DeletionFragmentTest {
.inRoot(isDialog())
.check(matches(isDisplayed()))
+ onView(
+ withText(
+ "Connected apps will no longer be able to access this data from Health\u00A0Connect"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
+
onView(withText("Cancel")).inRoot(isDialog()).perform(click())
onView(withText("Permanently delete all data from all time?")).check(doesNotExist())
}
+
+ @Test
+ fun deleteFragment_progressIndicatorStartedState_progressIndicatorShown() {
+ val deletionTypeAllData = DeletionType.DeletionTypeAllData()
+
+ Mockito.`when`(viewModel.deletionParameters).then {
+ MutableLiveData(
+ DeletionParameters(
+ deletionState = DeletionState.STATE_PROGRESS_INDICATOR_STARTED,
+ deletionType = deletionTypeAllData,
+ chosenRange = ChosenRange.DELETE_RANGE_ALL_DATA))
+ }
+
+ launchFragment<DeletionFragment>(Bundle()) {
+ (this as DeletionFragment)
+ .parentFragmentManager
+ .setFragmentResult(
+ START_DELETION_EVENT, bundleOf(DELETION_TYPE to deletionTypeAllData))
+ }
+
+ onView(withId(R.id.radio_button_all)).inRoot(isDialog()).perform(click())
+
+ onView(withText("Next")).inRoot(isDialog()).perform(click())
+
+ onView(withText("Permanently delete all data from all time?"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
+
+ onView(
+ withText(
+ "Connected apps will no longer be able to access this data from Health\u00A0Connect"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
+
+ onView(withText("Delete")).inRoot(isDialog()).perform(click())
+
+ onView(withText("Deleting your data")).inRoot(isDialog()).check(matches(isDisplayed()))
+ }
+
+ @Test
+ fun deleteFragment_progressIndicatorCanEndState_progressIndicatorDisappears() {
+ val deletionTypeAllData = DeletionType.DeletionTypeAllData()
+
+ Mockito.`when`(viewModel.deletionParameters).then {
+ MutableLiveData(
+ DeletionParameters(
+ deletionState = DeletionState.STATE_PROGRESS_INDICATOR_CAN_END,
+ deletionType = deletionTypeAllData,
+ chosenRange = ChosenRange.DELETE_RANGE_ALL_DATA))
+ }
+
+ launchFragment<DeletionFragment>(Bundle()) {
+ (this as DeletionFragment)
+ .parentFragmentManager
+ .setFragmentResult(
+ START_DELETION_EVENT, bundleOf(DELETION_TYPE to deletionTypeAllData))
+ }
+
+ onView(withId(R.id.radio_button_all)).inRoot(isDialog()).perform(click())
+
+ onView(withText("Next")).inRoot(isDialog()).perform(click())
+
+ onView(withText("Permanently delete all data from all time?"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
+
+ onView(
+ withText(
+ "Connected apps will no longer be able to access this data from Health\u00A0Connect"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
+
+ onView(withText("Delete")).inRoot(isDialog()).perform(click())
+
+ onView(withText("Deleting your data")).check(doesNotExist())
+ }
+
+ @Test
+ fun deleteFragment_deletionSuccessfulState_successMessageShown() {
+ val deletionTypeAllData = DeletionType.DeletionTypeAllData()
+
+ Mockito.`when`(viewModel.deletionParameters).then {
+ MutableLiveData(
+ DeletionParameters(
+ deletionState = DeletionState.STATE_DELETION_SUCCESSFUL,
+ deletionType = deletionTypeAllData,
+ chosenRange = ChosenRange.DELETE_RANGE_ALL_DATA))
+ }
+
+ launchFragment<DeletionFragment>(Bundle()) {
+ (this as DeletionFragment)
+ .parentFragmentManager
+ .setFragmentResult(
+ START_DELETION_EVENT, bundleOf(DELETION_TYPE to deletionTypeAllData))
+ }
+
+ onView(withId(R.id.radio_button_all)).inRoot(isDialog()).perform(click())
+
+ onView(withText("Next")).inRoot(isDialog()).perform(click())
+
+ onView(withText("Permanently delete all data from all time?"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
+
+ onView(
+ withText(
+ "Connected apps will no longer be able to access this data from Health\u00A0Connect"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
+
+ onView(withText("Delete")).inRoot(isDialog()).perform(click())
+
+ onView(withText("Deleting your data")).inRoot(isDialog()).check(doesNotExist())
+ onView(withText("Data deleted")).inRoot(isDialog()).check(matches(isDisplayed()))
+ onView(withText("This data is no longer stored in Health\u00A0Connect."))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
+ }
+
+ @Test
+ fun deleteFragment_deletionFailedState_failureMessageShown() {
+ val deletionTypeAllData = DeletionType.DeletionTypeAllData()
+
+ Mockito.`when`(viewModel.deletionParameters).then {
+ MutableLiveData(
+ DeletionParameters(
+ deletionState = DeletionState.STATE_DELETION_FAILED,
+ deletionType = deletionTypeAllData,
+ chosenRange = ChosenRange.DELETE_RANGE_ALL_DATA))
+ }
+
+ launchFragment<DeletionFragment>(Bundle()) {
+ (this as DeletionFragment)
+ .parentFragmentManager
+ .setFragmentResult(
+ START_DELETION_EVENT, bundleOf(DELETION_TYPE to deletionTypeAllData))
+ }
+
+ onView(withId(R.id.radio_button_all)).inRoot(isDialog()).perform(click())
+
+ onView(withText("Next")).inRoot(isDialog()).perform(click())
+
+ onView(withText("Permanently delete all data from all time?"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
+
+ onView(
+ withText(
+ "Connected apps will no longer be able to access this data from Health\u00A0Connect"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
+
+ onView(withText("Delete")).inRoot(isDialog()).perform(click())
+
+ onView(withText("Deleting your data")).inRoot(isDialog()).check(doesNotExist())
+ onView(withText("Couldn't delete data")).inRoot(isDialog()).check(matches(isDisplayed()))
+ onView(withText("Something went wrong and Health\u00A0Connect couldn't delete your data"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
+ }
}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/deletion/DeletionParametersTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/deletion/DeletionParametersTest.kt
index 79fa525c..903c4e9c 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/deletion/DeletionParametersTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/deletion/DeletionParametersTest.kt
@@ -6,9 +6,15 @@ import com.android.healthconnect.controller.deletion.ChosenRange
import com.android.healthconnect.controller.deletion.DeletionParameters
import com.android.healthconnect.controller.deletion.DeletionState
import com.android.healthconnect.controller.deletion.DeletionType
+import com.android.healthconnect.controller.permissions.data.HealthPermissionStrings
import com.android.healthconnect.controller.permissions.data.HealthPermissionType
+import com.android.healthconnect.controller.tests.utils.TEST_APP_NAME
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME
+import java.lang.IllegalStateException
+import java.time.Duration
import java.time.Instant
import junit.framework.Assert.assertTrue
+import org.junit.Assert.assertThrows
import org.junit.Test
class DeletionParametersTest {
@@ -98,4 +104,114 @@ class DeletionParametersTest {
assertTrue(
deletionParameters.showTimeRangePickerDialog == outValue.showTimeRangePickerDialog)
}
+
+ @Test
+ fun getPermissionTypeLabel_permissionTypeFromApp_correctPermissionLabelReturned() {
+ val deletionParameters =
+ DeletionParameters(
+ chosenRange = ChosenRange.DELETE_RANGE_LAST_24_HOURS,
+ deletionType =
+ DeletionType.DeletionTypeHealthPermissionTypeFromApp(
+ HealthPermissionType.ACTIVE_CALORIES_BURNED,
+ packageName = TEST_APP_PACKAGE_NAME,
+ appName = TEST_APP_NAME),
+ )
+
+ assertTrue(
+ deletionParameters.getPermissionTypeLabel() ==
+ HealthPermissionStrings.fromPermissionType(
+ HealthPermissionType.ACTIVE_CALORIES_BURNED)
+ .lowercaseLabel)
+ }
+
+ @Test
+ fun getPermissionTypeLabel_permissionTypeDoesNotExist_errorThrown() {
+ val deletionParameters = DeletionParameters()
+
+ assertThrows(IllegalStateException::class.java) {
+ deletionParameters.getPermissionTypeLabel()
+ }
+ }
+
+ @Test
+ fun getCategoryLabel_categoryDataDoesNotExist_errorThrown() {
+ val deletionParameters = DeletionParameters()
+
+ assertThrows(IllegalStateException::class.java) { deletionParameters.getCategoryLabel() }
+ }
+
+ @Test
+ fun getStartTimeInstant_24HoursRangeSelected_correctStartTimeReturned() {
+ val deletionParameters =
+ DeletionParameters(
+ chosenRange = ChosenRange.DELETE_RANGE_LAST_24_HOURS,
+ deletionType = DeletionType.DeletionTypeAllData())
+
+ assertTrue(
+ deletionParameters.getStartTimeInstant() ==
+ deletionParameters.getEndTimeInstant().minus(Duration.ofDays(1)))
+ }
+
+ @Test
+ fun getStartTimeInstant_7DaysRangeSelected_correctStartTimeReturned() {
+ val deletionParameters =
+ DeletionParameters(
+ chosenRange = ChosenRange.DELETE_RANGE_LAST_7_DAYS,
+ deletionType = DeletionType.DeletionTypeAllData())
+
+ assertTrue(
+ deletionParameters.getStartTimeInstant() ==
+ deletionParameters.getEndTimeInstant().minus(Duration.ofDays(7)))
+ }
+
+ @Test
+ fun getStartTimeInstant_30DaysRangeSelected_correctStartTimeReturned() {
+ val deletionParameters =
+ DeletionParameters(
+ chosenRange = ChosenRange.DELETE_RANGE_LAST_30_DAYS,
+ deletionType = DeletionType.DeletionTypeAllData())
+
+ assertTrue(
+ deletionParameters.getStartTimeInstant() ==
+ deletionParameters.getEndTimeInstant().minus(Duration.ofDays(30)))
+ }
+
+ @Test
+ fun getStartTimeInstant_allTimeRangeSelected_correctStartTimeReturned() {
+ val deletionParameters =
+ DeletionParameters(
+ chosenRange = ChosenRange.DELETE_RANGE_ALL_DATA,
+ deletionType = DeletionType.DeletionTypeAllData())
+
+ assertTrue(deletionParameters.getStartTimeInstant() == Instant.EPOCH)
+ }
+
+ @Test
+ fun getEndTimeInstant_24HoursRangeSelected_correctEndTimeReturned() {
+ val startTime = Instant.parse("2022-11-11T20:00:00.000Z")
+ val endTime = Instant.parse("2022-11-14T20:00:00.000Z")
+ val deletionParameters =
+ DeletionParameters(
+ chosenRange = ChosenRange.DELETE_RANGE_LAST_7_DAYS,
+ startTimeMs = startTime.toEpochMilli(),
+ endTimeMs = endTime.toEpochMilli(),
+ deletionType = DeletionType.DeletionTypeAllData())
+
+ assertTrue(
+ deletionParameters.getEndTimeInstant() == Instant.ofEpochMilli(endTime.toEpochMilli()))
+ }
+
+ @Test
+ fun getEndTimeInstant_allTimeRangeSelected_correctEndTimeReturned() {
+ val startTime = Instant.parse("2022-11-11T20:00:00.000Z")
+ val endTime = Instant.parse("2022-11-14T20:00:00.000Z")
+ val deletionParameters =
+ DeletionParameters(
+ chosenRange = ChosenRange.DELETE_RANGE_ALL_DATA,
+ startTimeMs = startTime.toEpochMilli(),
+ endTimeMs = endTime.toEpochMilli(),
+ deletionType = DeletionType.DeletionTypeAllData())
+
+ assertTrue(deletionParameters.getEndTimeInstant() == Instant.ofEpochMilli(Long.MAX_VALUE))
+ }
}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/managedata/ManageDataFragmentTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/managedata/ManageDataFragmentTest.kt
index 1e5e90a6..59196e2a 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/managedata/ManageDataFragmentTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/managedata/ManageDataFragmentTest.kt
@@ -17,27 +17,27 @@ import com.android.healthconnect.controller.utils.FeatureUtils
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
+import javax.inject.Inject
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito
-import javax.inject.Inject
@HiltAndroidTest
class ManageDataFragmentTest {
- @get:Rule
- val hiltRule = HiltAndroidRule(this)
+ @get:Rule val hiltRule = HiltAndroidRule(this)
@BindValue
val autoDeleteViewModel: AutoDeleteViewModel = Mockito.mock(AutoDeleteViewModel::class.java)
- @Inject
- lateinit var fakeFeatureUtils: FeatureUtils
+ @Inject lateinit var fakeFeatureUtils: FeatureUtils
@Before
fun setup() {
hiltRule.inject()
whenever(autoDeleteViewModel.storedAutoDeleteRange).then {
- MutableLiveData(AutoDeleteViewModel.AutoDeleteState.WithData(AutoDeleteRange.AUTO_DELETE_RANGE_NEVER))
+ MutableLiveData(
+ AutoDeleteViewModel.AutoDeleteState.WithData(
+ AutoDeleteRange.AUTO_DELETE_RANGE_NEVER))
}
}
@@ -47,7 +47,7 @@ class ManageDataFragmentTest {
launchFragment<ManageDataFragment>(Bundle())
onView(withText("Auto-delete")).check(matches(isDisplayed()))
- onView(withText("Data sources & priority")).check(matches(isDisplayed()))
+ onView(withText("Data sources and priority")).check(matches(isDisplayed()))
onView(withText("Set units")).check(matches(isDisplayed()))
}
@@ -57,7 +57,7 @@ class ManageDataFragmentTest {
launchFragment<ManageDataFragment>(Bundle())
onView(withText("Auto-delete")).check(matches(isDisplayed()))
- onView(withText("Data sources & priority")).check(doesNotExist())
+ onView(withText("Data sources and priority")).check(doesNotExist())
onView(withText("Set units")).check(matches(isDisplayed()))
}
-} \ No newline at end of file
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/migration/AppUpdateRequiredFragmentTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/migration/AppUpdateRequiredFragmentTest.kt
new file mode 100644
index 00000000..3ce5df5f
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/migration/AppUpdateRequiredFragmentTest.kt
@@ -0,0 +1,113 @@
+package com.android.healthconnect.controller.tests.migration
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.intent.Intents
+import androidx.test.espresso.intent.Intents.intended
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasPackage
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import com.android.healthconnect.controller.migration.AppUpdateRequiredFragment
+import com.android.healthconnect.controller.tests.utils.launchFragment
+import com.android.healthconnect.controller.tests.utils.whenever
+import com.android.healthconnect.controller.utils.AppStoreUtils
+import com.android.healthconnect.controller.utils.NavigationUtils
+import com.google.common.truth.Truth.assertThat
+import dagger.hilt.android.testing.BindValue
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.hamcrest.Matchers.allOf
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito
+import org.mockito.Mockito.doNothing
+import org.mockito.kotlin.any
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+
+@HiltAndroidTest
+class AppUpdateRequiredFragmentTest {
+
+ @get:Rule val hiltRule = HiltAndroidRule(this)
+ @BindValue val appStoreUtils: AppStoreUtils = Mockito.mock(AppStoreUtils::class.java)
+ @BindValue val navigationUtils: NavigationUtils = Mockito.mock(NavigationUtils::class.java)
+
+ @Before
+ fun setup() {
+ hiltRule.inject()
+ Intents.init()
+ }
+
+ @After
+ fun tearDown() {
+ Intents.release()
+ }
+
+ @Test
+ fun appUpdateRequiredFragment_displaysCorrectly() {
+ launchFragment<AppUpdateRequiredFragment>(Bundle())
+
+ onView(withText("Update needed")).check(matches(isDisplayed()))
+ onView(
+ withText(
+ "Health Connect is being integrated with the Android system so " +
+ "you can access it directly from your settings."))
+ .check(matches(isDisplayed()))
+ onView(withText("Before continuing, update the Health Connect app to the latest version."))
+ .check(matches(isDisplayed()))
+ onView(withText("Cancel")).check(matches(isDisplayed()))
+ onView(withText("Update")).check(matches(isDisplayed()))
+ }
+
+ @Test
+ fun appUpdateRequiredFragment_ifAppStoreExists_intentToAppStore() {
+ whenever(appStoreUtils.getAppStoreLink(any()))
+ .thenReturn(
+ Intent(Intent.ACTION_SHOW_APP_INFO).also {
+ it.setPackage("installer.package.name")
+ })
+ whenever(navigationUtils.startActivity(any(), any())).thenCallRealMethod()
+ launchFragment<AppUpdateRequiredFragment>(Bundle())
+ onView(withText("Update")).check(matches(isDisplayed()))
+ onView(withText("Update")).perform(click())
+
+ intended(
+ allOf(hasAction(Intent.ACTION_SHOW_APP_INFO), hasPackage("installer.package.name")))
+ }
+
+ @Test
+ fun appUpdateRequiredFragment_ifAppStoreDoesNotExist_doesNotNavigateToAppStore() {
+ whenever(appStoreUtils.getAppStoreLink(any())).thenReturn(null)
+
+ launchFragment<AppUpdateRequiredFragment>(Bundle())
+ onView(withText("Update")).check(matches(isDisplayed()))
+ onView(withText("Update")).perform(click())
+
+ // Check we are still on the same page
+ onView(withText("Before continuing, update the Health Connect app to the latest version."))
+ .check(matches(isDisplayed()))
+
+ verify(navigationUtils, never()).startActivity(any(), any())
+ }
+
+ @Test
+ fun appUpdateRequiredFragment_whenCancelButtonPressed_setsSharedPreferences() {
+ doNothing().whenever(navigationUtils).navigate(any(), any())
+ val scenario = launchFragment<AppUpdateRequiredFragment>(Bundle())
+ onView(withText("Cancel")).check(matches(isDisplayed()))
+ onView(withText("Cancel")).perform(click())
+
+ scenario.onActivity { activity ->
+ val preferences =
+ activity.getSharedPreferences("USER_ACTIVITY_TRACKER", Context.MODE_PRIVATE)
+ assertThat(preferences.getBoolean("App Update Seen", false)).isTrue()
+ }
+ }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/migration/MigrationInProgressFragmentTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/migration/MigrationInProgressFragmentTest.kt
new file mode 100644
index 00000000..4cd9bdc9
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/migration/MigrationInProgressFragmentTest.kt
@@ -0,0 +1,36 @@
+package com.android.healthconnect.controller.tests.migration
+
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import com.android.healthconnect.controller.migration.MigrationInProgressFragment
+import com.android.healthconnect.controller.tests.utils.launchFragment
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@HiltAndroidTest
+class MigrationInProgressFragmentTest {
+
+ @get:Rule val hiltRule = HiltAndroidRule(this)
+
+ @Before
+ fun setup() {
+ hiltRule.inject()
+ }
+
+ @Test
+ fun migrationInProgressFragment_displaysCorrectly() {
+ launchFragment<MigrationInProgressFragment>()
+
+ onView(withText("Integration in progress")).check(matches(isDisplayed()))
+ onView(
+ withText(
+ "Health Connect is being integrated with the Android system." +
+ "\n\nIt may take some time while your data and permissions are being transferred."))
+ .check(matches(isDisplayed()))
+ }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/migration/MigrationNavigationFragmentTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/migration/MigrationNavigationFragmentTest.kt
new file mode 100644
index 00000000..3a586710
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/migration/MigrationNavigationFragmentTest.kt
@@ -0,0 +1,217 @@
+package com.android.healthconnect.controller.tests.migration
+
+import android.content.Context
+import androidx.lifecycle.MutableLiveData
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import com.android.healthconnect.controller.R
+import com.android.healthconnect.controller.migration.MigrationNavigationFragment
+import com.android.healthconnect.controller.migration.MigrationViewModel
+import com.android.healthconnect.controller.migration.api.MigrationState
+import com.android.healthconnect.controller.tests.utils.launchFragment
+import com.android.healthconnect.controller.tests.utils.whenever
+import com.android.healthconnect.controller.utils.NavigationUtils
+import com.google.common.truth.Truth
+import dagger.hilt.android.testing.BindValue
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+@HiltAndroidTest
+class MigrationNavigationFragmentTest {
+
+ @get:Rule val hiltRule = HiltAndroidRule(this)
+ @BindValue val navigationUtils: NavigationUtils = Mockito.mock(NavigationUtils::class.java)
+ @BindValue
+ val migrationViewModel: MigrationViewModel = Mockito.mock(MigrationViewModel::class.java)
+
+ @Before
+ fun setup() {
+ hiltRule.inject()
+ }
+
+ @Test
+ fun migrationNavigationFragment_whenMigrationLoading_showsLoading() {
+ whenever(migrationViewModel.migrationState).then {
+ MutableLiveData<MigrationViewModel.MigrationFragmentState>(
+ MigrationViewModel.MigrationFragmentState.Loading)
+ }
+
+ launchFragment<MigrationNavigationFragment>()
+
+ onView(withId(R.id.progress_indicator)).check(matches(isDisplayed()))
+ }
+
+ @Test
+ fun migrationNavigationFragment_whenMigrationError_showsError() {
+ whenever(migrationViewModel.migrationState).then {
+ MutableLiveData<MigrationViewModel.MigrationFragmentState>(
+ MigrationViewModel.MigrationFragmentState.Error)
+ }
+
+ launchFragment<MigrationNavigationFragment>()
+
+ onView(withId(R.id.error_view)).check(matches(isDisplayed()))
+ }
+
+ @Test
+ fun migrationNavigationFragment_whenMigrationStateAllowedNotStarted_navigatesToMigrationPausedFragment() {
+ Mockito.doNothing().whenever(navigationUtils).navigate(any(), any())
+ whenever(migrationViewModel.migrationState).then {
+ MutableLiveData<MigrationViewModel.MigrationFragmentState>(
+ MigrationViewModel.MigrationFragmentState.WithData(
+ MigrationState.ALLOWED_NOT_STARTED))
+ }
+ launchFragment<MigrationNavigationFragment>()
+
+ verify(navigationUtils, times(1))
+ .navigate(any(), eq(R.id.action_migrationNavigationFragment_to_migrationPausedFragment))
+ }
+
+ @Test
+ fun migrationNavigationFragment_whenMigrationStateAllowedPaused_navigatesToMigrationPausedFragment() {
+ Mockito.doNothing().whenever(navigationUtils).navigate(any(), any())
+ whenever(migrationViewModel.migrationState).then {
+ MutableLiveData<MigrationViewModel.MigrationFragmentState>(
+ MigrationViewModel.MigrationFragmentState.WithData(MigrationState.ALLOWED_PAUSED))
+ }
+ launchFragment<MigrationNavigationFragment>()
+
+ verify(navigationUtils, times(1))
+ .navigate(any(), eq(R.id.action_migrationNavigationFragment_to_migrationPausedFragment))
+ }
+
+ @Test
+ fun migrationNavigationFragment_whenMigrationStateAppUpdateRequired_navigatesToAppUpdateRequiredFragment() {
+ Mockito.doNothing().whenever(navigationUtils).navigate(any(), any())
+ whenever(migrationViewModel.migrationState).then {
+ MutableLiveData<MigrationViewModel.MigrationFragmentState>(
+ MigrationViewModel.MigrationFragmentState.WithData(
+ MigrationState.APP_UPGRADE_REQUIRED))
+ }
+ launchFragment<MigrationNavigationFragment>()
+
+ verify(navigationUtils, times(1))
+ .navigate(
+ any(),
+ eq(R.id.action_migrationNavigationFragment_to_migrationAppUpdateNeededFragment))
+ }
+
+ @Test
+ fun migrationNavigationFragment_whenMigrationStateModuleUpdateRequired_navigatesToModuleUpdateRequiredFragment() {
+ Mockito.doNothing().whenever(navigationUtils).navigate(any(), any())
+ whenever(migrationViewModel.migrationState).then {
+ MutableLiveData<MigrationViewModel.MigrationFragmentState>(
+ MigrationViewModel.MigrationFragmentState.WithData(
+ MigrationState.MODULE_UPGRADE_REQUIRED))
+ }
+ launchFragment<MigrationNavigationFragment>()
+
+ verify(navigationUtils, times(1))
+ .navigate(
+ any(),
+ eq(R.id.action_migrationNavigationFragment_to_migrationModuleUpdateNeededFragment))
+ }
+
+ @Test
+ fun migrationNavigationFragment_whenMigrationStateInProgress_navigatesToMigrationInProgressFragment() {
+ Mockito.doNothing().whenever(navigationUtils).navigate(any(), any())
+ whenever(migrationViewModel.migrationState).then {
+ MutableLiveData<MigrationViewModel.MigrationFragmentState>(
+ MigrationViewModel.MigrationFragmentState.WithData(MigrationState.IN_PROGRESS))
+ }
+ launchFragment<MigrationNavigationFragment>()
+
+ verify(navigationUtils, times(1))
+ .navigate(
+ any(), eq(R.id.action_migrationNavigationFragment_to_migrationInProgressFragment))
+ }
+
+ @Test
+ fun migrationNavigationFragment_whenMigrationStateCompleteIdle_setsPreferenceAndNavigatesToHomeFragment() {
+ Mockito.doNothing().whenever(navigationUtils).navigate(any(), any())
+ whenever(migrationViewModel.migrationState).then {
+ MutableLiveData<MigrationViewModel.MigrationFragmentState>(
+ MigrationViewModel.MigrationFragmentState.WithData(MigrationState.COMPLETE_IDLE))
+ }
+ val scenario = launchFragment<MigrationNavigationFragment>()
+ scenario.onActivity { activity ->
+ val preferences =
+ activity.getSharedPreferences("USER_ACTIVITY_TRACKER", Context.MODE_PRIVATE)
+ Truth.assertThat(preferences.getBoolean("migration_complete_key", false)).isTrue()
+ }
+
+ verify(navigationUtils, times(1))
+ .navigate(any(), eq(R.id.action_migrationNavigationFragment_to_homeFragment))
+ }
+
+ @Test
+ fun migrationNavigationFragment_whenMigrationStateComplete_setsPreferenceAndNavigatesToHomeFragment() {
+ Mockito.doNothing().whenever(navigationUtils).navigate(any(), any())
+ whenever(migrationViewModel.migrationState).then {
+ MutableLiveData<MigrationViewModel.MigrationFragmentState>(
+ MigrationViewModel.MigrationFragmentState.WithData(MigrationState.COMPLETE))
+ }
+ val scenario = launchFragment<MigrationNavigationFragment>()
+ scenario.onActivity { activity ->
+ val preferences =
+ activity.getSharedPreferences("USER_ACTIVITY_TRACKER", Context.MODE_PRIVATE)
+ Truth.assertThat(preferences.getBoolean("migration_complete_key", false)).isTrue()
+ }
+
+ verify(navigationUtils, times(1))
+ .navigate(any(), eq(R.id.action_migrationNavigationFragment_to_homeFragment))
+ }
+
+ @Test
+ fun migrationNavigationFragment_whenMigrationStateIdle_navigatesToHomeFragment() {
+ Mockito.doNothing().whenever(navigationUtils).navigate(any(), any())
+ whenever(migrationViewModel.migrationState).then {
+ MutableLiveData<MigrationViewModel.MigrationFragmentState>(
+ MigrationViewModel.MigrationFragmentState.WithData(MigrationState.IDLE))
+ }
+
+ launchFragment<MigrationNavigationFragment>()
+
+ verify(navigationUtils, times(1))
+ .navigate(any(), eq(R.id.action_migrationNavigationFragment_to_homeFragment))
+ }
+
+ @Test
+ fun migrationNavigationFragment_whenMigrationStateAllowedMigratorDisabled_navigatesToHomeFragment() {
+ Mockito.doNothing().whenever(navigationUtils).navigate(any(), any())
+ whenever(migrationViewModel.migrationState).then {
+ MutableLiveData<MigrationViewModel.MigrationFragmentState>(
+ MigrationViewModel.MigrationFragmentState.WithData(
+ MigrationState.ALLOWED_MIGRATOR_DISABLED))
+ }
+
+ launchFragment<MigrationNavigationFragment>()
+
+ verify(navigationUtils, times(1))
+ .navigate(any(), eq(R.id.action_migrationNavigationFragment_to_homeFragment))
+ }
+
+ @Test
+ fun migrationNavigationFragment_whenMigrationStateUnknown_navigatesToHomeFragment() {
+ Mockito.doNothing().whenever(navigationUtils).navigate(any(), any())
+ whenever(migrationViewModel.migrationState).then {
+ MutableLiveData<MigrationViewModel.MigrationFragmentState>(
+ MigrationViewModel.MigrationFragmentState.WithData(MigrationState.UNKNOWN))
+ }
+
+ launchFragment<MigrationNavigationFragment>()
+
+ verify(navigationUtils, times(1))
+ .navigate(any(), eq(R.id.action_migrationNavigationFragment_to_homeFragment))
+ }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/migration/MigrationPausedFragmentTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/migration/MigrationPausedFragmentTest.kt
new file mode 100644
index 00000000..c120f645
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/migration/MigrationPausedFragmentTest.kt
@@ -0,0 +1,96 @@
+package com.android.healthconnect.controller.tests.migration
+
+import android.content.Context
+import android.os.Bundle
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import com.android.healthconnect.controller.R
+import com.android.healthconnect.controller.migration.MigrationPausedFragment
+import com.android.healthconnect.controller.tests.utils.launchFragment
+import com.android.healthconnect.controller.tests.utils.whenever
+import com.android.healthconnect.controller.utils.NavigationUtils
+import com.google.common.truth.Truth
+import dagger.hilt.android.testing.BindValue
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+@HiltAndroidTest
+class MigrationPausedFragmentTest {
+
+ @get:Rule val hiltRule = HiltAndroidRule(this)
+ @BindValue val navigationUtils: NavigationUtils = Mockito.mock(NavigationUtils::class.java)
+
+ @Before
+ fun setup() {
+ hiltRule.inject()
+ }
+
+ @Test
+ fun migrationPausedFragment_displaysCorrectly() {
+ launchFragment<MigrationPausedFragment>()
+
+ onView(withText("Integration paused")).check(matches(isDisplayed()))
+ onView(
+ withText(
+ "The Health Connect app closed while it was being integrated " +
+ "with the Android system.\n\nClick resume to reopen the app and continue " +
+ "transferring your data and permissions."))
+ .check(matches(isDisplayed()))
+ onView(withText("Cancel")).check(matches(isDisplayed()))
+ onView(withText("Resume")).check(matches(isDisplayed()))
+ }
+
+ @Test
+ fun migrationPausedFragment_whenCancelButtonPressed_setsSharedPreferences() {
+ Mockito.doNothing().whenever(navigationUtils).navigate(any(), any())
+ val scenario = launchFragment<MigrationPausedFragment>(Bundle())
+ onView(withText("Cancel")).check(matches(isDisplayed()))
+ onView(withText("Cancel")).perform(ViewActions.click())
+
+ scenario.onActivity { activity ->
+ val preferences =
+ activity.getSharedPreferences("USER_ACTIVITY_TRACKER", Context.MODE_PRIVATE)
+ Truth.assertThat(preferences.getBoolean("integration_paused_seen", false)).isTrue()
+ }
+ }
+
+ @Test
+ fun migrationPausedFragment_whenResumeButtonPressed_navigatesToMigratorApk() {
+ Mockito.doNothing().whenever(navigationUtils).navigate(any(), any())
+ launchFragment<MigrationPausedFragment>(Bundle())
+ onView(withText("Resume")).check(matches(isDisplayed()))
+ onView(withText("Resume")).perform(ViewActions.click())
+
+ verify(navigationUtils, times(1))
+ .navigate(any(), eq(R.id.action_migrationPausedFragment_to_migrationApk))
+ }
+
+ @Test
+ fun migrationPausedFragment_whenNavigateToMigratorApkFails_displaysCorrectly() {
+ whenever(navigationUtils.navigate(any(), any())).thenThrow(RuntimeException("Exception"))
+ launchFragment<MigrationPausedFragment>(Bundle())
+ onView(withText("Resume")).check(matches(isDisplayed()))
+ onView(withText("Resume")).perform(ViewActions.click())
+
+ onView(withText("Integration paused")).check(matches(isDisplayed()))
+ onView(
+ withText(
+ "The Health Connect app closed while it was being integrated " +
+ "with the Android system.\n\nClick resume to reopen the app and continue " +
+ "transferring your data and permissions."))
+ .check(matches(isDisplayed()))
+ onView(withText("Cancel")).check(matches(isDisplayed()))
+ onView(withText("Resume")).check(matches(isDisplayed()))
+ }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/migration/ModuleUpdateRequiredFragmentTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/migration/ModuleUpdateRequiredFragmentTest.kt
new file mode 100644
index 00000000..8c19ca61
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/migration/ModuleUpdateRequiredFragmentTest.kt
@@ -0,0 +1,117 @@
+package com.android.healthconnect.controller.tests.migration
+
+import android.content.Context
+import android.os.Bundle
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.intent.Intents
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import com.android.healthconnect.controller.R
+import com.android.healthconnect.controller.migration.ModuleUpdateRequiredFragment
+import com.android.healthconnect.controller.tests.utils.launchFragment
+import com.android.healthconnect.controller.tests.utils.whenever
+import com.android.healthconnect.controller.utils.NavigationUtils
+import com.google.common.truth.Truth
+import dagger.hilt.android.testing.BindValue
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+@HiltAndroidTest
+class ModuleUpdateRequiredFragmentTest {
+
+ @get:Rule val hiltRule = HiltAndroidRule(this)
+ @BindValue val navigationUtils: NavigationUtils = Mockito.mock(NavigationUtils::class.java)
+
+ @Before
+ fun setup() {
+ hiltRule.inject()
+ Intents.init()
+ }
+
+ @After
+ fun tearDown() {
+ Intents.release()
+ }
+
+ @Test
+ fun moduleUpdateRequiredFragment_displaysCorrectly() {
+ launchFragment<ModuleUpdateRequiredFragment>()
+
+ onView(withText("Update needed")).check(matches(isDisplayed()))
+ onView(
+ withText(
+ "Health Connect is being integrated with the Android system so " +
+ "you can access it directly from your settings."))
+ .check(matches(isDisplayed()))
+ onView(withText("Before continuing, update your phone system."))
+ .check(matches(isDisplayed()))
+ onView(
+ withText(
+ "If you\'ve already updated your phone system, " +
+ "try restarting your phone to continue the integration"))
+ .check(matches(isDisplayed()))
+ onView(withText("Cancel")).check(matches(isDisplayed()))
+ onView(withText("Update")).check(matches(isDisplayed()))
+ }
+
+ @Test
+ fun moduleUpdateRequiredFragment_whenCancelButtonPressed_setsSharedPreferences() {
+ Mockito.doNothing().whenever(navigationUtils).navigate(any(), any())
+ val scenario = launchFragment<ModuleUpdateRequiredFragment>(Bundle())
+ onView(withText("Cancel")).check(matches(isDisplayed()))
+ onView(withText("Cancel")).perform(ViewActions.click())
+
+ scenario.onActivity { activity ->
+ val preferences =
+ activity.getSharedPreferences("USER_ACTIVITY_TRACKER", Context.MODE_PRIVATE)
+ Truth.assertThat(preferences.getBoolean("Module Update Seen", false)).isTrue()
+ }
+ }
+
+ @Test
+ fun moduleUpdateRequiredFragment_whenUpdateButtonPressed_navigatesToSystemUpdate() {
+ Mockito.doNothing().whenever(navigationUtils).navigate(any(), any())
+ launchFragment<ModuleUpdateRequiredFragment>(Bundle())
+ onView(withText("Update")).check(matches(isDisplayed()))
+ onView(withText("Update")).perform(ViewActions.click())
+
+ verify(navigationUtils, times(1))
+ .navigate(
+ any(), eq(R.id.action_migrationModuleUpdateNeededFragment_to_systemUpdateActivity))
+ }
+
+ @Test
+ fun moduleUpdateRequiredFragment_whenNavigateToSystemUpdateFails_displaysCorrectly() {
+ whenever(navigationUtils.navigate(any(), any())).thenThrow(RuntimeException("Exception"))
+ launchFragment<ModuleUpdateRequiredFragment>(Bundle())
+ onView(withText("Update")).check(matches(isDisplayed()))
+ onView(withText("Update")).perform(ViewActions.click())
+
+ onView(withText("Update needed")).check(matches(isDisplayed()))
+ onView(
+ withText(
+ "Health Connect is being integrated with the Android system so " +
+ "you can access it directly from your settings."))
+ .check(matches(isDisplayed()))
+ onView(withText("Before continuing, update your phone system."))
+ .check(matches(isDisplayed()))
+ onView(
+ withText(
+ "If you\'ve already updated your phone system, " +
+ "try restarting your phone to continue the integration"))
+ .check(matches(isDisplayed()))
+ onView(withText("Cancel")).check(matches(isDisplayed()))
+ onView(withText("Update")).check(matches(isDisplayed()))
+ }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/onboarding/OnboardingScreenTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/onboarding/OnboardingScreenTest.kt
index c6b4719e..dffc80ac 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/onboarding/OnboardingScreenTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/onboarding/OnboardingScreenTest.kt
@@ -21,7 +21,6 @@ import android.content.Intent
import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
-import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.*
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.scrollTo
@@ -41,6 +40,7 @@ import org.junit.Test
@HiltAndroidTest
class OnboardingScreenTest {
@get:Rule val hiltRule = HiltAndroidRule(this)
+
@Before
fun setup() {
hiltRule.inject()
@@ -92,7 +92,7 @@ class OnboardingScreenTest {
onIdle()
onView(withId(R.id.go_back_button)).perform(ViewActions.click())
Thread.sleep(4_000) // Need to wait for Activity to close before checking state
- assertEquals(Lifecycle.State.DESTROYED, scenario.state)
+ assertEquals(Lifecycle.State.DESTROYED, scenario.getState())
}
@Test
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/permissions/api/GetGrantedHealthPermissionsUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/permissions/api/GetGrantedHealthPermissionsUseCaseTest.kt
index 5c8d229f..e1f5524e 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/permissions/api/GetGrantedHealthPermissionsUseCaseTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/permissions/api/GetGrantedHealthPermissionsUseCaseTest.kt
@@ -19,10 +19,13 @@ import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import com.android.healthconnect.controller.permissions.api.GetGrantedHealthPermissionsUseCase
import com.android.healthconnect.controller.permissions.api.HealthPermissionManager
+import com.android.healthconnect.controller.tests.utils.whenever
+import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
class GetGrantedHealthPermissionsUseCaseTest {
@@ -43,4 +46,25 @@ class GetGrantedHealthPermissionsUseCaseTest {
verify(healthPermissionManager).getGrantedHealthPermissions("TEST_APP")
}
+
+ @Test
+ fun invoke_callsHealthPermissionManager_returnsCorrectList() {
+ val expectedList = listOf("permission1", "permission2")
+ whenever(healthPermissionManager.getGrantedHealthPermissions(any()))
+ .thenReturn(expectedList)
+ val result = useCase.invoke("TEST_APP")
+
+ verify(healthPermissionManager).getGrantedHealthPermissions("TEST_APP")
+ assertThat(result).containsExactlyElementsIn(expectedList)
+ }
+
+ @Test
+ fun invoke_whenHealthPermissionManagerFails_returnsEmptyList() {
+ whenever(healthPermissionManager.getGrantedHealthPermissions(any()))
+ .thenThrow(RuntimeException("Error!"))
+ val result = useCase.invoke("TEST_APP")
+
+ verify(healthPermissionManager).getGrantedHealthPermissions("TEST_APP")
+ assertThat(result).isEmpty()
+ }
}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/permissions/api/GetHealthPermissionsFlagsUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/permissions/api/GetHealthPermissionsFlagsUseCaseTest.kt
index dfd3790b..296c7ed6 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/permissions/api/GetHealthPermissionsFlagsUseCaseTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/permissions/api/GetHealthPermissionsFlagsUseCaseTest.kt
@@ -20,10 +20,13 @@ import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import com.android.healthconnect.controller.permissions.api.GetHealthPermissionsFlagsUseCase
import com.android.healthconnect.controller.permissions.api.HealthPermissionManager
+import com.android.healthconnect.controller.tests.utils.whenever
+import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito
import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
class GetHealthPermissionsFlagsUseCaseTest {
private lateinit var context: Context
@@ -45,4 +48,14 @@ class GetHealthPermissionsFlagsUseCaseTest {
.getHealthPermissionsFlags(
"TEST_APP", listOf("PERMISSION_1", "PERMISSION_2", "PERMISSION_3"))
}
+
+ @Test
+ fun invoke_whenHealthPermissionManagerFails_returnsEmptyMap() {
+ whenever(healthPermissionManager.getHealthPermissionsFlags(any(), any()))
+ .thenThrow(RuntimeException("Exception"))
+
+ val result =
+ useCase.invoke("TEST_APP", listOf("PERMISSION_1", "PERMISSION_2", "PERMISSION_3"))
+ assertThat(result).isEmpty()
+ }
}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/permissions/api/HealthPermissionManagerImplTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/permissions/api/HealthPermissionManagerImplTest.kt
new file mode 100644
index 00000000..24da0d96
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/permissions/api/HealthPermissionManagerImplTest.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.healthconnect.controller.tests.permissions.api
+
+import android.health.connect.HealthConnectManager
+import com.android.healthconnect.controller.permissions.api.HealthPermissionManagerImpl
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+@HiltAndroidTest
+class HealthPermissionManagerImplTest {
+
+ @get:Rule val hiltRule = HiltAndroidRule(this)
+
+ private val healthConnectManager = Mockito.mock(HealthConnectManager::class.java)
+
+ private lateinit var healthPermissionManager: HealthPermissionManagerImpl
+
+ @Before
+ fun setup() {
+ hiltRule.inject()
+ healthPermissionManager = HealthPermissionManagerImpl(healthConnectManager)
+ }
+
+ @Test
+ fun getGrantedHealthPermissions_callsHealthConnectManager() {
+ healthPermissionManager.getGrantedHealthPermissions("packageName")
+
+ verify(healthConnectManager, times(1)).getGrantedHealthPermissions("packageName")
+ }
+
+ @Test
+ fun getHealthPermissionsFlags_callsHealthConnectManager() {
+ val packageName = "package.name"
+ val permissions = listOf("Permission 1", "Permission 2")
+ healthPermissionManager.getHealthPermissionsFlags(packageName, permissions)
+
+ verify(healthConnectManager, times(1)).getHealthPermissionsFlags(packageName, permissions)
+ }
+
+ @Test
+ fun makeHealthPermissionsRequestable_callsHealthConnectManager() {
+ val packageName = "package.name"
+ val permissions = listOf("Permission 1", "Permission 2")
+ healthPermissionManager.makeHealthPermissionsRequestable(packageName, permissions)
+
+ verify(healthConnectManager, times(1))
+ .makeHealthPermissionsRequestable(packageName, permissions)
+ }
+
+ @Test
+ fun grantHealthPermission_callsHealthConnectManager() {
+ val packageName = "package.name"
+ val permission = "Permission 1"
+ healthPermissionManager.grantHealthPermission(packageName, permission)
+
+ verify(healthConnectManager, times(1)).grantHealthPermission(packageName, permission)
+ }
+
+ @Test
+ fun revokeHealthPermission_callsHealthConnectManager() {
+ val packageName = "package.name"
+ val permission = "Permission 1"
+ val reason = ""
+
+ healthPermissionManager.revokeHealthPermission(packageName, permission)
+
+ verify(healthConnectManager, times(1))
+ .revokeHealthPermission(packageName, permission, reason)
+ }
+
+ @Test
+ fun revokeAllHealthPermissions_callsHealthConnectManager() {
+ val packageName = "package.name"
+ val reason = ""
+ healthPermissionManager.revokeAllHealthPermissions(packageName)
+
+ verify(healthConnectManager, times(1)).revokeAllHealthPermissions(packageName, reason)
+ }
+
+ @Test
+ fun loadStartAccessDate_callsHealthConnectManager() {
+ val packageName = "package.name"
+ healthPermissionManager.loadStartAccessDate(packageName)
+
+ verify(healthConnectManager, times(1)).getHealthDataHistoricalAccessStartDate(packageName)
+ }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/permissions/api/LoadAccessDateUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/permissions/api/LoadAccessDateUseCaseTest.kt
new file mode 100644
index 00000000..38526274
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/permissions/api/LoadAccessDateUseCaseTest.kt
@@ -0,0 +1,69 @@
+/**
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * ```
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * ```
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.android.healthconnect.controller.tests.permissions.api
+
+import com.android.healthconnect.controller.permissions.api.HealthPermissionManager
+import com.android.healthconnect.controller.permissions.api.LoadAccessDateUseCase
+import com.android.healthconnect.controller.tests.utils.whenever
+import com.google.common.truth.Truth.assertThat
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import java.time.Instant
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito
+import org.mockito.kotlin.any
+
+@HiltAndroidTest
+class LoadAccessDateUseCaseTest {
+
+ @get:Rule val hiltRule = HiltAndroidRule(this)
+
+ private val healthPermissionManager = Mockito.mock(HealthPermissionManager::class.java)
+
+ private lateinit var loadAccessDateUseCase: LoadAccessDateUseCase
+
+ @Before
+ fun setup() {
+ hiltRule.inject()
+ loadAccessDateUseCase = LoadAccessDateUseCase(healthPermissionManager)
+ }
+
+ @Test
+ fun loadAccessDate_callsHealthPermissionManager() {
+ val expected = Instant.parse("2023-04-16T12:00:00Z")
+ whenever(healthPermissionManager.loadStartAccessDate(any())).thenReturn(expected)
+
+ val result = loadAccessDateUseCase.invoke("package.name")
+ assertThat(result).isEqualTo(expected)
+ }
+
+ @Test
+ fun loadAccessDate_whenHealthPermissionManagerFails_returnsNull() {
+ whenever(healthPermissionManager.loadStartAccessDate(any()))
+ .thenThrow(RuntimeException("Exception"))
+
+ val result = loadAccessDateUseCase.invoke("package.name")
+ assertThat(result).isNull()
+ }
+
+ @Test
+ fun loadAccessDate_whenPackageNameNull_returnsNull() {
+ val result = loadAccessDateUseCase.invoke(null)
+ assertThat(result).isNull()
+ }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/HealthPermissionTypesFragmentTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/HealthPermissionTypesFragmentTest.kt
index 767890ec..62fb37fc 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/HealthPermissionTypesFragmentTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/HealthPermissionTypesFragmentTest.kt
@@ -15,6 +15,7 @@
*/
package com.android.healthconnect.controller.tests.permissiontypes
+import android.health.connect.HealthDataCategory
import android.os.Bundle
import androidx.lifecycle.MutableLiveData
import androidx.test.espresso.Espresso.onView
@@ -23,34 +24,38 @@ import androidx.test.espresso.action.ViewActions.scrollTo
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.RootMatchers.isDialog
+import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
+import com.android.healthconnect.controller.R
import com.android.healthconnect.controller.categories.HealthDataCategoriesFragment
import com.android.healthconnect.controller.permissions.data.HealthPermissionType
import com.android.healthconnect.controller.permissiontypes.HealthPermissionTypesFragment
import com.android.healthconnect.controller.permissiontypes.HealthPermissionTypesViewModel
+import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.lowercaseTitle
import com.android.healthconnect.controller.shared.app.AppMetadata
import com.android.healthconnect.controller.tests.utils.TEST_APP
import com.android.healthconnect.controller.tests.utils.TEST_APP_2
import com.android.healthconnect.controller.tests.utils.TEST_APP_3
+import com.android.healthconnect.controller.tests.utils.atPosition
import com.android.healthconnect.controller.tests.utils.di.FakeFeatureUtils
import com.android.healthconnect.controller.tests.utils.launchFragment
import com.android.healthconnect.controller.utils.FeatureUtils
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
+import javax.inject.Inject
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito
-import javax.inject.Inject
@HiltAndroidTest
class HealthPermissionTypesFragmentTest {
@get:Rule val hiltRule = HiltAndroidRule(this)
- @Inject
- lateinit var fakeFeatureUtils: FeatureUtils
+ @Inject lateinit var fakeFeatureUtils: FeatureUtils
@BindValue
val viewModel: HealthPermissionTypesViewModel =
@@ -63,7 +68,7 @@ class HealthPermissionTypesFragmentTest {
}
@Test
- fun permissionTypesFragment_isDisplayed() {
+ fun permissionTypesFragment_activityCategory_isDisplayed() {
Mockito.`when`(viewModel.permissionTypesData).then {
MutableLiveData<HealthPermissionTypesViewModel.PermissionTypesState>(
HealthPermissionTypesViewModel.PermissionTypesState.WithData(
@@ -102,6 +107,39 @@ class HealthPermissionTypesFragmentTest {
onView(withText("App priority")).check(matches(isDisplayed()))
onView(withText("Health Connect test app")).check(matches(isDisplayed()))
onView(withText("Delete activity data")).check(matches(isDisplayed()))
+ onView(withText("Data sources and priority")).check(doesNotExist())
+ }
+
+ @Test
+ fun permissionTypesFragment_sleepCategory_isDisplayed() {
+ Mockito.`when`(viewModel.permissionTypesData).then {
+ MutableLiveData<HealthPermissionTypesViewModel.PermissionTypesState>(
+ HealthPermissionTypesViewModel.PermissionTypesState.WithData(
+ listOf(HealthPermissionType.SLEEP)))
+ }
+ Mockito.`when`(viewModel.priorityList).then {
+ MutableLiveData<HealthPermissionTypesViewModel.PriorityListState>(
+ HealthPermissionTypesViewModel.PriorityListState.WithData(
+ listOf(TEST_APP, TEST_APP_2)))
+ }
+ Mockito.`when`(viewModel.appsWithData).then {
+ MutableLiveData<HealthPermissionTypesViewModel.AppsWithDataFragmentState>(
+ HealthPermissionTypesViewModel.AppsWithDataFragmentState.WithData(listOf()))
+ }
+ Mockito.`when`(viewModel.selectedAppFilter).then { MutableLiveData("") }
+ Mockito.`when`(viewModel.editedPriorityList).then {
+ MutableLiveData<List<AppMetadata>>(listOf(TEST_APP, TEST_APP_2))
+ }
+ Mockito.`when`(viewModel.categoryLabel).then {
+ MutableLiveData(HealthDataCategory.SLEEP.lowercaseTitle())
+ }
+ launchFragment<HealthPermissionTypesFragment>(sleepCategoryBundle())
+
+ onView(withText("Manage data")).check(matches(isDisplayed()))
+ onView(withText("App priority")).check(matches(isDisplayed()))
+ onView(withText("Health Connect test app")).check(matches(isDisplayed()))
+ onView(withText("Delete sleep data")).check(matches(isDisplayed()))
+ onView(withText("Data sources and priority")).check(doesNotExist())
}
@Test
@@ -143,6 +181,7 @@ class HealthPermissionTypesFragmentTest {
onView(withText("App priority")).check(doesNotExist())
onView(withText("Health Connect test app")).check(doesNotExist())
onView(withText("Delete activity data")).perform(scrollTo()).check(matches(isDisplayed()))
+ onView(withText("Data sources and priority")).check(doesNotExist())
}
@Test
@@ -182,6 +221,7 @@ class HealthPermissionTypesFragmentTest {
onView(withText("App priority")).check(doesNotExist())
onView(withText("Health Connect test app")).check(doesNotExist())
onView(withText("Delete activity data")).perform(scrollTo()).check(matches(isDisplayed()))
+ onView(withText("Data sources and priority")).check(doesNotExist())
}
@Test
@@ -230,6 +270,44 @@ class HealthPermissionTypesFragmentTest {
}
@Test
+ fun permissionTypesFragment_priorityListDialog_priorityListChanged_appsAreArrangedCorrectly() {
+ Mockito.`when`(viewModel.permissionTypesData).then {
+ MutableLiveData<HealthPermissionTypesViewModel.PermissionTypesState>(
+ HealthPermissionTypesViewModel.PermissionTypesState.WithData(
+ listOf(
+ HealthPermissionType.DISTANCE,
+ HealthPermissionType.EXERCISE,
+ HealthPermissionType.STEPS)))
+ }
+ Mockito.`when`(viewModel.priorityList).then {
+ MutableLiveData<HealthPermissionTypesViewModel.PriorityListState>(
+ HealthPermissionTypesViewModel.PriorityListState.WithData(
+ listOf(TEST_APP, TEST_APP_2)))
+ }
+ Mockito.`when`(viewModel.appsWithData).then {
+ MutableLiveData<HealthPermissionTypesViewModel.AppsWithDataFragmentState>(
+ HealthPermissionTypesViewModel.AppsWithDataFragmentState.WithData(listOf()))
+ }
+ Mockito.`when`(viewModel.selectedAppFilter).then { MutableLiveData("") }
+ Mockito.`when`(viewModel.editedPriorityList).then {
+ MutableLiveData(listOf(TEST_APP_2, TEST_APP))
+ }
+ Mockito.`when`(viewModel.categoryLabel).then { MutableLiveData("activity") }
+
+ val expectedAppsOrder = listOf("Health Connect test app 2", "Health Connect test app")
+
+ launchFragment<HealthPermissionTypesFragment>(activityCategoryBundle())
+
+ onView(withText("App priority")).perform(click())
+
+ for ((index, expectedItem) in expectedAppsOrder.withIndex()) {
+ onView(withId(R.id.priority_list_recycle_view))
+ .inRoot(isDialog())
+ .check(matches(atPosition(index, hasDescendant(withText(expectedItem)))))
+ }
+ }
+
+ @Test
fun permissionTypesFragment_withTwoOrMoreContributingApps_appFilters_areDisplayed() {
Mockito.`when`(viewModel.permissionTypesData).then {
MutableLiveData<HealthPermissionTypesViewModel.PermissionTypesState>(
@@ -315,7 +393,7 @@ class HealthPermissionTypesFragmentTest {
}
@Test
- fun permissionTypesFragment_whenNewPriorityEnabled_doesNotShowAppPriority() {
+ fun permissionTypesFragment_activityCategory_whenNewPriorityEnabled_showsNewAppPriorityButton() {
(fakeFeatureUtils as FakeFeatureUtils).setIsNewAppPriorityEnabled(true)
Mockito.`when`(viewModel.permissionTypesData).then {
MutableLiveData<HealthPermissionTypesViewModel.PermissionTypesState>(
@@ -353,13 +431,320 @@ class HealthPermissionTypesFragmentTest {
onView(withText("Wheelchair pushes")).check(doesNotExist())
onView(withText("Manage data")).check(matches(isDisplayed()))
onView(withText("App priority")).check(doesNotExist())
+ onView(withText("Data sources and priority")).check(matches(isDisplayed()))
onView(withText("Health Connect test app")).check(doesNotExist())
onView(withText("Delete activity data")).check(matches(isDisplayed()))
}
+ @Test
+ fun permissionTypesFragment_sleepCategory_whenNewPriorityEnabled_showsNewAppPriorityButton() {
+ (fakeFeatureUtils as FakeFeatureUtils).setIsNewAppPriorityEnabled(true)
+ Mockito.`when`(viewModel.permissionTypesData).then {
+ MutableLiveData<HealthPermissionTypesViewModel.PermissionTypesState>(
+ HealthPermissionTypesViewModel.PermissionTypesState.WithData(
+ listOf(HealthPermissionType.SLEEP)))
+ }
+ Mockito.`when`(viewModel.priorityList).then {
+ MutableLiveData<HealthPermissionTypesViewModel.PriorityListState>(
+ HealthPermissionTypesViewModel.PriorityListState.WithData(
+ listOf(TEST_APP, TEST_APP_2)))
+ }
+ Mockito.`when`(viewModel.appsWithData).then {
+ MutableLiveData<HealthPermissionTypesViewModel.AppsWithDataFragmentState>(
+ HealthPermissionTypesViewModel.AppsWithDataFragmentState.WithData(listOf()))
+ }
+ Mockito.`when`(viewModel.selectedAppFilter).then { MutableLiveData("") }
+ Mockito.`when`(viewModel.editedPriorityList).then {
+ MutableLiveData<List<AppMetadata>>(listOf(TEST_APP, TEST_APP_2))
+ }
+ Mockito.`when`(viewModel.categoryLabel).then {
+ MutableLiveData(HealthDataCategory.SLEEP.lowercaseTitle())
+ }
+ launchFragment<HealthPermissionTypesFragment>(sleepCategoryBundle())
+
+ onView(withText("Manage data")).check(matches(isDisplayed()))
+ onView(withText("App priority")).check(doesNotExist())
+ onView(withText("Data sources and priority")).check(matches(isDisplayed()))
+ onView(withText("Health Connect test app")).check(doesNotExist())
+ onView(withText("Delete sleep data")).check(matches(isDisplayed()))
+ }
+
+ @Test
+ fun permissionTypesFragment_whenBodyMeasurementsCategory_doesNotShowOldPriorityButton() {
+ Mockito.`when`(viewModel.permissionTypesData).then {
+ MutableLiveData<HealthPermissionTypesViewModel.PermissionTypesState>(
+ HealthPermissionTypesViewModel.PermissionTypesState.WithData(
+ listOf(
+ HealthPermissionType.BASAL_METABOLIC_RATE,
+ HealthPermissionType.BODY_FAT,
+ HealthPermissionType.HEIGHT)))
+ }
+ Mockito.`when`(viewModel.priorityList).then {
+ MutableLiveData<HealthPermissionTypesViewModel.PriorityListState>(
+ HealthPermissionTypesViewModel.PriorityListState.WithData(
+ listOf(TEST_APP, TEST_APP_2)))
+ }
+ Mockito.`when`(viewModel.appsWithData).then {
+ MutableLiveData<HealthPermissionTypesViewModel.AppsWithDataFragmentState>(
+ HealthPermissionTypesViewModel.AppsWithDataFragmentState.WithData(listOf()))
+ }
+ Mockito.`when`(viewModel.selectedAppFilter).then { MutableLiveData("") }
+ Mockito.`when`(viewModel.editedPriorityList).then {
+ MutableLiveData<List<AppMetadata>>(listOf(TEST_APP, TEST_APP_2))
+ }
+ Mockito.`when`(viewModel.categoryLabel).then {
+ MutableLiveData(HealthDataCategory.BODY_MEASUREMENTS.lowercaseTitle())
+ }
+ launchFragment<HealthPermissionTypesFragment>(bodyMeasurementsCategoryBundle())
+
+ onView(withText("App priority")).check(doesNotExist())
+ onView(withText("Data sources and priority")).check(doesNotExist())
+ }
+
+ @Test
+ fun permissionTypesFragment_whenBodyMeasurementsCategory_doesNotShowNewPriorityButton() {
+ (fakeFeatureUtils as FakeFeatureUtils).setIsNewAppPriorityEnabled(true)
+ Mockito.`when`(viewModel.permissionTypesData).then {
+ MutableLiveData<HealthPermissionTypesViewModel.PermissionTypesState>(
+ HealthPermissionTypesViewModel.PermissionTypesState.WithData(
+ listOf(
+ HealthPermissionType.BASAL_METABOLIC_RATE,
+ HealthPermissionType.BODY_FAT,
+ HealthPermissionType.HEIGHT)))
+ }
+ Mockito.`when`(viewModel.priorityList).then {
+ MutableLiveData<HealthPermissionTypesViewModel.PriorityListState>(
+ HealthPermissionTypesViewModel.PriorityListState.WithData(
+ listOf(TEST_APP, TEST_APP_2)))
+ }
+ Mockito.`when`(viewModel.appsWithData).then {
+ MutableLiveData<HealthPermissionTypesViewModel.AppsWithDataFragmentState>(
+ HealthPermissionTypesViewModel.AppsWithDataFragmentState.WithData(listOf()))
+ }
+ Mockito.`when`(viewModel.selectedAppFilter).then { MutableLiveData("") }
+ Mockito.`when`(viewModel.editedPriorityList).then {
+ MutableLiveData<List<AppMetadata>>(listOf(TEST_APP, TEST_APP_2))
+ }
+ Mockito.`when`(viewModel.categoryLabel).then {
+ MutableLiveData(HealthDataCategory.BODY_MEASUREMENTS.lowercaseTitle())
+ }
+ launchFragment<HealthPermissionTypesFragment>(bodyMeasurementsCategoryBundle())
+
+ onView(withText("App priority")).check(doesNotExist())
+ onView(withText("Data sources and priority")).check(doesNotExist())
+ }
+
+ @Test
+ fun permissionTypesFragment_whenCycleTrackingCategory_doesNotShowOldPriorityButton() {
+ Mockito.`when`(viewModel.permissionTypesData).then {
+ MutableLiveData<HealthPermissionTypesViewModel.PermissionTypesState>(
+ HealthPermissionTypesViewModel.PermissionTypesState.WithData(
+ listOf(HealthPermissionType.MENSTRUATION)))
+ }
+ Mockito.`when`(viewModel.priorityList).then {
+ MutableLiveData<HealthPermissionTypesViewModel.PriorityListState>(
+ HealthPermissionTypesViewModel.PriorityListState.WithData(
+ listOf(TEST_APP, TEST_APP_2)))
+ }
+ Mockito.`when`(viewModel.appsWithData).then {
+ MutableLiveData<HealthPermissionTypesViewModel.AppsWithDataFragmentState>(
+ HealthPermissionTypesViewModel.AppsWithDataFragmentState.WithData(listOf()))
+ }
+ Mockito.`when`(viewModel.selectedAppFilter).then { MutableLiveData("") }
+ Mockito.`when`(viewModel.editedPriorityList).then {
+ MutableLiveData<List<AppMetadata>>(listOf(TEST_APP, TEST_APP_2))
+ }
+ Mockito.`when`(viewModel.categoryLabel).then {
+ MutableLiveData(HealthDataCategory.CYCLE_TRACKING.lowercaseTitle())
+ }
+ launchFragment<HealthPermissionTypesFragment>(cycleCategoryBundle())
+
+ onView(withText("App priority")).check(doesNotExist())
+ onView(withText("Data sources and priority")).check(doesNotExist())
+ }
+
+ @Test
+ fun permissionTypesFragment_whenCycleTrackingCategory_doesNotShowNewPriorityButton() {
+ (fakeFeatureUtils as FakeFeatureUtils).setIsNewAppPriorityEnabled(true)
+ Mockito.`when`(viewModel.permissionTypesData).then {
+ MutableLiveData<HealthPermissionTypesViewModel.PermissionTypesState>(
+ HealthPermissionTypesViewModel.PermissionTypesState.WithData(
+ listOf(HealthPermissionType.MENSTRUATION)))
+ }
+ Mockito.`when`(viewModel.priorityList).then {
+ MutableLiveData<HealthPermissionTypesViewModel.PriorityListState>(
+ HealthPermissionTypesViewModel.PriorityListState.WithData(
+ listOf(TEST_APP, TEST_APP_2)))
+ }
+ Mockito.`when`(viewModel.appsWithData).then {
+ MutableLiveData<HealthPermissionTypesViewModel.AppsWithDataFragmentState>(
+ HealthPermissionTypesViewModel.AppsWithDataFragmentState.WithData(listOf()))
+ }
+ Mockito.`when`(viewModel.selectedAppFilter).then { MutableLiveData("") }
+ Mockito.`when`(viewModel.editedPriorityList).then {
+ MutableLiveData<List<AppMetadata>>(listOf(TEST_APP, TEST_APP_2))
+ }
+ Mockito.`when`(viewModel.categoryLabel).then {
+ MutableLiveData(HealthDataCategory.CYCLE_TRACKING.lowercaseTitle())
+ }
+ launchFragment<HealthPermissionTypesFragment>(cycleCategoryBundle())
+
+ onView(withText("App priority")).check(doesNotExist())
+ onView(withText("Data sources and priority")).check(doesNotExist())
+ }
+
+ @Test
+ fun permissionTypesFragment_whenNutritionCategory_doesNotShowOldPriorityButton() {
+ Mockito.`when`(viewModel.permissionTypesData).then {
+ MutableLiveData<HealthPermissionTypesViewModel.PermissionTypesState>(
+ HealthPermissionTypesViewModel.PermissionTypesState.WithData(
+ listOf(HealthPermissionType.NUTRITION)))
+ }
+ Mockito.`when`(viewModel.priorityList).then {
+ MutableLiveData<HealthPermissionTypesViewModel.PriorityListState>(
+ HealthPermissionTypesViewModel.PriorityListState.WithData(
+ listOf(TEST_APP, TEST_APP_2)))
+ }
+ Mockito.`when`(viewModel.appsWithData).then {
+ MutableLiveData<HealthPermissionTypesViewModel.AppsWithDataFragmentState>(
+ HealthPermissionTypesViewModel.AppsWithDataFragmentState.WithData(listOf()))
+ }
+ Mockito.`when`(viewModel.selectedAppFilter).then { MutableLiveData("") }
+ Mockito.`when`(viewModel.editedPriorityList).then {
+ MutableLiveData<List<AppMetadata>>(listOf(TEST_APP, TEST_APP_2))
+ }
+ Mockito.`when`(viewModel.categoryLabel).then {
+ MutableLiveData(HealthDataCategory.NUTRITION.lowercaseTitle())
+ }
+ launchFragment<HealthPermissionTypesFragment>(nutritionCategoryBundle())
+
+ onView(withText("App priority")).check(doesNotExist())
+ onView(withText("Data sources and priority")).check(doesNotExist())
+ }
+
+ @Test
+ fun permissionTypesFragment_whenNutritionCategory_doesNotShowNewPriorityButton() {
+ (fakeFeatureUtils as FakeFeatureUtils).setIsNewAppPriorityEnabled(true)
+ Mockito.`when`(viewModel.permissionTypesData).then {
+ MutableLiveData<HealthPermissionTypesViewModel.PermissionTypesState>(
+ HealthPermissionTypesViewModel.PermissionTypesState.WithData(
+ listOf(HealthPermissionType.NUTRITION)))
+ }
+ Mockito.`when`(viewModel.priorityList).then {
+ MutableLiveData<HealthPermissionTypesViewModel.PriorityListState>(
+ HealthPermissionTypesViewModel.PriorityListState.WithData(
+ listOf(TEST_APP, TEST_APP_2)))
+ }
+ Mockito.`when`(viewModel.appsWithData).then {
+ MutableLiveData<HealthPermissionTypesViewModel.AppsWithDataFragmentState>(
+ HealthPermissionTypesViewModel.AppsWithDataFragmentState.WithData(listOf()))
+ }
+ Mockito.`when`(viewModel.selectedAppFilter).then { MutableLiveData("") }
+ Mockito.`when`(viewModel.editedPriorityList).then {
+ MutableLiveData<List<AppMetadata>>(listOf(TEST_APP, TEST_APP_2))
+ }
+ Mockito.`when`(viewModel.categoryLabel).then {
+ MutableLiveData(HealthDataCategory.NUTRITION.lowercaseTitle())
+ }
+ launchFragment<HealthPermissionTypesFragment>(nutritionCategoryBundle())
+
+ onView(withText("App priority")).check(doesNotExist())
+ onView(withText("Data sources and priority")).check(doesNotExist())
+ }
+
+ @Test
+ fun permissionTypesFragment_whenVitalsCategory_doesNotShowOldPriorityButton() {
+ Mockito.`when`(viewModel.permissionTypesData).then {
+ MutableLiveData<HealthPermissionTypesViewModel.PermissionTypesState>(
+ HealthPermissionTypesViewModel.PermissionTypesState.WithData(
+ listOf(HealthPermissionType.HEART_RATE)))
+ }
+ Mockito.`when`(viewModel.priorityList).then {
+ MutableLiveData<HealthPermissionTypesViewModel.PriorityListState>(
+ HealthPermissionTypesViewModel.PriorityListState.WithData(
+ listOf(TEST_APP, TEST_APP_2)))
+ }
+ Mockito.`when`(viewModel.appsWithData).then {
+ MutableLiveData<HealthPermissionTypesViewModel.AppsWithDataFragmentState>(
+ HealthPermissionTypesViewModel.AppsWithDataFragmentState.WithData(listOf()))
+ }
+ Mockito.`when`(viewModel.selectedAppFilter).then { MutableLiveData("") }
+ Mockito.`when`(viewModel.editedPriorityList).then {
+ MutableLiveData<List<AppMetadata>>(listOf(TEST_APP, TEST_APP_2))
+ }
+ Mockito.`when`(viewModel.categoryLabel).then {
+ MutableLiveData(HealthDataCategory.VITALS.lowercaseTitle())
+ }
+ launchFragment<HealthPermissionTypesFragment>(vitalsCategoryBundle())
+
+ onView(withText("App priority")).check(doesNotExist())
+ onView(withText("Data sources and priority")).check(doesNotExist())
+ }
+
+ @Test
+ fun permissionTypesFragment_whenVitalsCategory_doesNotShowNewPriorityButton() {
+ (fakeFeatureUtils as FakeFeatureUtils).setIsNewAppPriorityEnabled(true)
+ Mockito.`when`(viewModel.permissionTypesData).then {
+ MutableLiveData<HealthPermissionTypesViewModel.PermissionTypesState>(
+ HealthPermissionTypesViewModel.PermissionTypesState.WithData(
+ listOf(HealthPermissionType.HEART_RATE)))
+ }
+ Mockito.`when`(viewModel.priorityList).then {
+ MutableLiveData<HealthPermissionTypesViewModel.PriorityListState>(
+ HealthPermissionTypesViewModel.PriorityListState.WithData(
+ listOf(TEST_APP, TEST_APP_2)))
+ }
+ Mockito.`when`(viewModel.appsWithData).then {
+ MutableLiveData<HealthPermissionTypesViewModel.AppsWithDataFragmentState>(
+ HealthPermissionTypesViewModel.AppsWithDataFragmentState.WithData(listOf()))
+ }
+ Mockito.`when`(viewModel.selectedAppFilter).then { MutableLiveData("") }
+ Mockito.`when`(viewModel.editedPriorityList).then {
+ MutableLiveData<List<AppMetadata>>(listOf(TEST_APP, TEST_APP_2))
+ }
+ Mockito.`when`(viewModel.categoryLabel).then {
+ MutableLiveData(HealthDataCategory.VITALS.lowercaseTitle())
+ }
+ launchFragment<HealthPermissionTypesFragment>(vitalsCategoryBundle())
+
+ onView(withText("App priority")).check(doesNotExist())
+ onView(withText("Data sources and priority")).check(doesNotExist())
+ }
+
private fun activityCategoryBundle(): Bundle {
val bundle = Bundle()
- bundle.putInt(HealthDataCategoriesFragment.CATEGORY_KEY, 1)
+ bundle.putInt(HealthDataCategoriesFragment.CATEGORY_KEY, HealthDataCategory.ACTIVITY)
+ return bundle
+ }
+
+ private fun bodyMeasurementsCategoryBundle(): Bundle {
+ val bundle = Bundle()
+ bundle.putInt(
+ HealthDataCategoriesFragment.CATEGORY_KEY, HealthDataCategory.BODY_MEASUREMENTS)
+ return bundle
+ }
+
+ private fun cycleCategoryBundle(): Bundle {
+ val bundle = Bundle()
+ bundle.putInt(HealthDataCategoriesFragment.CATEGORY_KEY, HealthDataCategory.CYCLE_TRACKING)
+ return bundle
+ }
+
+ private fun nutritionCategoryBundle(): Bundle {
+ val bundle = Bundle()
+ bundle.putInt(HealthDataCategoriesFragment.CATEGORY_KEY, HealthDataCategory.NUTRITION)
+ return bundle
+ }
+
+ private fun sleepCategoryBundle(): Bundle {
+ val bundle = Bundle()
+ bundle.putInt(HealthDataCategoriesFragment.CATEGORY_KEY, HealthDataCategory.SLEEP)
+ return bundle
+ }
+
+ private fun vitalsCategoryBundle(): Bundle {
+ val bundle = Bundle()
+ bundle.putInt(HealthDataCategoriesFragment.CATEGORY_KEY, HealthDataCategory.VITALS)
return bundle
}
}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/FilterPermissionTypesUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/api/FilterPermissionTypesUseCaseTest.kt
index bd513961..562cf00e 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/FilterPermissionTypesUseCaseTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/api/FilterPermissionTypesUseCaseTest.kt
@@ -1,11 +1,10 @@
-package com.android.healthconnect.controller.tests.permissiontypes
+package com.android.healthconnect.controller.tests.permissiontypes.api
import android.content.Context
import android.health.connect.HealthConnectManager
import android.health.connect.HealthDataCategory
import android.health.connect.HealthPermissionCategory
import android.health.connect.RecordTypeInfoResponse
-import android.health.connect.datatypes.DataOrigin
import android.health.connect.datatypes.ExerciseLap
import android.health.connect.datatypes.ExerciseSegment
import android.health.connect.datatypes.ExerciseSessionRecord
@@ -20,6 +19,7 @@ import com.android.healthconnect.controller.tests.utils.CoroutineTestRule
import com.android.healthconnect.controller.tests.utils.NOW
import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME
import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME_2
+import com.android.healthconnect.controller.tests.utils.getDataOrigin
import com.android.healthconnect.controller.tests.utils.getMetaData
import com.google.common.truth.Truth
import dagger.hilt.android.testing.HiltAndroidRule
@@ -158,8 +158,4 @@ class FilterPermissionTypesUseCaseTest {
.setSegments(segments)
.build()
}
-
- private fun getDataOrigin(packageName: String): DataOrigin {
- return DataOrigin.Builder().setPackageName(packageName).build()
- }
}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/api/LoadContributingAppsUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/api/LoadContributingAppsUseCaseTest.kt
new file mode 100644
index 00000000..e310b132
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/api/LoadContributingAppsUseCaseTest.kt
@@ -0,0 +1,138 @@
+package com.android.healthconnect.controller.tests.permissiontypes.api
+
+import android.content.Context
+import android.health.connect.HealthConnectManager
+import android.health.connect.HealthDataCategory
+import android.health.connect.HealthPermissionCategory
+import android.health.connect.RecordTypeInfoResponse
+import android.health.connect.datatypes.ExerciseLap
+import android.health.connect.datatypes.ExerciseSegment
+import android.health.connect.datatypes.ExerciseSessionRecord
+import android.health.connect.datatypes.ExerciseSessionType
+import android.health.connect.datatypes.Record
+import android.health.connect.datatypes.StepsCadenceRecord
+import android.os.OutcomeReceiver
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.healthconnect.controller.permissiontypes.api.LoadContributingAppsUseCase
+import com.android.healthconnect.controller.shared.app.AppInfoReader
+import com.android.healthconnect.controller.tests.utils.CoroutineTestRule
+import com.android.healthconnect.controller.tests.utils.NOW
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME_2
+import com.android.healthconnect.controller.tests.utils.getDataOrigin
+import com.android.healthconnect.controller.tests.utils.getMetaData
+import com.google.common.truth.Truth
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import javax.inject.Inject
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Matchers
+import org.mockito.Mockito
+import org.mockito.invocation.InvocationOnMock
+
+@ExperimentalCoroutinesApi
+@HiltAndroidTest
+class LoadContributingAppsUseCaseTest {
+ @get:Rule val hiltRule = HiltAndroidRule(this)
+ @get:Rule val coroutineTestRule = CoroutineTestRule()
+
+ private var manager: HealthConnectManager = Mockito.mock(HealthConnectManager::class.java)
+ @Inject lateinit var appInfoReader: AppInfoReader
+ private lateinit var usecase: LoadContributingAppsUseCase
+ private lateinit var context: Context
+
+ @Before
+ fun setup() {
+ hiltRule.inject()
+ context = InstrumentationRegistry.getInstrumentation().context
+ usecase = LoadContributingAppsUseCase(appInfoReader, manager, Dispatchers.Main)
+ }
+
+ @Test
+ fun loadContributingApps_contributingAppsLoadedCorrectly() = runTest {
+ val recordTypeInfo =
+ mapOf<Record, RecordTypeInfoResponse>(
+ getStepsCadence(listOf(10.3, 20.1)) to
+ RecordTypeInfoResponse(
+ HealthPermissionCategory.STEPS,
+ HealthDataCategory.ACTIVITY,
+ listOf(getDataOrigin(TEST_APP_PACKAGE_NAME))),
+ getRecord(type = ExerciseSessionType.EXERCISE_SESSION_TYPE_BIKING) to
+ RecordTypeInfoResponse(
+ HealthPermissionCategory.EXERCISE,
+ HealthDataCategory.ACTIVITY,
+ listOf(getDataOrigin(TEST_APP_PACKAGE_NAME_2))))
+
+ Mockito.doAnswer(prepareAnswer(recordTypeInfo))
+ .`when`(manager)
+ .queryAllRecordTypesInfo(Matchers.any(), Matchers.any())
+
+ val loadedContributingApps = usecase.invoke(HealthDataCategory.ACTIVITY)
+
+ Truth.assertThat(loadedContributingApps.size).isEqualTo(2)
+ Truth.assertThat(loadedContributingApps)
+ .containsExactlyElementsIn(
+ listOf(
+ appInfoReader.getAppMetadata(TEST_APP_PACKAGE_NAME),
+ appInfoReader.getAppMetadata(TEST_APP_PACKAGE_NAME_2)))
+ }
+
+ @Test
+ fun loadContributingApps_failedToLoadData_emptyListReturned() = runTest {
+ Mockito.doThrow(RuntimeException())
+ .`when`(manager)
+ .queryAllRecordTypesInfo(Matchers.any(), Matchers.any())
+
+ val loadedContributingApps = usecase.invoke(HealthDataCategory.ACTIVITY)
+
+ Truth.assertThat(loadedContributingApps).isEmpty()
+ }
+
+ private fun prepareAnswer(
+ recordTypeInfo: Map<Record, RecordTypeInfoResponse>
+ ): (InvocationOnMock) -> Nothing? {
+ val answer = { args: InvocationOnMock ->
+ val receiver =
+ args.arguments[1] as OutcomeReceiver<Map<Record, RecordTypeInfoResponse>, *>
+ receiver.onResult(recordTypeInfo)
+ null
+ }
+ return answer
+ }
+
+ private fun prepareNullAnswer(): (InvocationOnMock) -> Nothing? {
+ val answer = { _: InvocationOnMock -> null }
+ return answer
+ }
+
+ private fun getStepsCadence(samples: List<Double>): StepsCadenceRecord {
+ return StepsCadenceRecord.Builder(
+ getMetaData(),
+ NOW,
+ NOW.plusSeconds(samples.size.toLong() + 1),
+ samples.map { rate ->
+ StepsCadenceRecord.StepsCadenceRecordSample(rate, NOW.plusSeconds(1))
+ })
+ .build()
+ }
+
+ private fun getRecord(
+ type: Int = ExerciseSessionType.EXERCISE_SESSION_TYPE_BIKING,
+ title: String? = null,
+ note: String? = null,
+ laps: List<ExerciseLap> = emptyList(),
+ segments: List<ExerciseSegment> = emptyList()
+ ): ExerciseSessionRecord {
+ return ExerciseSessionRecord.Builder(getMetaData(), NOW, NOW.plusSeconds(1000), type)
+ .setNotes(note)
+ .setLaps(laps)
+ .setTitle(title)
+ .setSegments(segments)
+ .build()
+ }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/api/LoadPermissionTypesUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/api/LoadPermissionTypesUseCaseTest.kt
new file mode 100644
index 00000000..77d2174e
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/api/LoadPermissionTypesUseCaseTest.kt
@@ -0,0 +1,129 @@
+package com.android.healthconnect.controller.tests.permissiontypes.api
+
+import android.content.Context
+import android.health.connect.HealthConnectManager
+import android.health.connect.HealthDataCategory
+import android.health.connect.HealthPermissionCategory
+import android.health.connect.RecordTypeInfoResponse
+import android.health.connect.datatypes.ExerciseLap
+import android.health.connect.datatypes.ExerciseSegment
+import android.health.connect.datatypes.ExerciseSessionRecord
+import android.health.connect.datatypes.ExerciseSessionType
+import android.health.connect.datatypes.Record
+import android.health.connect.datatypes.StepsCadenceRecord
+import android.os.OutcomeReceiver
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType
+import com.android.healthconnect.controller.permissiontypes.api.LoadPermissionTypesUseCase
+import com.android.healthconnect.controller.tests.utils.CoroutineTestRule
+import com.android.healthconnect.controller.tests.utils.NOW
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME_2
+import com.android.healthconnect.controller.tests.utils.getDataOrigin
+import com.android.healthconnect.controller.tests.utils.getMetaData
+import com.google.common.truth.Truth
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Matchers
+import org.mockito.Mockito
+import org.mockito.invocation.InvocationOnMock
+
+@ExperimentalCoroutinesApi
+@HiltAndroidTest
+class LoadPermissionTypesUseCaseTest {
+ @get:Rule val hiltRule = HiltAndroidRule(this)
+ @get:Rule val coroutineTestRule = CoroutineTestRule()
+
+ private var manager: HealthConnectManager = Mockito.mock(HealthConnectManager::class.java)
+ private lateinit var usecase: LoadPermissionTypesUseCase
+ private lateinit var context: Context
+
+ @Before
+ fun setup() {
+ hiltRule.inject()
+ context = InstrumentationRegistry.getInstrumentation().context
+ usecase = LoadPermissionTypesUseCase(manager, Dispatchers.Main)
+ }
+
+ @Test
+ fun loadPermissionTypes_permissionTypesUnderCategoryLoadedCorrectly() = runTest {
+ val recordTypeInfo =
+ mapOf<Record, RecordTypeInfoResponse>(
+ getStepsCadence(listOf(10.3, 20.1)) to
+ RecordTypeInfoResponse(
+ HealthPermissionCategory.STEPS,
+ HealthDataCategory.ACTIVITY,
+ listOf(getDataOrigin(TEST_APP_PACKAGE_NAME))),
+ getRecord(type = ExerciseSessionType.EXERCISE_SESSION_TYPE_BIKING) to
+ RecordTypeInfoResponse(
+ HealthPermissionCategory.EXERCISE,
+ HealthDataCategory.ACTIVITY,
+ listOf(getDataOrigin(TEST_APP_PACKAGE_NAME_2))))
+
+ Mockito.doAnswer(prepareAnswer(recordTypeInfo))
+ .`when`(manager)
+ .queryAllRecordTypesInfo(Matchers.any(), Matchers.any())
+
+ val loadedContributingApps = usecase.invoke(HealthDataCategory.ACTIVITY)
+
+ Truth.assertThat(loadedContributingApps.size).isEqualTo(2)
+ Truth.assertThat(loadedContributingApps)
+ .containsExactlyElementsIn(
+ listOf(HealthPermissionType.STEPS, HealthPermissionType.EXERCISE))
+ }
+
+ @Test
+ fun loadPermissionTypes_failedToLoadData_emptyListReturned() = runTest {
+ Mockito.doThrow(RuntimeException())
+ .`when`(manager)
+ .queryAllRecordTypesInfo(Matchers.any(), Matchers.any())
+
+ val loadedContributingApps = usecase.invoke(HealthDataCategory.ACTIVITY)
+
+ Truth.assertThat(loadedContributingApps).isEmpty()
+ }
+
+ private fun prepareAnswer(
+ recordTypeInfo: Map<Record, RecordTypeInfoResponse>
+ ): (InvocationOnMock) -> Nothing? {
+ val answer = { args: InvocationOnMock ->
+ val receiver =
+ args.arguments[1] as OutcomeReceiver<Map<Record, RecordTypeInfoResponse>, *>
+ receiver.onResult(recordTypeInfo)
+ null
+ }
+ return answer
+ }
+
+ private fun getStepsCadence(samples: List<Double>): StepsCadenceRecord {
+ return StepsCadenceRecord.Builder(
+ getMetaData(),
+ NOW,
+ NOW.plusSeconds(samples.size.toLong() + 1),
+ samples.map { rate ->
+ StepsCadenceRecord.StepsCadenceRecordSample(rate, NOW.plusSeconds(1))
+ })
+ .build()
+ }
+
+ private fun getRecord(
+ type: Int = ExerciseSessionType.EXERCISE_SESSION_TYPE_BIKING,
+ title: String? = null,
+ note: String? = null,
+ laps: List<ExerciseLap> = emptyList(),
+ segments: List<ExerciseSegment> = emptyList()
+ ): ExerciseSessionRecord {
+ return ExerciseSessionRecord.Builder(getMetaData(), NOW, NOW.plusSeconds(1000), type)
+ .setNotes(note)
+ .setLaps(laps)
+ .setTitle(title)
+ .setSegments(segments)
+ .build()
+ }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/api/LoadPriorityListUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/api/LoadPriorityListUseCaseTest.kt
new file mode 100644
index 00000000..75dbf0bd
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/api/LoadPriorityListUseCaseTest.kt
@@ -0,0 +1,81 @@
+package com.android.healthconnect.controller.tests.permissiontypes.api
+
+import android.content.Context
+import android.health.connect.FetchDataOriginsPriorityOrderResponse
+import android.health.connect.HealthConnectManager
+import android.health.connect.HealthDataCategory
+import android.os.OutcomeReceiver
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.healthconnect.controller.permissiontypes.api.LoadPriorityListUseCase
+import com.android.healthconnect.controller.shared.app.AppInfoReader
+import com.android.healthconnect.controller.tests.utils.CoroutineTestRule
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME_2
+import com.android.healthconnect.controller.tests.utils.getDataOrigin
+import com.google.common.truth.Truth
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import javax.inject.Inject
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Matchers
+import org.mockito.Mockito
+import org.mockito.invocation.InvocationOnMock
+
+@ExperimentalCoroutinesApi
+@HiltAndroidTest
+class LoadPriorityListUseCaseTest {
+ @get:Rule val hiltRule = HiltAndroidRule(this)
+ @get:Rule val coroutineTestRule = CoroutineTestRule()
+
+ private val manager: HealthConnectManager = Mockito.mock(HealthConnectManager::class.java)
+ @Inject lateinit var appInfoReader: AppInfoReader
+ private lateinit var usecase: LoadPriorityListUseCase
+ private lateinit var context: Context
+
+ @Before
+ fun setup() {
+ hiltRule.inject()
+ context = InstrumentationRegistry.getInstrumentation().context
+ usecase = LoadPriorityListUseCase(manager, appInfoReader, Dispatchers.Main)
+ }
+
+ @Test
+ fun loadPriorityList_listOfAppsInPriorityListReturnedCorrectly() = runTest {
+ val dataOriginsPriorityOrderResponse =
+ FetchDataOriginsPriorityOrderResponse(
+ mutableListOf(
+ getDataOrigin(TEST_APP_PACKAGE_NAME), getDataOrigin(TEST_APP_PACKAGE_NAME_2)))
+
+ Mockito.doAnswer(prepareAnswer(dataOriginsPriorityOrderResponse))
+ .`when`(manager)
+ .fetchDataOriginsPriorityOrder(
+ Matchers.eq(HealthDataCategory.ACTIVITY), Matchers.any(), Matchers.any())
+
+ val loadedAppsPriorityList = usecase.execute(HealthDataCategory.ACTIVITY)
+
+ Truth.assertThat(loadedAppsPriorityList.size).isEqualTo(2)
+
+ Truth.assertThat(loadedAppsPriorityList)
+ .contains(appInfoReader.getAppMetadata(TEST_APP_PACKAGE_NAME))
+
+ Truth.assertThat(loadedAppsPriorityList)
+ .contains(appInfoReader.getAppMetadata(TEST_APP_PACKAGE_NAME_2))
+ }
+
+ private fun prepareAnswer(
+ fetchDataOriginsPriorityOrderResponse: FetchDataOriginsPriorityOrderResponse
+ ): (InvocationOnMock) -> Nothing? {
+ val answer = { args: InvocationOnMock ->
+ val receiver =
+ args.arguments[2] as OutcomeReceiver<FetchDataOriginsPriorityOrderResponse, *>
+ receiver.onResult(fetchDataOriginsPriorityOrderResponse)
+ null
+ }
+ return answer
+ }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/api/UpdatePriorityListUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/api/UpdatePriorityListUseCaseTest.kt
new file mode 100644
index 00000000..d34baa24
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/api/UpdatePriorityListUseCaseTest.kt
@@ -0,0 +1,76 @@
+package com.android.healthconnect.controller.tests.permissiontypes.api
+
+import android.content.Context
+import android.health.connect.HealthConnectManager
+import android.health.connect.HealthDataCategory
+import android.health.connect.UpdateDataOriginPriorityOrderRequest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.healthconnect.controller.permissiontypes.api.UpdatePriorityListUseCase
+import com.android.healthconnect.controller.shared.app.AppInfoReader
+import com.android.healthconnect.controller.tests.utils.CoroutineTestRule
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME_2
+import com.android.healthconnect.controller.tests.utils.getDataOrigin
+import com.google.common.truth.Truth
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import javax.inject.Inject
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+import org.mockito.invocation.InvocationOnMock
+
+@ExperimentalCoroutinesApi
+@HiltAndroidTest
+class UpdatePriorityListUseCaseTest {
+ @get:Rule val hiltRule = HiltAndroidRule(this)
+ @get:Rule val coroutineTestRule = CoroutineTestRule()
+
+ private val manager: HealthConnectManager = Mockito.mock(HealthConnectManager::class.java)
+ private lateinit var useCase: UpdatePriorityListUseCase
+ @Inject lateinit var appInfoReader: AppInfoReader
+ private lateinit var context: Context
+ @Captor lateinit var filtersCaptor: ArgumentCaptor<UpdateDataOriginPriorityOrderRequest>
+
+ @Before
+ fun setup() {
+ hiltRule.inject()
+ MockitoAnnotations.initMocks(this)
+ context = InstrumentationRegistry.getInstrumentation().context
+ useCase = UpdatePriorityListUseCase(manager, Dispatchers.Main)
+ }
+
+ @Test
+ fun invoke_updatePriorityList_callsHealthManager() = runTest {
+ Mockito.doAnswer(prepareAnswer())
+ .`when`(manager)
+ .updateDataOriginPriorityOrder(
+ Mockito.any(UpdateDataOriginPriorityOrderRequest::class.java),
+ Mockito.any(),
+ Mockito.any())
+
+ val updatedPriorityList = listOf(TEST_APP_PACKAGE_NAME, TEST_APP_PACKAGE_NAME_2)
+
+ useCase.invoke(updatedPriorityList, HealthDataCategory.ACTIVITY)
+
+ Mockito.verify(manager, Mockito.times(1))
+ .updateDataOriginPriorityOrder(filtersCaptor.capture(), Mockito.any(), Mockito.any())
+ Truth.assertThat(filtersCaptor.value.dataCategory).isEqualTo(HealthDataCategory.ACTIVITY)
+ Truth.assertThat(filtersCaptor.value.dataOriginInOrder)
+ .containsExactlyElementsIn(
+ listOf(
+ getDataOrigin(TEST_APP_PACKAGE_NAME), getDataOrigin(TEST_APP_PACKAGE_NAME_2)))
+ }
+
+ private fun prepareAnswer(): (InvocationOnMock) -> Nothing? {
+ val answer = { _: InvocationOnMock -> null }
+ return answer
+ }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/prioritylist/PriorityListAdapterTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/prioritylist/PriorityListAdapterTest.kt
new file mode 100644
index 00000000..cd111ead
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/permissiontypes/prioritylist/PriorityListAdapterTest.kt
@@ -0,0 +1,63 @@
+package com.android.healthconnect.controller.tests.permissiontypes.prioritylist
+
+import com.android.healthconnect.controller.permissiontypes.HealthPermissionTypesViewModel
+import com.android.healthconnect.controller.permissiontypes.prioritylist.PriorityListAdapter
+import com.android.healthconnect.controller.shared.app.AppInfoReader
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME_2
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME_3
+import com.google.common.truth.Truth
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.mock
+
+@ExperimentalCoroutinesApi
+@HiltAndroidTest
+class PriorityListAdapterTest {
+ @get:Rule val hiltRule = HiltAndroidRule(this)
+
+ private val viewModel = mock(HealthPermissionTypesViewModel::class.java)
+ private lateinit var priorityListAdapter: PriorityListAdapter
+
+ @Inject lateinit var appInfoReader: AppInfoReader
+
+ @Before
+ fun setup() {
+ hiltRule.inject()
+ }
+
+ @Test
+ fun getPackageNameList_packagesAreReturnedCorrectly() = runTest {
+ priorityListAdapter =
+ PriorityListAdapter(
+ listOf(
+ appInfoReader.getAppMetadata(TEST_APP_PACKAGE_NAME),
+ appInfoReader.getAppMetadata(TEST_APP_PACKAGE_NAME_2),
+ appInfoReader.getAppMetadata(TEST_APP_PACKAGE_NAME_3)),
+ viewModel)
+ Truth.assertThat(priorityListAdapter.getPackageNameList())
+ .containsExactly(
+ TEST_APP_PACKAGE_NAME, TEST_APP_PACKAGE_NAME_2, TEST_APP_PACKAGE_NAME_3)
+ }
+
+ @Test
+ fun onItemMove_itemsAreReArrangedCorrectly() = runTest {
+ priorityListAdapter =
+ PriorityListAdapter(
+ listOf(
+ appInfoReader.getAppMetadata(TEST_APP_PACKAGE_NAME),
+ appInfoReader.getAppMetadata(TEST_APP_PACKAGE_NAME_2),
+ appInfoReader.getAppMetadata(TEST_APP_PACKAGE_NAME_3)),
+ viewModel)
+ priorityListAdapter.onItemMove(2, 1)
+ Truth.assertThat(priorityListAdapter.getPackageNameList())
+ .containsExactly(
+ TEST_APP_PACKAGE_NAME, TEST_APP_PACKAGE_NAME_3, TEST_APP_PACKAGE_NAME_2)
+ }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/selectabledeletion/DeletionTypeTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/selectabledeletion/DeletionTypeTest.kt
new file mode 100644
index 00000000..47001787
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/selectabledeletion/DeletionTypeTest.kt
@@ -0,0 +1,101 @@
+package com.android.healthconnect.controller.tests.selectabledeletion
+
+import android.os.Parcel
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType
+import com.android.healthconnect.controller.selectabledeletion.DeletionType
+import com.android.healthconnect.controller.shared.DataType
+import com.android.healthconnect.controller.tests.utils.TEST_APP_NAME
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME
+import junit.framework.Assert.assertTrue
+import org.junit.Test
+
+class DeletionTypeTest {
+
+ @Test
+ fun deletionTypeHealthPermissionTypeData_isParcelable() {
+ val deletionType =
+ DeletionType.DeletionTypeHealthPermissionTypes(
+ listOf(
+ HealthPermissionType.ACTIVE_CALORIES_BURNED,
+ HealthPermissionType.BLOOD_GLUCOSE))
+
+ val parcel = Parcel.obtain()
+ deletionType.writeToParcel(parcel, 0)
+ parcel.setDataPosition(0)
+
+ val recreatedDeletionType =
+ DeletionType.DeletionTypeHealthPermissionTypes.CREATOR.createFromParcel(parcel)
+
+ assertTrue(
+ recreatedDeletionType.healthPermissionTypes == deletionType.healthPermissionTypes)
+ assertTrue(recreatedDeletionType.hasPermissionTypes)
+ assertTrue(!recreatedDeletionType.hasAppData)
+ assertTrue(!recreatedDeletionType.hasEntryIds)
+ }
+
+ @Test
+ fun deletionTypeAppData_isParcelable() {
+ val deletionType =
+ DeletionType.DeletionTypeAppData(
+ packageName = TEST_APP_PACKAGE_NAME, appName = TEST_APP_NAME)
+
+ val parcel = Parcel.obtain()
+ deletionType.writeToParcel(parcel, 0)
+ parcel.setDataPosition(0)
+
+ val recreatedDeletionType =
+ DeletionType.DeletionTypeAppData.CREATOR.createFromParcel(parcel)
+
+ assertTrue(recreatedDeletionType.appName == deletionType.appName)
+ assertTrue(recreatedDeletionType.packageName == deletionType.packageName)
+ assertTrue(!recreatedDeletionType.hasPermissionTypes)
+ assertTrue(recreatedDeletionType.hasAppData)
+ assertTrue(!recreatedDeletionType.hasEntryIds)
+ }
+
+ @Test
+ fun deletionTypeEntries_isParcelable() {
+ val deletionType =
+ DeletionType.DeletionTypeEntries(
+ listOf("dataEntryId1", "dataEntryId2"), DataType.ACTIVE_CALORIES_BURNED)
+
+ val parcel = Parcel.obtain()
+ deletionType.writeToParcel(parcel, 0)
+ parcel.setDataPosition(0)
+
+ val recreatedDeletionType =
+ DeletionType.DeletionTypeEntries.CREATOR.createFromParcel(parcel)
+
+ assertTrue(recreatedDeletionType.dataType == deletionType.dataType)
+ assertTrue(recreatedDeletionType.ids == deletionType.ids)
+ assertTrue(!recreatedDeletionType.hasPermissionTypes)
+ assertTrue(!recreatedDeletionType.hasAppData)
+ assertTrue(recreatedDeletionType.hasEntryIds)
+ }
+
+ @Test
+ fun deletionTypeHealthPermissionTypesFromApp_isParcelable() {
+ val deletionType =
+ DeletionType.DeletionTypeHealthPermissionTypesFromApp(
+ listOf(
+ HealthPermissionType.ACTIVE_CALORIES_BURNED,
+ HealthPermissionType.BLOOD_GLUCOSE),
+ packageName = TEST_APP_PACKAGE_NAME,
+ appName = TEST_APP_NAME)
+
+ val parcel = Parcel.obtain()
+ deletionType.writeToParcel(parcel, 0)
+ parcel.setDataPosition(0)
+
+ val recreatedDeletionType =
+ DeletionType.DeletionTypeHealthPermissionTypesFromApp.CREATOR.createFromParcel(parcel)
+
+ assertTrue(recreatedDeletionType.appName == deletionType.appName)
+ assertTrue(recreatedDeletionType.packageName == deletionType.packageName)
+ assertTrue(
+ recreatedDeletionType.healthPermissionTypes == deletionType.healthPermissionTypes)
+ assertTrue(recreatedDeletionType.hasPermissionTypes)
+ assertTrue(recreatedDeletionType.hasAppData)
+ assertTrue(!deletionType.hasEntryIds)
+ }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/utils/ApiExtensions.kt b/apk/tests/src/com/android/healthconnect/controller/tests/utils/ApiExtensions.kt
new file mode 100644
index 00000000..3f60e2d6
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/utils/ApiExtensions.kt
@@ -0,0 +1,22 @@
+package com.android.healthconnect.controller.tests.utils
+
+import android.health.connect.ReadRecordsRequestUsingFilters
+import android.health.connect.TimeInstantRangeFilter
+import android.health.connect.datatypes.Record
+
+fun ReadRecordsRequestUsingFilters<Record>.fromDataSource(packageName: String): Boolean {
+ return this.dataOrigins.any { dataOrigin -> dataOrigin.packageName == packageName }
+}
+
+fun ReadRecordsRequestUsingFilters<Record>.fromTimeRange(
+ sourceTimeFilter: TimeInstantRangeFilter
+): Boolean {
+ val thisTimeRangeFilter = this.timeRangeFilter
+ if (thisTimeRangeFilter !is TimeInstantRangeFilter) return false
+ return thisTimeRangeFilter.startTime == sourceTimeFilter.startTime &&
+ thisTimeRangeFilter.endTime == sourceTimeFilter.endTime
+}
+
+fun ReadRecordsRequestUsingFilters<Record>.forDataType(dataType: Class<out Record>): Boolean {
+ return this.recordType == dataType
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/utils/AppStoreUtilTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/utils/AppStoreUtilTest.kt
new file mode 100644
index 00000000..d77d924b
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/utils/AppStoreUtilTest.kt
@@ -0,0 +1,44 @@
+package com.android.healthconnect.controller.tests.utils
+
+import android.content.Context
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.healthconnect.controller.utils.AppStoreUtils
+import com.android.healthconnect.controller.utils.DeviceInfoUtils
+import com.google.common.truth.Truth.assertThat
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import javax.inject.Inject
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@HiltAndroidTest
+class AppStoreUtilTest {
+
+ @get:Rule val hiltRule = HiltAndroidRule(this)
+
+ @Inject lateinit var appStoreUtils: AppStoreUtils
+ @Inject lateinit var deviceInfoUtils: DeviceInfoUtils
+ private lateinit var context: Context
+
+ @Before
+ fun setup() {
+ context = InstrumentationRegistry.getInstrumentation().context
+ hiltRule.inject()
+ }
+
+ @Test
+ fun getAppStoreLink_validPackage_returnsCorrectIntent() {
+ // skip the test on AOSP devices
+ if (!deviceInfoUtils.isPlayStoreAvailable(context)) {
+ return
+ }
+
+ val intent = appStoreUtils.getAppStoreLink(TEST_APP_PACKAGE_NAME)
+
+ assertThat(intent).isNotNull()
+ assertThat(intent!!.action).isEqualTo("android.intent.action.SHOW_APP_INFO")
+ assertThat(intent.extras?.get("android.intent.extra.PACKAGE_NAME"))
+ .isEqualTo(TEST_APP_PACKAGE_NAME)
+ }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/utils/LocalDateTimeFormatterTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/utils/LocalDateTimeFormatterTest.kt
new file mode 100644
index 00000000..bcb66f32
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/utils/LocalDateTimeFormatterTest.kt
@@ -0,0 +1,165 @@
+package com.android.healthconnect.controller.tests.utils
+
+import android.content.Context
+import androidx.test.platform.app.InstrumentationRegistry.*
+import com.android.healthconnect.controller.utils.LocalDateTimeFormatter
+import com.google.common.truth.Truth.assertThat
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import java.time.Instant
+import java.time.temporal.ChronoUnit
+import java.util.Locale
+import java.util.TimeZone
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@HiltAndroidTest
+class LocalDateTimeFormatterTest {
+
+ @get:Rule val hiltRule = HiltAndroidRule(this)
+
+ private lateinit var formatter: LocalDateTimeFormatter
+
+ private lateinit var context: Context
+
+ private var previousDefaultTimeZone: TimeZone? = null
+ private var previousLocale: Locale? = null
+
+ private val time = Instant.parse("2022-10-20T14:06:05.432Z")
+
+ @Before
+ fun setup() {
+ hiltRule.inject()
+
+ context = getInstrumentation().context
+ previousDefaultTimeZone = TimeZone.getDefault()
+ previousLocale = context.resources.configuration.locale
+
+ // set default local
+ context.setLocale(Locale.UK)
+
+ // set time zone
+ TimeZone.setDefault(TimeZone.getTimeZone("UTC"))
+ formatter = LocalDateTimeFormatter(context)
+ }
+
+ fun tearDown() {
+ TimeZone.setDefault(previousDefaultTimeZone)
+ previousLocale?.let { locale -> context.setLocale(locale) }
+ }
+
+ @Test
+ fun formatTime_ukLocale() {
+ context.setLocale(Locale.UK)
+ assertThat(formatter.formatTime(time)).isEqualTo("14:06")
+ }
+
+ @Test
+ fun formatTime_usLocale() {
+ context.setLocale(Locale.US)
+ assertThat(formatter.formatTime(time)).isEqualTo("2:06 PM")
+ }
+
+ @Test
+ fun formatLongDate_ukLocale() {
+ context.setLocale(Locale.UK)
+ assertThat(formatter.formatLongDate(time)).isEqualTo("20 October 2022")
+ }
+
+ @Test
+ fun formatLongDate_usLocale() {
+ context.setLocale(Locale.US)
+ assertThat(formatter.formatLongDate(time)).isEqualTo("October 20, 2022")
+ }
+
+ @Test
+ fun formatShortDate_ukLocale() {
+ context.setLocale(Locale.UK)
+ assertThat(formatter.formatShortDate(time)).isEqualTo("20 October")
+ }
+
+ @Test
+ fun formatShortDate_usLocale() {
+ context.setLocale(Locale.US)
+ assertThat(formatter.formatShortDate(time)).isEqualTo("October 20")
+ }
+
+ @Test
+ fun formatTimeRange_ukLocale() {
+ context.setLocale(Locale.UK)
+ val end = time.plus(1, ChronoUnit.HOURS)
+ assertThat(formatter.formatTimeRange(time, end)).isEqualTo("14:06 - 15:06")
+ }
+
+ @Test
+ fun formatTimeRange_usLocale() {
+ context.setLocale(Locale.US)
+ val end = time.plus(1, ChronoUnit.HOURS)
+ assertThat(formatter.formatTimeRange(time, end)).isEqualTo("2:06 PM - 3:06 PM")
+ }
+
+ @Test
+ fun formatTimeRangeA11y_ukLocale() {
+ context.setLocale(Locale.UK)
+ val end = time.plus(1, ChronoUnit.HOURS)
+ assertThat(formatter.formatTimeRangeA11y(time, end)).isEqualTo("from 14:06 to 15:06")
+ }
+
+ @Test
+ fun formatTimeRangeA11y_usLocale() {
+ context.setLocale(Locale.US)
+ val end = time.plus(1, ChronoUnit.HOURS)
+ assertThat(formatter.formatTimeRangeA11y(time, end)).isEqualTo("from 2:06 PM to 3:06 PM")
+ }
+
+ @Test
+ fun formatDateRangeWithYear_ukLocale() {
+ context.setLocale(Locale.UK)
+ val end = time.plus(10, ChronoUnit.DAYS)
+ assertThat(formatter.formatDateRangeWithYear(time, end)).isEqualTo("20–30 Oct 2022")
+ }
+
+ @Test
+ fun formatDateRangeWithoutYear_ukLocale() {
+ context.setLocale(Locale.UK)
+ val end = time.plus(10, ChronoUnit.DAYS)
+ assertThat(formatter.formatDateRangeWithoutYear(time, end)).isEqualTo("20–30 Oct")
+ }
+
+ @Test
+ fun formatMonthWithYear_ukLocale() {
+ context.setLocale(Locale.UK)
+ assertThat(formatter.formatMonthWithYear(time)).isEqualTo("October 2022")
+ }
+
+ @Test
+ fun formatMonthWithoutYear_ukLocale() {
+ context.setLocale(Locale.UK)
+ assertThat(formatter.formatMonthWithoutYear(time)).isEqualTo("October")
+ }
+
+ @Test
+ fun formatWeekdayDateWithYear_ukLocale() {
+ context.setLocale(Locale.UK)
+ assertThat(formatter.formatWeekdayDateWithYear(time)).isEqualTo("Thu, 20 Oct 2022")
+ }
+
+ @Test
+ fun formatWeekdayDateWithYear_usLocale() {
+ context.setLocale(Locale.US)
+ assertThat(formatter.formatWeekdayDateWithYear(time)).isEqualTo("Thu, Oct 20, 2022")
+ }
+
+ @Test
+ fun formatWeekdayDateWithoutYear_ukLocale() {
+ context.setLocale(Locale.UK)
+ assertThat(formatter.formatWeekdayDateWithoutYear(time)).isEqualTo("Thu, 20 Oct")
+ }
+
+ @Test
+ fun formatWeekdayDateWithoutYear_usLocale() {
+ context.setLocale(Locale.US)
+ assertThat(formatter.formatWeekdayDateWithoutYear(time)).isEqualTo("Thu, Oct 20")
+ }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/utils/TestConstants.kt b/apk/tests/src/com/android/healthconnect/controller/tests/utils/TestConstants.kt
index 5c9532d6..7ee2fa60 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/utils/TestConstants.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/utils/TestConstants.kt
@@ -18,13 +18,26 @@ package com.android.healthconnect.controller.tests.utils
import android.health.connect.datatypes.BasalMetabolicRateRecord
import android.health.connect.datatypes.DataOrigin
import android.health.connect.datatypes.Device
+import android.health.connect.datatypes.DistanceRecord
import android.health.connect.datatypes.HeartRateRecord
import android.health.connect.datatypes.Metadata
+import android.health.connect.datatypes.Record
+import android.health.connect.datatypes.SleepSessionRecord
import android.health.connect.datatypes.StepsRecord
+import android.health.connect.datatypes.TotalCaloriesBurnedRecord
+import android.health.connect.datatypes.units.Energy
+import android.health.connect.datatypes.units.Length
import android.health.connect.datatypes.units.Power
import com.android.healthconnect.controller.dataentries.units.PowerConverter
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType
import com.android.healthconnect.controller.shared.app.AppMetadata
+import com.android.healthconnect.controller.utils.randomInstant
+import com.android.healthconnect.controller.utils.toInstant
+import com.android.healthconnect.controller.utils.toLocalDateTime
+import com.google.common.truth.Truth.assertThat
import java.time.Instant
+import java.time.LocalDate
+import kotlin.random.Random
val NOW: Instant = Instant.parse("2022-10-20T07:06:05.432Z")
val MIDNIGHT: Instant = Instant.parse("2022-10-20T00:00:00.000Z")
@@ -47,6 +60,40 @@ fun getBasalMetabolicRateRecord(calories: Long): BasalMetabolicRateRecord {
return BasalMetabolicRateRecord.Builder(getMetaData(), NOW, Power.fromWatts(watts)).build()
}
+fun getDistanceRecord(distance: Length, time: Instant = NOW): DistanceRecord {
+ return DistanceRecord.Builder(getMetaData(), time, time.plusSeconds(2), distance).build()
+}
+
+fun getTotalCaloriesBurnedRecord(calories: Energy, time: Instant = NOW): TotalCaloriesBurnedRecord {
+ return TotalCaloriesBurnedRecord.Builder(getMetaData(), time, time.plusSeconds(2), calories)
+ .build()
+}
+
+fun getSleepSessionRecord(startTime: Instant = NOW): SleepSessionRecord {
+ val endTime = startTime.toLocalDateTime().plusHours(8).toInstant()
+ return SleepSessionRecord.Builder(getMetaData(), startTime, endTime).build()
+}
+
+fun getSleepSessionRecord(startTime: Instant, endTime: Instant): SleepSessionRecord {
+ return SleepSessionRecord.Builder(getMetaData(), startTime, endTime).build()
+}
+
+fun getRandomRecord(healthPermissionType: HealthPermissionType, date: LocalDate): Record {
+ return when (healthPermissionType) {
+ HealthPermissionType.STEPS -> getStepsRecord(Random.nextLong(0, 5000), date.randomInstant())
+ HealthPermissionType.DISTANCE ->
+ getDistanceRecord(
+ Length.fromMeters(Random.nextDouble(0.0, 5000.0)), date.randomInstant())
+ HealthPermissionType.TOTAL_CALORIES_BURNED ->
+ getTotalCaloriesBurnedRecord(
+ Energy.fromCalories(Random.nextDouble(1500.0, 5000.0)), date.randomInstant())
+ HealthPermissionType.SLEEP -> getSleepSessionRecord(date.randomInstant())
+ else ->
+ throw IllegalArgumentException(
+ "HealthPermissionType $healthPermissionType not supported")
+ }
+}
+
fun getMetaData(): Metadata {
return getMetaData(TEST_APP_PACKAGE_NAME)
}
@@ -66,6 +113,30 @@ fun getMetaData(packageName: String): Metadata {
fun getDataOrigin(packageName: String): DataOrigin =
DataOrigin.Builder().setPackageName(packageName).build()
+fun getSleepSessionRecords(inputDates: List<Pair<Instant, Instant>>): List<SleepSessionRecord> {
+ val result = arrayListOf<SleepSessionRecord>()
+ inputDates.forEach { (startTime, endTime) ->
+ result.add(SleepSessionRecord.Builder(getMetaData(), startTime, endTime).build())
+ }
+
+ return result
+}
+
+fun verifySleepSessionListsEqual(actual: List<Record>, expected: List<SleepSessionRecord>) {
+ assertThat(actual.size).isEqualTo(expected.size)
+ for ((index, element) in actual.withIndex()) {
+ assertThat(element is SleepSessionRecord).isTrue()
+ val expectedElement = expected[index]
+ val actualElement = element as SleepSessionRecord
+
+ assertThat(actualElement.startTime).isEqualTo(expectedElement.startTime)
+ assertThat(actualElement.endTime).isEqualTo(expectedElement.endTime)
+ assertThat(actualElement.notes).isEqualTo(expectedElement.notes)
+ assertThat(actualElement.title).isEqualTo(expectedElement.title)
+ assertThat(actualElement.stages).isEqualTo(expectedElement.stages)
+ }
+}
+
// region apps
const val TEST_APP_PACKAGE_NAME = "android.healthconnect.controller.test.app"
@@ -81,6 +152,6 @@ val TEST_APP =
val TEST_APP_2 =
AppMetadata(packageName = TEST_APP_PACKAGE_NAME_2, appName = TEST_APP_NAME_2, icon = null)
val TEST_APP_3 =
- AppMetadata(packageName = TEST_APP_PACKAGE_NAME_2, appName = TEST_APP_NAME_3, icon = null)
+ AppMetadata(packageName = TEST_APP_PACKAGE_NAME_3, appName = TEST_APP_NAME_3, icon = null)
// endregion
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/utils/TestTimeSource.kt b/apk/tests/src/com/android/healthconnect/controller/tests/utils/TestTimeSource.kt
index c5ded855..ee547d2b 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/utils/TestTimeSource.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/utils/TestTimeSource.kt
@@ -3,9 +3,11 @@
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
+ *
* ```
* http://www.apache.org/licenses/LICENSE-2.0
* ```
+ *
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
@@ -27,18 +29,26 @@ import javax.inject.Singleton
/** Time source for testing purposes. */
object TestTimeSource : TimeSource {
- override fun currentTimeMillis(): Long = NOW.toEpochMilli()
+ private var localNow: Instant = NOW
+
+ override fun currentTimeMillis(): Long = localNow.toEpochMilli()
override fun deviceZoneOffset(): ZoneId = UTC
override fun currentLocalDateTime(): LocalDateTime =
Instant.ofEpochMilli(currentTimeMillis()).atZone(deviceZoneOffset()).toLocalDateTime()
+
+ fun setNow(instant: Instant) {
+ localNow = instant
+ }
+
+ fun reset() {
+ localNow = NOW
+ }
}
@Module
@TestInstallIn(components = [SingletonComponent::class], replaces = [SystemTimeSourceModule::class])
object TestTimeSourceModule {
- @Provides
- @Singleton
- fun providesTestTimeSource() : TimeSource = TestTimeSource
-} \ No newline at end of file
+ @Provides @Singleton fun providesTestTimeSource(): TimeSource = TestTimeSource
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/utils/TimeExtensionsTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/utils/TimeExtensionsTest.kt
index b64f3ad0..02692c7f 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/utils/TimeExtensionsTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/utils/TimeExtensionsTest.kt
@@ -21,13 +21,16 @@ import com.android.healthconnect.controller.utils.isAtLeastOneDayAfter
import com.android.healthconnect.controller.utils.isOnDayAfter
import com.android.healthconnect.controller.utils.isOnDayBefore
import com.android.healthconnect.controller.utils.isOnSameDay
+import com.android.healthconnect.controller.utils.randomInstant
import com.android.healthconnect.controller.utils.toInstant
import com.android.healthconnect.controller.utils.toInstantAtStartOfDay
import com.android.healthconnect.controller.utils.toLocalDate
+import com.android.healthconnect.controller.utils.toLocalDateTime
import com.android.healthconnect.controller.utils.toLocalTime
import com.google.common.truth.Truth.assertThat
import java.time.Instant
import java.time.LocalDate
+import java.time.LocalDateTime
import java.time.LocalTime
import java.time.ZoneId
import java.util.TimeZone
@@ -182,4 +185,36 @@ class TimeExtensionsTest {
val expectedInstant = Instant.parse("2021-10-01T03:00:00Z")
assertThat(testLocalDate.toInstantAtStartOfDay()).isEqualTo(expectedInstant)
}
+
+ @Test
+ fun instantToLocalDateTime_returnsLocalizedDateTime() {
+ // UTC + 8
+ TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("Australia/Perth")))
+
+ val testInstant = Instant.parse("2023-05-18T20:00:00Z")
+ val expectedLocalDateTime = LocalDateTime.of(2023, 5, 19, 4, 0)
+
+ assertThat(testInstant.toLocalDateTime()).isEqualTo(expectedLocalDateTime)
+ }
+
+ @Test
+ fun localDateRandomInstant_returnsInstantOnTheLocalDate() {
+ // UTC + 5:30
+ TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("Asia/Kolkata")))
+
+ val localDate = LocalDate.of(2023, 7, 18)
+ val randomInstant = localDate.randomInstant()
+
+ assertThat(randomInstant.isOnSameDay(localDate.toInstantAtStartOfDay())).isTrue()
+ }
+
+ @Test
+ fun localDateTimeToInstant_returnsCorrectInstant() {
+ // UTC - 3
+ TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("America/Sao_Paulo")))
+
+ val testLocalDateTime = LocalDateTime.of(2021, 10, 1, 18, 0)
+ val expectedInstant = Instant.parse("2021-10-01T21:00:00Z")
+ assertThat(testLocalDateTime.toInstant()).isEqualTo(expectedInstant)
+ }
}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/utils/di/FakeComponents.kt b/apk/tests/src/com/android/healthconnect/controller/tests/utils/di/FakeComponents.kt
index a7187858..d0887908 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/utils/di/FakeComponents.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/utils/di/FakeComponents.kt
@@ -18,26 +18,33 @@ package com.android.healthconnect.controller.tests.utils.di
import android.health.connect.HealthDataCategory
import android.health.connect.accesslog.AccessLog
import android.health.connect.datatypes.Record
+import com.android.healthconnect.controller.data.access.AppAccessState
+import com.android.healthconnect.controller.data.access.ILoadAccessUseCase
+import com.android.healthconnect.controller.data.access.ILoadPermissionTypeContributorAppsUseCase
import com.android.healthconnect.controller.data.entries.FormattedEntry
import com.android.healthconnect.controller.data.entries.api.ILoadDataAggregationsUseCase
import com.android.healthconnect.controller.data.entries.api.ILoadDataEntriesUseCase
import com.android.healthconnect.controller.data.entries.api.ILoadMenstruationDataUseCase
-import com.android.healthconnect.controller.data.entries.api.ILoadSleepDataUseCase
import com.android.healthconnect.controller.data.entries.api.LoadAggregationInput
import com.android.healthconnect.controller.data.entries.api.LoadDataEntriesInput
import com.android.healthconnect.controller.data.entries.api.LoadMenstruationDataInput
import com.android.healthconnect.controller.datasources.AggregationCardInfo
+import com.android.healthconnect.controller.datasources.api.ILoadLastDateWithPriorityDataUseCase
import com.android.healthconnect.controller.datasources.api.ILoadMostRecentAggregationsUseCase
import com.android.healthconnect.controller.datasources.api.ILoadPotentialPriorityListUseCase
+import com.android.healthconnect.controller.datasources.api.ILoadPriorityEntriesUseCase
+import com.android.healthconnect.controller.datasources.api.ISleepSessionHelper
import com.android.healthconnect.controller.datasources.api.IUpdatePriorityListUseCase
+import com.android.healthconnect.controller.permissions.api.IGetGrantedHealthPermissionsUseCase
import com.android.healthconnect.controller.permissions.connectedapps.ILoadHealthPermissionApps
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType
import com.android.healthconnect.controller.permissiontypes.api.ILoadPriorityListUseCase
import com.android.healthconnect.controller.recentaccess.ILoadRecentAccessUseCase
import com.android.healthconnect.controller.shared.HealthDataCategoryInt
import com.android.healthconnect.controller.shared.app.AppMetadata
import com.android.healthconnect.controller.shared.app.ConnectedAppMetadata
import com.android.healthconnect.controller.shared.usecase.UseCaseResults
-import com.android.healthconnect.controller.utils.toLocalDate
+import java.time.Instant
import java.time.LocalDate
class FakeRecentAccessUseCase : ILoadRecentAccessUseCase {
@@ -65,18 +72,18 @@ class FakeHealthPermissionAppsUseCase : ILoadHealthPermissionApps {
}
class FakeLoadDataEntriesUseCase : ILoadDataEntriesUseCase {
- private var list: List<FormattedEntry> = emptyList()
+ private var formattedList = listOf<FormattedEntry>()
fun updateList(list: List<FormattedEntry>) {
- this.list = list
+ formattedList = list
}
override suspend fun invoke(input: LoadDataEntriesInput): UseCaseResults<List<FormattedEntry>> {
- return UseCaseResults.Success(list)
+ return UseCaseResults.Success(formattedList)
}
override suspend fun execute(input: LoadDataEntriesInput): List<FormattedEntry> {
- return list
+ return formattedList
}
}
@@ -103,8 +110,9 @@ class FakeLoadDataAggregationsUseCase : ILoadDataAggregationsUseCase {
FormattedEntry.FormattedAggregation("100 steps", "100 steps", "Test App")
private var aggregations: List<FormattedEntry.FormattedAggregation> = listOf(aggregation)
- private var invocationCount = 0
- private var shouldReturnFailed = false
+ var invocationCount = 0
+ private var forceFail = false
+ private var exceptionMessage = ""
fun updateAggregation(aggregation: FormattedEntry.FormattedAggregation) {
this.aggregations = listOf(aggregation)
@@ -115,8 +123,9 @@ class FakeLoadDataAggregationsUseCase : ILoadDataAggregationsUseCase {
this.aggregations = aggregations
}
- fun updateErrorResponse() {
- this.shouldReturnFailed = true
+ fun setFailure(exceptionMessage: String) {
+ forceFail = true
+ this.exceptionMessage = exceptionMessage
}
override suspend fun invoke(
@@ -127,8 +136,8 @@ class FakeLoadDataAggregationsUseCase : ILoadDataAggregationsUseCase {
IllegalStateException(
"AggregationResponsesSize = ${this.aggregations.size}, " +
"invocationCount = $invocationCount. Please update aggregation responses before invoking."))
- } else if (shouldReturnFailed) {
- UseCaseResults.Failed(IllegalStateException("Custom failure"))
+ } else if (forceFail) {
+ UseCaseResults.Failed(IllegalStateException(exceptionMessage))
} else {
val result = UseCaseResults.Success(aggregations[invocationCount])
invocationCount += 1
@@ -143,7 +152,8 @@ class FakeLoadDataAggregationsUseCase : ILoadDataAggregationsUseCase {
fun reset() {
this.invocationCount = 0
this.aggregations = listOf(aggregation)
- this.shouldReturnFailed = false
+ exceptionMessage = ""
+ forceFail = false
}
}
@@ -166,6 +176,66 @@ class FakeLoadMostRecentAggregationsUseCase : ILoadMostRecentAggregationsUseCase
}
}
+class FakeSleepSessionHelper : ISleepSessionHelper {
+
+ private var forceFail = false
+ private var exceptionMessage = ""
+ private var datePair = Pair(Instant.EPOCH, Instant.EPOCH)
+
+ fun setDatePair(minDate: Instant, maxDate: Instant) {
+ datePair = Pair(minDate, maxDate)
+ }
+
+ fun setFailure(exceptionMessage: String) {
+ forceFail = true
+ this.exceptionMessage = exceptionMessage
+ }
+
+ override suspend fun clusterSleepSessions(
+ lastDateWithData: LocalDate
+ ): UseCaseResults<Pair<Instant, Instant>> {
+ return if (forceFail) UseCaseResults.Failed(Exception(this.exceptionMessage))
+ else UseCaseResults.Success(datePair)
+ }
+
+ fun reset() {
+ datePair = Pair(Instant.EPOCH, Instant.EPOCH)
+ exceptionMessage = ""
+ forceFail = false
+ }
+}
+
+class FakeLoadPriorityEntriesUseCase : ILoadPriorityEntriesUseCase {
+
+ private var priorityEntries = mutableMapOf<LocalDate, List<Record>>()
+ private var forceFail = false
+ private var exceptionMessage = ""
+
+ override suspend fun invoke(
+ healthPermissionType: HealthPermissionType,
+ localDate: LocalDate
+ ): UseCaseResults<List<Record>> {
+ return if (forceFail) UseCaseResults.Failed(Exception(this.exceptionMessage))
+ else UseCaseResults.Success(priorityEntries.getOrDefault(localDate, listOf()))
+ }
+
+ fun setEntriesList(localDate: LocalDate, list: List<Record>) {
+
+ priorityEntries[localDate] = list
+ }
+
+ fun setFailure(exceptionMessage: String) {
+ forceFail = true
+ this.exceptionMessage = exceptionMessage
+ }
+
+ fun reset() {
+ priorityEntries.clear()
+ exceptionMessage = ""
+ forceFail = false
+ }
+}
+
class FakeLoadPotentialPriorityListUseCase : ILoadPotentialPriorityListUseCase {
private var potentialPriorityList = listOf<AppMetadata>()
@@ -188,11 +258,14 @@ class FakeLoadPotentialPriorityListUseCase : ILoadPotentialPriorityListUseCase {
class FakeLoadPriorityListUseCase : ILoadPriorityListUseCase {
private var priorityList = listOf<AppMetadata>()
+ private var forceFail = false
+ private var exceptionMessage = ""
override suspend fun invoke(
input: @HealthDataCategoryInt Int
): UseCaseResults<List<AppMetadata>> {
- return UseCaseResults.Success(priorityList)
+ return if (forceFail) UseCaseResults.Failed(Exception(this.exceptionMessage))
+ else UseCaseResults.Success(priorityList)
}
override suspend fun execute(input: Int): List<AppMetadata> {
@@ -203,8 +276,15 @@ class FakeLoadPriorityListUseCase : ILoadPriorityListUseCase {
this.priorityList = priorityList
}
+ fun setFailure(exceptionMessage: String) {
+ forceFail = true
+ this.exceptionMessage = exceptionMessage
+ }
+
fun reset() {
this.priorityList = listOf()
+ exceptionMessage = ""
+ forceFail = false
}
}
@@ -224,24 +304,89 @@ class FakeUpdatePriorityListUseCase : IUpdatePriorityListUseCase {
}
}
-class FakeLoadSleepDataUseCase : ILoadSleepDataUseCase {
+class FakeLoadAccessUseCase : ILoadAccessUseCase {
- private var sleepDataMap: MutableMap<LocalDate, List<Record>> = mutableMapOf()
+ private var appDataMap: Map<AppAccessState, List<AppMetadata>> = mutableMapOf()
- fun updateSleepData(date: LocalDate, recordsList: List<Record>) {
- sleepDataMap[date] = recordsList
+ override suspend fun invoke(
+ permissionType: HealthPermissionType
+ ): UseCaseResults<Map<AppAccessState, List<AppMetadata>>> {
+ return UseCaseResults.Success(appDataMap)
+ }
+
+ fun updateMap(map: Map<AppAccessState, List<AppMetadata>>) {
+ appDataMap = map
+ }
+
+ fun reset() {
+ this.appDataMap = mutableMapOf()
+ }
+}
+
+class FakeLoadPermissionTypeContributorAppsUseCase : ILoadPermissionTypeContributorAppsUseCase {
+
+ private var contributorApps: List<AppMetadata> = listOf()
+
+ override suspend fun invoke(permissionType: HealthPermissionType): List<AppMetadata> {
+ return contributorApps
+ }
+
+ fun updateList(list: List<AppMetadata>) {
+ contributorApps = list
+ }
+
+ fun reset() {
+ this.contributorApps = listOf()
}
+}
+
+class FakeGetGrantedHealthPermissionsUseCase : IGetGrantedHealthPermissionsUseCase {
+
+ private var permissionsPerApp: MutableMap<String, List<String>> = mutableMapOf()
+
+ override fun invoke(packageName: String): List<String> {
+ return permissionsPerApp.getOrDefault(packageName, listOf())
+ }
+
+ fun updateData(packageName: String, permissions: List<String>) {
+ permissionsPerApp[packageName] = permissions
+ }
+
+ fun reset() {
+ this.permissionsPerApp = mutableMapOf()
+ }
+}
+
+class FakeLoadLastDateWithPriorityDataUseCase : ILoadLastDateWithPriorityDataUseCase {
- override suspend fun invoke(input: LoadDataEntriesInput): UseCaseResults<List<Record>> {
- val result = sleepDataMap.getOrDefault(input.displayedStartTime.toLocalDate(), listOf())
- return UseCaseResults.Success(result)
+ private var lastDateWithPriorityDataMap = mutableMapOf<HealthPermissionType, LocalDate?>()
+ private var forceFail = false
+ private var exceptionMessage = ""
+
+ fun setLastDateWithPriorityDataForHealthPermissionType(
+ healthPermissionType: HealthPermissionType,
+ localDate: LocalDate?
+ ) {
+ lastDateWithPriorityDataMap[healthPermissionType] = localDate
+ }
+
+ fun setFailure(exceptionMessage: String) {
+ forceFail = true
+ this.exceptionMessage = exceptionMessage
}
- override suspend fun execute(input: LoadDataEntriesInput): List<Record> {
- return sleepDataMap.getOrDefault(input.displayedStartTime.toLocalDate(), listOf())
+ override suspend fun invoke(
+ healthPermissionType: HealthPermissionType
+ ): UseCaseResults<LocalDate?> {
+ if (forceFail) return UseCaseResults.Failed(Exception(this.exceptionMessage))
+ return if (lastDateWithPriorityDataMap.containsKey(healthPermissionType))
+ UseCaseResults.Success(lastDateWithPriorityDataMap[healthPermissionType])
+ else UseCaseResults.Success(null)
}
fun reset() {
- this.sleepDataMap = mutableMapOf()
+ lastDateWithPriorityDataMap.clear()
+ exceptionMessage = ""
+ forceFail = false
}
}
diff --git a/framework/java/android/health/connect/AggregateRecordsResponse.java b/framework/java/android/health/connect/AggregateRecordsResponse.java
index 83bbc184..d4579494 100644
--- a/framework/java/android/health/connect/AggregateRecordsResponse.java
+++ b/framework/java/android/health/connect/AggregateRecordsResponse.java
@@ -131,6 +131,22 @@ public final class AggregateRecordsResponse<T> {
}
/**
+ * Returns {@link ZoneOffset} of the first {@link AggregationType}.
+ *
+ * @hide
+ */
+ @Nullable
+ public ZoneOffset getFirstZoneOffset() {
+ AggregationType<T> firstAggregationType = getFirstAggregationType();
+ return firstAggregationType != null ? getZoneOffset(firstAggregationType) : null;
+ }
+
+ @Nullable
+ private AggregationType<T> getFirstAggregationType() {
+ return mAggregateResults.keySet().stream().findFirst().orElse(null);
+ }
+
+ /**
* Returns a set of {@link DataOrigin}s for the underlying aggregation record, empty set if the
* corresponding aggregation doesn't exist and or if multiple records were present.
*/
diff --git a/framework/java/android/health/connect/TimeRangeFilterHelper.java b/framework/java/android/health/connect/TimeRangeFilterHelper.java
index 4df49f0c..17095467 100644
--- a/framework/java/android/health/connect/TimeRangeFilterHelper.java
+++ b/framework/java/android/health/connect/TimeRangeFilterHelper.java
@@ -17,6 +17,7 @@
package android.health.connect;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import java.time.Instant;
import java.time.LocalDateTime;
@@ -75,4 +76,15 @@ public final class TimeRangeFilterHelper {
public static long getMillisOfLocalTime(LocalDateTime time) {
return time.toInstant(LOCAL_TIME_ZERO_OFFSET).toEpochMilli();
}
+
+ /**
+ * Converts the provided {@link LocalDateTime} to {@link Instant} using the provided {@link
+ * ZoneOffset} if it's not null, or using the system default zone offset otherwise.
+ */
+ public static Instant getInstantFromLocalTime(
+ @NonNull LocalDateTime time, @Nullable ZoneOffset zoneOffset) {
+ return zoneOffset != null
+ ? time.toInstant(zoneOffset)
+ : time.toInstant(ZoneOffset.systemDefault().getRules().getOffset(time));
+ }
}
diff --git a/framework/java/android/health/connect/aidl/AggregateDataResponseParcel.java b/framework/java/android/health/connect/aidl/AggregateDataResponseParcel.java
index 230e3ab6..5439c405 100644
--- a/framework/java/android/health/connect/aidl/AggregateDataResponseParcel.java
+++ b/framework/java/android/health/connect/aidl/AggregateDataResponseParcel.java
@@ -18,6 +18,7 @@ package android.health.connect.aidl;
import static android.health.connect.Constants.DEFAULT_INT;
import static android.health.connect.Constants.DEFAULT_LONG;
+import static android.health.connect.TimeRangeFilterHelper.getInstantFromLocalTime;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -159,21 +160,69 @@ public class AggregateDataResponseParcel implements Parcelable {
getAggregateDataResponseGroupedByDuration() {
Objects.requireNonNull(mDuration);
- List<AggregateRecordsGroupedByDurationResponse<?>>
- aggregateRecordsGroupedByDurationResponse = new ArrayList<>();
- long mStartTime = TimeRangeFilterHelper.getFilterStartTimeMillis(mTimeRangeFilter);
- long mEndTime = TimeRangeFilterHelper.getFilterEndTimeMillis(mTimeRangeFilter);
- long mDelta = getDurationDelta(mDuration);
- for (AggregateRecordsResponse<?> aggregateRecordsResponse : mAggregateRecordsResponses) {
- aggregateRecordsGroupedByDurationResponse.add(
+ if (mAggregateRecordsResponses.isEmpty()) {
+ return List.of();
+ }
+
+ if (mTimeRangeFilter instanceof LocalTimeRangeFilter timeFilter) {
+ return getAggregateDataResponseForLocalTimeGroupedByDuration(
+ timeFilter.getStartTime(), timeFilter.getEndTime());
+ }
+
+ if (mTimeRangeFilter instanceof TimeInstantRangeFilter timeFilter) {
+ return getAggregateDataResponseForInstantTimeGroupedByDuration(
+ timeFilter.getStartTime(), timeFilter.getEndTime());
+ }
+
+ throw new IllegalArgumentException(
+ "Invalid time filter object. Object should be either TimeInstantRangeFilter or "
+ + "LocalTimeRangeFilter.");
+ }
+
+ private List<AggregateRecordsGroupedByDurationResponse<?>>
+ getAggregateDataResponseForLocalTimeGroupedByDuration(
+ LocalDateTime startTime, LocalDateTime endTime) {
+ List<AggregateRecordsGroupedByDurationResponse<?>> responses = new ArrayList<>();
+ Duration bucketStartTimeOffset = Duration.ZERO;
+ for (AggregateRecordsResponse<?> response : mAggregateRecordsResponses) {
+ ZoneOffset zoneOffset = response.getFirstZoneOffset();
+ Instant endTimeInstant = getInstantFromLocalTime(endTime, zoneOffset);
+ Instant bucketStartTime =
+ getInstantFromLocalTime(startTime, zoneOffset).plus(bucketStartTimeOffset);
+ Instant bucketEndTime = bucketStartTime.plus(mDuration);
+ if (bucketEndTime.isAfter(endTimeInstant)) {
+ bucketEndTime = endTimeInstant;
+ }
+
+ responses.add(
new AggregateRecordsGroupedByDurationResponse<>(
- getDurationInstant(mStartTime),
- getDurationInstant(Math.min(mStartTime + mDelta, mEndTime)),
- aggregateRecordsResponse.getAggregateResults()));
- mStartTime += mDelta;
+ bucketStartTime, bucketEndTime, response.getAggregateResults()));
+ bucketStartTimeOffset = bucketStartTimeOffset.plus(mDuration);
+ }
+
+ return responses;
+ }
+
+ private List<AggregateRecordsGroupedByDurationResponse<?>>
+ getAggregateDataResponseForInstantTimeGroupedByDuration(
+ Instant startTime, Instant endTime) {
+ List<AggregateRecordsGroupedByDurationResponse<?>> responses = new ArrayList<>();
+ Duration offsetDuration = Duration.ZERO;
+ for (AggregateRecordsResponse<?> response : mAggregateRecordsResponses) {
+ Instant buckedStartTime = startTime.plus(offsetDuration);
+ Instant buckedEndTime = buckedStartTime.plus(mDuration);
+ if (buckedEndTime.isAfter(endTime)) {
+ buckedEndTime = endTime;
+ }
+
+ responses.add(
+ new AggregateRecordsGroupedByDurationResponse<>(
+ buckedStartTime, buckedEndTime, response.getAggregateResults()));
+
+ offsetDuration = offsetDuration.plus(mDuration);
}
- return aggregateRecordsGroupedByDurationResponse;
+ return responses;
}
/**
diff --git a/framework/java/android/health/connect/aidl/DeletedLogsParcel.java b/framework/java/android/health/connect/aidl/DeletedLogsParcel.java
new file mode 100644
index 00000000..38915241
--- /dev/null
+++ b/framework/java/android/health/connect/aidl/DeletedLogsParcel.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.health.connect.aidl;
+
+import android.annotation.NonNull;
+import android.health.connect.changelog.ChangeLogsResponse.DeletedLog;
+import android.health.connect.internal.ParcelUtils;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A {@link Parcelable} that reads and writes {@link DeletedLog}s.
+ *
+ * @hide
+ */
+public final class DeletedLogsParcel implements Parcelable {
+
+ @NonNull
+ public static final Creator<DeletedLogsParcel> CREATOR =
+ new Creator<>() {
+ @Override
+ public DeletedLogsParcel createFromParcel(Parcel in) {
+ return new DeletedLogsParcel(in);
+ }
+
+ @Override
+ public DeletedLogsParcel[] newArray(int size) {
+ return new DeletedLogsParcel[size];
+ }
+ };
+
+ private final List<DeletedLog> mDeletedLogs;
+
+ public DeletedLogsParcel(@NonNull List<DeletedLog> deletedLogs) {
+ mDeletedLogs = deletedLogs;
+ }
+
+ private DeletedLogsParcel(@NonNull Parcel in) {
+ in = ParcelUtils.getParcelForSharedMemoryIfRequired(in);
+ int size = in.readInt();
+ mDeletedLogs = new ArrayList<>(size);
+ for (int i = 0; i < size; i++) {
+ String id = in.readString();
+ long time = in.readLong();
+ mDeletedLogs.add(new DeletedLog(id, time));
+ }
+ }
+
+ @NonNull
+ public List<DeletedLog> getDeletedLogs() {
+ return mDeletedLogs;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ ParcelUtils.putToRequiredMemory(dest, flags, this::writeToParcelInternal);
+ }
+
+ private void writeToParcelInternal(@NonNull Parcel dest) {
+ dest.writeInt(mDeletedLogs.size());
+ for (DeletedLog deletedLog : mDeletedLogs) {
+ dest.writeString(deletedLog.getDeletedRecordId());
+ dest.writeLong(deletedLog.getDeletedTime().toEpochMilli());
+ }
+ }
+}
diff --git a/framework/java/android/health/connect/aidl/InsertRecordsResponseParcel.java b/framework/java/android/health/connect/aidl/InsertRecordsResponseParcel.java
index 19d3eae6..610f0486 100644
--- a/framework/java/android/health/connect/aidl/InsertRecordsResponseParcel.java
+++ b/framework/java/android/health/connect/aidl/InsertRecordsResponseParcel.java
@@ -18,6 +18,7 @@ package android.health.connect.aidl;
import android.annotation.NonNull;
import android.health.connect.HealthConnectManager;
+import android.health.connect.internal.ParcelUtils;
import android.os.Parcel;
import android.os.Parcelable;
@@ -39,7 +40,8 @@ public class InsertRecordsResponseParcel implements Parcelable {
mUids = uids;
}
- protected InsertRecordsResponseParcel(Parcel in) {
+ private InsertRecordsResponseParcel(Parcel in) {
+ in = ParcelUtils.getParcelForSharedMemoryIfRequired(in);
mUids = in.createStringArrayList();
}
@@ -50,7 +52,7 @@ public class InsertRecordsResponseParcel implements Parcelable {
@NonNull
public static final Creator<InsertRecordsResponseParcel> CREATOR =
- new Creator<InsertRecordsResponseParcel>() {
+ new Creator<>() {
@Override
public InsertRecordsResponseParcel createFromParcel(Parcel in) {
return new InsertRecordsResponseParcel(in);
@@ -69,6 +71,10 @@ public class InsertRecordsResponseParcel implements Parcelable {
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
+ ParcelUtils.putToRequiredMemory(dest, flags, this::writeToParcelInternal);
+ }
+
+ private void writeToParcelInternal(@NonNull Parcel dest) {
dest.writeStringList(mUids);
}
}
diff --git a/framework/java/android/health/connect/changelog/ChangeLogsResponse.java b/framework/java/android/health/connect/changelog/ChangeLogsResponse.java
index c2a6839f..7a9c0dbd 100644
--- a/framework/java/android/health/connect/changelog/ChangeLogsResponse.java
+++ b/framework/java/android/health/connect/changelog/ChangeLogsResponse.java
@@ -18,6 +18,7 @@ package android.health.connect.changelog;
import android.annotation.NonNull;
import android.health.connect.HealthConnectManager;
+import android.health.connect.aidl.DeletedLogsParcel;
import android.health.connect.aidl.RecordsParcel;
import android.health.connect.datatypes.Record;
import android.health.connect.internal.datatypes.RecordInternal;
@@ -70,14 +71,9 @@ public final class ChangeLogsResponse implements Parcelable {
RecordsParcel.class.getClassLoader(),
RecordsParcel.class)
.getRecords());
- int size = in.readInt();
- List<DeletedLog> deletedLogs = new ArrayList<>(size);
- for (int i = 0; i < size; i++) {
- String id = in.readString();
- long time = in.readLong();
- deletedLogs.add(new DeletedLog(id, time));
- }
- mDeletedLogs = deletedLogs;
+ mDeletedLogs =
+ in.readParcelable(DeletedLogsParcel.class.getClassLoader(), DeletedLogsParcel.class)
+ .getDeletedLogs();
mNextChangesToken = in.readString();
mHasMorePages = in.readBoolean();
}
@@ -142,11 +138,7 @@ public final class ChangeLogsResponse implements Parcelable {
recordInternal.add(record.toRecordInternal());
}
dest.writeParcelable(new RecordsParcel(recordInternal), 0);
- dest.writeInt(mDeletedLogs.size());
- for (DeletedLog deletedLog : mDeletedLogs) {
- dest.writeString(deletedLog.getDeletedRecordId());
- dest.writeLong(deletedLog.getDeletedTime().toEpochMilli());
- }
+ dest.writeParcelable(new DeletedLogsParcel(mDeletedLogs), 0);
dest.writeString(mNextChangesToken);
dest.writeBoolean(mHasMorePages);
}
diff --git a/framework/java/android/health/connect/datatypes/CyclingPedalingCadenceRecord.java b/framework/java/android/health/connect/datatypes/CyclingPedalingCadenceRecord.java
index 9b8e49bc..5043eeba 100644
--- a/framework/java/android/health/connect/datatypes/CyclingPedalingCadenceRecord.java
+++ b/framework/java/android/health/connect/datatypes/CyclingPedalingCadenceRecord.java
@@ -16,6 +16,7 @@
package android.health.connect.datatypes;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.health.connect.HealthConnectManager;
import android.health.connect.datatypes.validation.ValidationUtils;
import android.health.connect.internal.datatypes.CyclingPedalingCadenceRecordInternal;
@@ -169,7 +170,7 @@ public final class CyclingPedalingCadenceRecord extends IntervalRecord {
* @return {@code true} if this object is the same as the obj
*/
@Override
- public boolean equals(@NonNull Object object) {
+ public boolean equals(@Nullable Object object) {
if (super.equals(object) && object instanceof CyclingPedalingCadenceRecordSample) {
CyclingPedalingCadenceRecordSample other =
(CyclingPedalingCadenceRecordSample) object;
@@ -294,7 +295,7 @@ public final class CyclingPedalingCadenceRecord extends IntervalRecord {
* @return {@code true} if this object is the same as the obj
*/
@Override
- public boolean equals(@NonNull Object object) {
+ public boolean equals(@Nullable Object object) {
if (super.equals(object)) {
CyclingPedalingCadenceRecord other = (CyclingPedalingCadenceRecord) object;
if (getSamples().size() != other.getSamples().size()) return false;
diff --git a/framework/java/android/health/connect/datatypes/DataOrigin.java b/framework/java/android/health/connect/datatypes/DataOrigin.java
index 64db503c..34ac9581 100644
--- a/framework/java/android/health/connect/datatypes/DataOrigin.java
+++ b/framework/java/android/health/connect/datatypes/DataOrigin.java
@@ -17,6 +17,7 @@
package android.health.connect.datatypes;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import java.util.Objects;
@@ -70,7 +71,7 @@ public final class DataOrigin {
* @return {@code true} if this object is the same as the obj
*/
@Override
- public boolean equals(@NonNull Object object) {
+ public boolean equals(@Nullable Object object) {
if (this == object) return true;
if (object instanceof DataOrigin) {
DataOrigin other = (DataOrigin) object;
diff --git a/framework/java/android/health/connect/datatypes/Device.java b/framework/java/android/health/connect/datatypes/Device.java
index e813d2cb..f3e7e484 100644
--- a/framework/java/android/health/connect/datatypes/Device.java
+++ b/framework/java/android/health/connect/datatypes/Device.java
@@ -129,7 +129,7 @@ public final class Device {
* @return {@code true} if this object is the same as the obj
*/
@Override
- public boolean equals(@NonNull Object object) {
+ public boolean equals(@Nullable Object object) {
if (this == object) return true;
if (object instanceof Device) {
Device other = (Device) object;
diff --git a/framework/java/android/health/connect/datatypes/HeartRateRecord.java b/framework/java/android/health/connect/datatypes/HeartRateRecord.java
index a96b02c1..06414268 100644
--- a/framework/java/android/health/connect/datatypes/HeartRateRecord.java
+++ b/framework/java/android/health/connect/datatypes/HeartRateRecord.java
@@ -19,6 +19,7 @@ package android.health.connect.datatypes;
import static android.health.connect.datatypes.RecordTypeIdentifier.RECORD_TYPE_HEART_RATE;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.health.connect.HealthConnectManager;
import android.health.connect.datatypes.validation.ValidationUtils;
import android.health.connect.internal.datatypes.HeartRateRecordInternal;
@@ -115,7 +116,7 @@ public final class HeartRateRecord extends IntervalRecord {
* @return {@code true} if this object is the same as the obj
*/
@Override
- public boolean equals(@NonNull Object object) {
+ public boolean equals(@Nullable Object object) {
if (super.equals(object) && object instanceof HeartRateRecord) {
HeartRateRecord other = (HeartRateRecord) object;
if (getSamples().size() != other.getSamples().size()) return false;
@@ -193,7 +194,7 @@ public final class HeartRateRecord extends IntervalRecord {
* @return {@code true} if this object is the same as the obj
*/
@Override
- public boolean equals(@NonNull Object object) {
+ public boolean equals(@Nullable Object object) {
if (super.equals(object) && object instanceof HeartRateSample) {
HeartRateSample other = (HeartRateSample) object;
return getBeatsPerMinute() == other.getBeatsPerMinute()
diff --git a/framework/java/android/health/connect/datatypes/InstantRecord.java b/framework/java/android/health/connect/datatypes/InstantRecord.java
index eb91584a..a7129180 100644
--- a/framework/java/android/health/connect/datatypes/InstantRecord.java
+++ b/framework/java/android/health/connect/datatypes/InstantRecord.java
@@ -17,6 +17,7 @@
package android.health.connect.datatypes;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import java.time.Instant;
import java.time.ZoneOffset;
@@ -75,7 +76,7 @@ public abstract class InstantRecord extends Record {
* @return {@code true} if this object is the same as the obj
*/
@Override
- public boolean equals(@NonNull Object object) {
+ public boolean equals(@Nullable Object object) {
if (super.equals(object)) {
InstantRecord other = (InstantRecord) object;
return this.getTime().toEpochMilli() == other.getTime().toEpochMilli()
diff --git a/framework/java/android/health/connect/datatypes/IntervalRecord.java b/framework/java/android/health/connect/datatypes/IntervalRecord.java
index 03fbd096..5d55c5c3 100644
--- a/framework/java/android/health/connect/datatypes/IntervalRecord.java
+++ b/framework/java/android/health/connect/datatypes/IntervalRecord.java
@@ -16,6 +16,7 @@
package android.health.connect.datatypes;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import java.time.Instant;
import java.time.ZoneOffset;
@@ -102,7 +103,7 @@ public abstract class IntervalRecord extends Record {
* @return {@code true} if this object is the same as the obj
*/
@Override
- public boolean equals(@NonNull Object object) {
+ public boolean equals(@Nullable Object object) {
if (super.equals(object)) {
IntervalRecord other = (IntervalRecord) object;
return getStartTime().toEpochMilli() == other.getStartTime().toEpochMilli()
diff --git a/framework/java/android/health/connect/datatypes/MenstruationPeriodRecord.java b/framework/java/android/health/connect/datatypes/MenstruationPeriodRecord.java
index 6398d896..d533e74c 100644
--- a/framework/java/android/health/connect/datatypes/MenstruationPeriodRecord.java
+++ b/framework/java/android/health/connect/datatypes/MenstruationPeriodRecord.java
@@ -16,6 +16,7 @@
package android.health.connect.datatypes;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.health.connect.internal.datatypes.MenstruationPeriodRecordInternal;
import java.time.Instant;
@@ -52,7 +53,7 @@ public final class MenstruationPeriodRecord extends IntervalRecord {
* otherwise.
*/
@Override
- public boolean equals(@NonNull Object object) {
+ public boolean equals(@Nullable Object object) {
return super.equals(object);
}
diff --git a/framework/java/android/health/connect/datatypes/Metadata.java b/framework/java/android/health/connect/datatypes/Metadata.java
index 9f2daeab..a80b6e33 100644
--- a/framework/java/android/health/connect/datatypes/Metadata.java
+++ b/framework/java/android/health/connect/datatypes/Metadata.java
@@ -189,7 +189,7 @@ public final class Metadata {
* @return {@code true} if this object is the same as the obj
*/
@Override
- public boolean equals(@NonNull Object object) {
+ public boolean equals(@Nullable Object object) {
if (this == object) return true;
if (object instanceof Metadata) {
Metadata other = (Metadata) object;
diff --git a/framework/java/android/health/connect/datatypes/PowerRecord.java b/framework/java/android/health/connect/datatypes/PowerRecord.java
index 70e35fda..859d80bf 100644
--- a/framework/java/android/health/connect/datatypes/PowerRecord.java
+++ b/framework/java/android/health/connect/datatypes/PowerRecord.java
@@ -18,6 +18,7 @@ package android.health.connect.datatypes;
import static android.health.connect.datatypes.RecordTypeIdentifier.RECORD_TYPE_POWER;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.health.connect.HealthConnectManager;
import android.health.connect.datatypes.units.Power;
import android.health.connect.datatypes.validation.ValidationUtils;
@@ -280,7 +281,7 @@ public final class PowerRecord extends IntervalRecord {
* @return {@code true} if this object is the same as the obj
*/
@Override
- public boolean equals(@NonNull Object object) {
+ public boolean equals(@Nullable Object object) {
if (super.equals(object) && object instanceof PowerRecord) {
PowerRecord other = (PowerRecord) object;
if (getSamples().size() != other.getSamples().size()) return false;
diff --git a/framework/java/android/health/connect/datatypes/Record.java b/framework/java/android/health/connect/datatypes/Record.java
index b9804fd9..15261529 100644
--- a/framework/java/android/health/connect/datatypes/Record.java
+++ b/framework/java/android/health/connect/datatypes/Record.java
@@ -19,6 +19,7 @@ package android.health.connect.datatypes;
import static android.health.connect.datatypes.validation.ValidationUtils.validateIntDefValue;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.health.connect.internal.datatypes.RecordInternal;
@@ -67,7 +68,7 @@ public abstract class Record {
* @return {@code true} if this object is the same as the obj
*/
@Override
- public boolean equals(@NonNull Object object) {
+ public boolean equals(@Nullable Object object) {
if (this == object) return true;
if (Objects.isNull(object)) {
return false;
diff --git a/framework/java/android/health/connect/datatypes/SpeedRecord.java b/framework/java/android/health/connect/datatypes/SpeedRecord.java
index 5259ae69..bcc00539 100644
--- a/framework/java/android/health/connect/datatypes/SpeedRecord.java
+++ b/framework/java/android/health/connect/datatypes/SpeedRecord.java
@@ -16,6 +16,7 @@
package android.health.connect.datatypes;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.health.connect.HealthConnectManager;
import android.health.connect.datatypes.units.Velocity;
import android.health.connect.datatypes.validation.ValidationUtils;
@@ -165,7 +166,7 @@ public final class SpeedRecord extends IntervalRecord {
* @return {@code true} if this object is the same as the obj
*/
@Override
- public boolean equals(@NonNull Object object) {
+ public boolean equals(@Nullable Object object) {
if (super.equals(object) && object instanceof SpeedRecordSample) {
SpeedRecordSample other = (SpeedRecordSample) object;
return getSpeed().equals(other.getSpeed())
@@ -286,7 +287,7 @@ public final class SpeedRecord extends IntervalRecord {
* @return {@code true} if this object is the same as the obj
*/
@Override
- public boolean equals(@NonNull Object object) {
+ public boolean equals(@Nullable Object object) {
if (super.equals(object) && object instanceof SpeedRecord) {
SpeedRecord other = (SpeedRecord) object;
if (getSamples().size() != other.getSamples().size()) return false;
diff --git a/framework/java/android/health/connect/datatypes/StepsCadenceRecord.java b/framework/java/android/health/connect/datatypes/StepsCadenceRecord.java
index 6a026563..6b3fdeec 100644
--- a/framework/java/android/health/connect/datatypes/StepsCadenceRecord.java
+++ b/framework/java/android/health/connect/datatypes/StepsCadenceRecord.java
@@ -16,6 +16,7 @@
package android.health.connect.datatypes;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.health.connect.HealthConnectManager;
import android.health.connect.datatypes.validation.ValidationUtils;
import android.health.connect.internal.datatypes.StepsCadenceRecordInternal;
@@ -162,7 +163,7 @@ public final class StepsCadenceRecord extends IntervalRecord {
* @return {@code true} if this object is the same as the obj
*/
@Override
- public boolean equals(@NonNull Object object) {
+ public boolean equals(@Nullable Object object) {
if (super.equals(object) && object instanceof StepsCadenceRecordSample) {
StepsCadenceRecordSample other = (StepsCadenceRecordSample) object;
return getRate() == other.getRate()
@@ -282,7 +283,7 @@ public final class StepsCadenceRecord extends IntervalRecord {
* @return {@code true} if this object is the same as the obj
*/
@Override
- public boolean equals(@NonNull Object object) {
+ public boolean equals(@Nullable Object object) {
if (super.equals(object) && object instanceof StepsCadenceRecord) {
StepsCadenceRecord other = (StepsCadenceRecord) object;
if (getSamples().size() != other.getSamples().size()) return false;
diff --git a/framework/java/android/health/connect/datatypes/TotalCaloriesBurnedRecord.java b/framework/java/android/health/connect/datatypes/TotalCaloriesBurnedRecord.java
index 729fc28e..b75f14ed 100644
--- a/framework/java/android/health/connect/datatypes/TotalCaloriesBurnedRecord.java
+++ b/framework/java/android/health/connect/datatypes/TotalCaloriesBurnedRecord.java
@@ -18,6 +18,7 @@ package android.health.connect.datatypes;
import static android.health.connect.datatypes.RecordTypeIdentifier.RECORD_TYPE_TOTAL_CALORIES_BURNED;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.health.connect.HealthConnectManager;
import android.health.connect.datatypes.units.Energy;
import android.health.connect.datatypes.validation.ValidationUtils;
@@ -95,7 +96,7 @@ public final class TotalCaloriesBurnedRecord extends IntervalRecord {
* otherwise.
*/
@Override
- public boolean equals(@NonNull Object object) {
+ public boolean equals(@Nullable Object object) {
if (super.equals(object) && object instanceof TotalCaloriesBurnedRecord) {
TotalCaloriesBurnedRecord other = (TotalCaloriesBurnedRecord) object;
return this.getEnergy().equals(other.getEnergy());
diff --git a/framework/java/android/health/connect/datatypes/WheelchairPushesRecord.java b/framework/java/android/health/connect/datatypes/WheelchairPushesRecord.java
index bcb30c41..d308e92f 100644
--- a/framework/java/android/health/connect/datatypes/WheelchairPushesRecord.java
+++ b/framework/java/android/health/connect/datatypes/WheelchairPushesRecord.java
@@ -20,6 +20,7 @@ import static android.health.connect.datatypes.RecordTypeIdentifier.RECORD_TYPE_
import android.annotation.IntRange;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.health.connect.HealthConnectManager;
import android.health.connect.datatypes.validation.ValidationUtils;
import android.health.connect.internal.datatypes.WheelchairPushesRecordInternal;
@@ -94,7 +95,7 @@ public final class WheelchairPushesRecord extends IntervalRecord {
* otherwise.
*/
@Override
- public boolean equals(@NonNull Object object) {
+ public boolean equals(@Nullable Object object) {
if (super.equals(object) && object instanceof WheelchairPushesRecord) {
WheelchairPushesRecord other = (WheelchairPushesRecord) object;
return this.getCount() == other.getCount();
diff --git a/framework/java/android/health/connect/internal/ParcelUtils.java b/framework/java/android/health/connect/internal/ParcelUtils.java
index 8a8f2dac..06f487ac 100644
--- a/framework/java/android/health/connect/internal/ParcelUtils.java
+++ b/framework/java/android/health/connect/internal/ParcelUtils.java
@@ -81,10 +81,11 @@ public final class ParcelUtils {
parcelRunnable.writeToParcel(dataParcel);
final int dataParcelSize = dataParcel.dataSize();
if (dataParcelSize > IPC_PARCEL_LIMIT) {
- SharedMemory sharedMemory =
- ParcelUtils.getSharedMemoryForParcel(dataParcel, dataParcelSize);
- dest.writeInt(USING_SHARED_MEMORY);
- sharedMemory.writeToParcel(dest, flags);
+ try (SharedMemory sharedMemory =
+ ParcelUtils.getSharedMemoryForParcel(dataParcel, dataParcelSize)) {
+ dest.writeInt(USING_SHARED_MEMORY);
+ sharedMemory.writeToParcel(dest, flags);
+ }
} else {
dest.writeInt(USING_PARCEL);
parcelRunnable.writeToParcel(dest);
diff --git a/service/java/com/android/server/healthconnect/HealthConnectDeviceConfigManager.java b/service/java/com/android/server/healthconnect/HealthConnectDeviceConfigManager.java
index 5251636b..b2395ce9 100644
--- a/service/java/com/android/server/healthconnect/HealthConnectDeviceConfigManager.java
+++ b/service/java/com/android/server/healthconnect/HealthConnectDeviceConfigManager.java
@@ -125,10 +125,14 @@ public class HealthConnectDeviceConfigManager implements DeviceConfig.OnProperti
private static final boolean SESSION_DATATYPE_DEFAULT_FLAG_VALUE = true;
private static final boolean EXERCISE_ROUTE_DEFAULT_FLAG_VALUE = true;
public static final boolean ENABLE_RATE_LIMITER_DEFAULT_FLAG_VALUE = true;
- public static final int QUOTA_BUCKET_PER_15M_FOREGROUND_DEFAULT_FLAG_VALUE = 1000;
- public static final int QUOTA_BUCKET_PER_24H_FOREGROUND_DEFAULT_FLAG_VALUE = 8000;
- public static final int QUOTA_BUCKET_PER_15M_BACKGROUND_DEFAULT_FLAG_VALUE = 1000;
- public static final int QUOTA_BUCKET_PER_24H_BACKGROUND_DEFAULT_FLAG_VALUE = 8000;
+ public static final int QUOTA_BUCKET_READS_PER_15M_FOREGROUND_DEFAULT_FLAG_VALUE = 2000;
+ public static final int QUOTA_BUCKET_READS_PER_24H_FOREGROUND_DEFAULT_FLAG_VALUE = 16000;
+ public static final int QUOTA_BUCKET_READS_PER_15M_BACKGROUND_DEFAULT_FLAG_VALUE = 1000;
+ public static final int QUOTA_BUCKET_READS_PER_24H_BACKGROUND_DEFAULT_FLAG_VALUE = 8000;
+ public static final int QUOTA_BUCKET_WRITES_PER_15M_FOREGROUND_DEFAULT_FLAG_VALUE = 1000;
+ public static final int QUOTA_BUCKET_WRITES_PER_24H_FOREGROUND_DEFAULT_FLAG_VALUE = 8000;
+ public static final int QUOTA_BUCKET_WRITES_PER_15M_BACKGROUND_DEFAULT_FLAG_VALUE = 1000;
+ public static final int QUOTA_BUCKET_WRITES_PER_24H_BACKGROUND_DEFAULT_FLAG_VALUE = 8000;
public static final int CHUNK_SIZE_LIMIT_IN_BYTES_DEFAULT_FLAG_VALUE = 5000000;
public static final int RECORD_SIZE_LIMIT_IN_BYTES_DEFAULT_FLAG_VALUE = 1000000;
public static final int DATA_PUSH_LIMIT_PER_APP_15M_DEFAULT_FLAG_VALUE = 35000000;
@@ -512,49 +516,49 @@ public class HealthConnectDeviceConfigManager implements DeviceConfig.OnProperti
DeviceConfig.getInt(
DeviceConfig.NAMESPACE_HEALTH_FITNESS,
MAX_READ_REQUESTS_PER_24H_FOREGROUND_FLAG,
- QUOTA_BUCKET_PER_24H_FOREGROUND_DEFAULT_FLAG_VALUE));
+ QUOTA_BUCKET_READS_PER_24H_FOREGROUND_DEFAULT_FLAG_VALUE));
quotaBucketToMaxRollingQuotaMap.put(
QuotaBucket.QUOTA_BUCKET_READS_PER_24H_BACKGROUND,
DeviceConfig.getInt(
DeviceConfig.NAMESPACE_HEALTH_FITNESS,
MAX_READ_REQUESTS_PER_24H_BACKGROUND_FLAG,
- QUOTA_BUCKET_PER_24H_BACKGROUND_DEFAULT_FLAG_VALUE));
+ QUOTA_BUCKET_READS_PER_24H_BACKGROUND_DEFAULT_FLAG_VALUE));
quotaBucketToMaxRollingQuotaMap.put(
QuotaBucket.QUOTA_BUCKET_READS_PER_15M_FOREGROUND,
DeviceConfig.getInt(
DeviceConfig.NAMESPACE_HEALTH_FITNESS,
MAX_READ_REQUESTS_PER_15M_FOREGROUND_FLAG,
- QUOTA_BUCKET_PER_15M_FOREGROUND_DEFAULT_FLAG_VALUE));
+ QUOTA_BUCKET_READS_PER_15M_FOREGROUND_DEFAULT_FLAG_VALUE));
quotaBucketToMaxRollingQuotaMap.put(
QuotaBucket.QUOTA_BUCKET_READS_PER_15M_BACKGROUND,
DeviceConfig.getInt(
DeviceConfig.NAMESPACE_HEALTH_FITNESS,
MAX_READ_REQUESTS_PER_15M_BACKGROUND_FLAG,
- QUOTA_BUCKET_PER_15M_BACKGROUND_DEFAULT_FLAG_VALUE));
+ QUOTA_BUCKET_READS_PER_15M_BACKGROUND_DEFAULT_FLAG_VALUE));
quotaBucketToMaxRollingQuotaMap.put(
QuotaBucket.QUOTA_BUCKET_WRITES_PER_24H_FOREGROUND,
DeviceConfig.getInt(
DeviceConfig.NAMESPACE_HEALTH_FITNESS,
MAX_WRITE_REQUESTS_PER_24H_FOREGROUND_FLAG,
- QUOTA_BUCKET_PER_24H_FOREGROUND_DEFAULT_FLAG_VALUE));
+ QUOTA_BUCKET_WRITES_PER_24H_FOREGROUND_DEFAULT_FLAG_VALUE));
quotaBucketToMaxRollingQuotaMap.put(
QuotaBucket.QUOTA_BUCKET_WRITES_PER_24H_BACKGROUND,
DeviceConfig.getInt(
DeviceConfig.NAMESPACE_HEALTH_FITNESS,
MAX_WRITE_REQUESTS_PER_24H_BACKGROUND_FLAG,
- QUOTA_BUCKET_PER_24H_BACKGROUND_DEFAULT_FLAG_VALUE));
+ QUOTA_BUCKET_WRITES_PER_24H_BACKGROUND_DEFAULT_FLAG_VALUE));
quotaBucketToMaxRollingQuotaMap.put(
QuotaBucket.QUOTA_BUCKET_WRITES_PER_15M_FOREGROUND,
DeviceConfig.getInt(
DeviceConfig.NAMESPACE_HEALTH_FITNESS,
MAX_WRITE_REQUESTS_PER_15M_FOREGROUND_FLAG,
- QUOTA_BUCKET_PER_15M_FOREGROUND_DEFAULT_FLAG_VALUE));
+ QUOTA_BUCKET_WRITES_PER_15M_FOREGROUND_DEFAULT_FLAG_VALUE));
quotaBucketToMaxRollingQuotaMap.put(
QuotaBucket.QUOTA_BUCKET_WRITES_PER_15M_BACKGROUND,
DeviceConfig.getInt(
DeviceConfig.NAMESPACE_HEALTH_FITNESS,
MAX_WRITE_REQUESTS_PER_15M_BACKGROUND_FLAG,
- QUOTA_BUCKET_PER_15M_BACKGROUND_DEFAULT_FLAG_VALUE));
+ QUOTA_BUCKET_WRITES_PER_15M_BACKGROUND_DEFAULT_FLAG_VALUE));
quotaBucketToMaxRollingQuotaMap.put(
QuotaBucket.QUOTA_BUCKET_DATA_PUSH_LIMIT_PER_APP_15M,
DeviceConfig.getInt(
diff --git a/service/java/com/android/server/healthconnect/HealthConnectServiceImpl.java b/service/java/com/android/server/healthconnect/HealthConnectServiceImpl.java
index 5db5b39a..ae903796 100644
--- a/service/java/com/android/server/healthconnect/HealthConnectServiceImpl.java
+++ b/service/java/com/android/server/healthconnect/HealthConnectServiceImpl.java
@@ -150,6 +150,7 @@ import java.io.IOException;
import java.time.Instant;
import java.time.LocalDate;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -488,6 +489,8 @@ final class HealthConnectServiceImpl extends IHealthConnectService.Stub {
}
long startDateAccess;
+ // TODO(b/309776578): Consider making background reads possible for
+ // aggregations when only using own data
if (!holdsDataManagementPermission) {
boolean isInForeground = mAppOpsManagerLocal.isUidInForeground(uid);
logger.setCallerForegroundState(isInForeground);
@@ -504,13 +507,24 @@ final class HealthConnectServiceImpl extends IHealthConnectService.Stub {
RateLimiter.QuotaCategory.QUOTA_CATEGORY_READ,
isInForeground,
logger);
- mDataPermissionEnforcer.enforceRecordIdsReadPermissions(
- recordTypesToTest, attributionSource);
+ boolean enforceSelfRead =
+ mDataPermissionEnforcer.enforceReadAccessAndGetEnforceSelfRead(
+ recordTypesToTest, attributionSource);
startDateAccess =
mPermissionHelper
.getHealthDataStartDateAccessOrThrow(
attributionSource.getPackageName(), userHandle)
.toEpochMilli();
+ maybeEnforceOnlyCallingPackageDataRequested(
+ request.getPackageFilters(),
+ attributionSource.getPackageName(),
+ enforceSelfRead,
+ "aggregationTypes: "
+ + Arrays.stream(request.getAggregateIds())
+ .mapToObj(
+ AggregationTypeIdMapper.getInstance()
+ ::getAggregationTypeFor)
+ .collect(Collectors.toList()));
} else {
startDateAccess = request.getStartTime();
}
@@ -608,6 +622,20 @@ final class HealthConnectServiceImpl extends IHealthConnectService.Stub {
// then enforce self read
enforceSelfRead = isOnlySelfReadInBackgroundAllowed(uid, pid);
}
+ if (request.getRecordIdFiltersParcel() == null) {
+ // Only enforce requested packages if this is a
+ // ReadRecordsByRequest using filters. Reading by IDs does not have
+ // data origins specified.
+ // TODO(b/309778116): Consider throwing an error when reading by Id
+ maybeEnforceOnlyCallingPackageDataRequested(
+ request.getPackageFilters(),
+ callingPackageName,
+ enforceSelfRead,
+ "recordType: "
+ + RecordMapper.getInstance()
+ .getRecordIdToExternalRecordClassMap()
+ .get(request.getRecordType()));
+ }
if (Constants.DEBUG) {
Slog.d(
@@ -753,6 +781,21 @@ final class HealthConnectServiceImpl extends IHealthConnectService.Stub {
holdsDataManagementPermission);
}
+ private void maybeEnforceOnlyCallingPackageDataRequested(
+ List<String> packageFilters,
+ String callingPackageName,
+ boolean enforceSelfRead,
+ String entityFailureMessage) {
+ if (enforceSelfRead
+ && (packageFilters.size() != 1
+ || !packageFilters.get(0).equals(callingPackageName))) {
+ throwSecurityException(
+ "Caller does not have permission to read data for the following ("
+ + entityFailureMessage
+ + ") from other applications.");
+ }
+ }
+
/**
* Updates {@code recordsParcel} into the HealthConnect database.
*
diff --git a/service/java/com/android/server/healthconnect/permission/DataPermissionEnforcer.java b/service/java/com/android/server/healthconnect/permission/DataPermissionEnforcer.java
index f0acfc6f..62146e9c 100644
--- a/service/java/com/android/server/healthconnect/permission/DataPermissionEnforcer.java
+++ b/service/java/com/android/server/healthconnect/permission/DataPermissionEnforcer.java
@@ -104,6 +104,26 @@ public class DataPermissionEnforcer {
return enforceSelfRead;
}
+ // TODO(b/312952346): Consider refactoring how permission enforcement is done within
+ // HealthConnectServiceImpl. This goes beyond just this method.
+ /**
+ * Enforces that the caller has either read or write permissions for all the given recordTypes,
+ * and returns {@code true} if the caller is allowed to read only records written by itself,
+ * false otherwise.
+ *
+ * @throws SecurityException if the app has neither read nor write permissions for any of the
+ * specified record types.
+ */
+ public boolean enforceReadAccessAndGetEnforceSelfRead(
+ List<Integer> recordTypes, AttributionSource attributionSource) {
+ boolean enforceSelfRead = false;
+ for (int recordTypeId : recordTypes) {
+ enforceSelfRead |=
+ enforceReadAccessAndGetEnforceSelfRead(recordTypeId, attributionSource);
+ }
+ return enforceSelfRead;
+ }
+
/**
* Enforces that caller has all write permissions to write given records. Includes permissions
* for writing optional extra data if it's present in given records.
diff --git a/service/java/com/android/server/healthconnect/storage/datatypehelpers/AccessLogsHelper.java b/service/java/com/android/server/healthconnect/storage/datatypehelpers/AccessLogsHelper.java
index 8a104a21..4e1f7e0e 100644
--- a/service/java/com/android/server/healthconnect/storage/datatypehelpers/AccessLogsHelper.java
+++ b/service/java/com/android/server/healthconnect/storage/datatypehelpers/AccessLogsHelper.java
@@ -16,7 +16,6 @@
package com.android.server.healthconnect.storage.datatypehelpers;
-import static com.android.server.healthconnect.storage.datatypehelpers.InstantRecordHelper.TIME_COLUMN_NAME;
import static com.android.server.healthconnect.storage.datatypehelpers.RecordHelper.PRIMARY_COLUMN_NAME;
import static com.android.server.healthconnect.storage.utils.StorageUtils.DELIMITER;
import static com.android.server.healthconnect.storage.utils.StorageUtils.INTEGER_NOT_NULL;
@@ -154,7 +153,7 @@ public final class AccessLogsHelper extends DatabaseHelper {
public DeleteTableRequest getDeleteRequestForAutoDelete() {
return new DeleteTableRequest(TABLE_NAME)
.setTimeFilter(
- TIME_COLUMN_NAME,
+ ACCESS_TIME_COLUMN_NAME,
Instant.EPOCH.toEpochMilli(),
Instant.now()
.minus(DEFAULT_ACCESS_LOG_TIME_PERIOD_IN_DAYS, ChronoUnit.DAYS)
diff --git a/service/java/com/android/server/healthconnect/storage/datatypehelpers/HealthDataCategoryPriorityHelper.java b/service/java/com/android/server/healthconnect/storage/datatypehelpers/HealthDataCategoryPriorityHelper.java
index a2cea142..98331042 100644
--- a/service/java/com/android/server/healthconnect/storage/datatypehelpers/HealthDataCategoryPriorityHelper.java
+++ b/service/java/com/android/server/healthconnect/storage/datatypehelpers/HealthDataCategoryPriorityHelper.java
@@ -401,11 +401,11 @@ public class HealthDataCategoryPriorityHelper extends DatabaseHelper {
HealthConnectDeviceConfigManager.getInitialisedInstance()
.isAggregationSourceControlsEnabled();
// Candidates to be added to the priority list
- Map<Integer, Set<Long>> dataCategoryToAppIdMapHavingPermission =
+ Map<Integer, List<Long>> dataCategoryToAppIdMapHavingPermission =
getHealthDataCategoryToAppIdPriorityMap().entrySet().stream()
.collect(
Collectors.toMap(
- Map.Entry::getKey, e -> new HashSet<>(e.getValue())));
+ Map.Entry::getKey, e -> new ArrayList<>(e.getValue())));
// Candidates to be removed from the priority list
Map<Integer, Set<Long>> dataCategoryToAppIdMapWithoutPermission =
getHealthDataCategoryToAppIdPriorityMap().entrySet().stream()
@@ -421,10 +421,11 @@ public class HealthDataCategoryPriorityHelper extends DatabaseHelper {
long appInfoId = appInfoHelper.getOrInsertAppInfoId(packageInfo.packageName, context);
for (int dataCategory : dataCategoriesWithWritePermissionsForThisPackage) {
- Set<Long> appIdsHavingPermission =
+ List<Long> appIdsHavingPermission =
dataCategoryToAppIdMapHavingPermission.getOrDefault(
- dataCategory, new HashSet<>());
- if (appIdsHavingPermission.add(appInfoId)) {
+ dataCategory, new ArrayList<>());
+ if (!appIdsHavingPermission.contains(appInfoId)
+ && appIdsHavingPermission.add(appInfoId)) {
dataCategoryToAppIdMapHavingPermission.put(
dataCategory, appIdsHavingPermission);
}
@@ -512,7 +513,7 @@ public class HealthDataCategoryPriorityHelper extends DatabaseHelper {
}
private synchronized void updateTableWithNewPriorityList(
- Map<Integer, Set<Long>> healthDataCategoryToAppIdPriorityMap) {
+ Map<Integer, List<Long>> healthDataCategoryToAppIdPriorityMap) {
for (int dataCategory : healthDataCategoryToAppIdPriorityMap.keySet()) {
List<Long> appInfoIdList =
List.copyOf(healthDataCategoryToAppIdPriorityMap.get(dataCategory));
diff --git a/service/java/com/android/server/healthconnect/storage/datatypehelpers/aggregation/PriorityRecordsAggregator.java b/service/java/com/android/server/healthconnect/storage/datatypehelpers/aggregation/PriorityRecordsAggregator.java
index 2e68637a..5af3e163 100644
--- a/service/java/com/android/server/healthconnect/storage/datatypehelpers/aggregation/PriorityRecordsAggregator.java
+++ b/service/java/com/android/server/healthconnect/storage/datatypehelpers/aggregation/PriorityRecordsAggregator.java
@@ -190,6 +190,13 @@ public class PriorityRecordsAggregator {
return null;
}
+ // TODO(b/313924267): workaround for b/308467442, should be remove once we have a long term
+ // solution
+ if (data.getStartTime() > data.getEndTime()) {
+ // skip records with start time > end time to keep the algorithm functional
+ return null;
+ }
+
mTimestampsBuffer.add(data.getStartTimestamp());
mTimestampsBuffer.add(data.getEndTimestamp());
return data;
diff --git a/tests/cts/hostsidetests/healthconnect/HealthConnectTestHelper/src/android/healthconnect/cts/testhelper/DailyLogsTests.java b/tests/cts/hostsidetests/healthconnect/HealthConnectTestHelper/src/android/healthconnect/cts/testhelper/DailyLogsTests.java
index f8c3735d..e7fd2c7a 100644
--- a/tests/cts/hostsidetests/healthconnect/HealthConnectTestHelper/src/android/healthconnect/cts/testhelper/DailyLogsTests.java
+++ b/tests/cts/hostsidetests/healthconnect/HealthConnectTestHelper/src/android/healthconnect/cts/testhelper/DailyLogsTests.java
@@ -21,12 +21,16 @@ import static android.healthconnect.cts.testhelper.TestHelperUtils.getBloodPress
import static android.healthconnect.cts.testhelper.TestHelperUtils.getHeartRateRecord;
import static android.healthconnect.cts.testhelper.TestHelperUtils.getStepsRecord;
import static android.healthconnect.cts.testhelper.TestHelperUtils.insertRecords;
+import static android.healthconnect.cts.testhelper.TestHelperUtils.queryAccessLogs;
+
+import static com.google.common.truth.Truth.assertThat;
import android.health.connect.HealthConnectManager;
import androidx.test.InstrumentationRegistry;
import com.android.compatibility.common.util.NonApiTest;
+import com.android.compatibility.common.util.SystemUtil;
import org.junit.Test;
@@ -46,10 +50,42 @@ public class DailyLogsTests {
InstrumentationRegistry.getContext().getSystemService(HealthConnectManager.class);
@Test
- public void testHealthConnectDatabaseStats() throws Exception {
- insertRecords(
- List.of(getStepsRecord(), getBloodPressureRecord(), getHeartRateRecord()),
- mHealthConnectManager);
+ public void testInsertRecordsSucceed() throws Exception {
+ assertThat(
+ insertRecords(
+ List.of(
+ getStepsRecord(),
+ getBloodPressureRecord(),
+ getHeartRateRecord()),
+ mHealthConnectManager))
+ .hasSize(3);
+ }
+
+ @Test
+ public void testHealthConnectAccessLogsEqualsZero() throws Exception {
+ SystemUtil.runWithShellPermissionIdentity(
+ () -> {
+ assertThat(queryAccessLogs(mHealthConnectManager)).hasSize(0);
+ },
+ "android.permission.MANAGE_HEALTH_DATA");
+ }
+
+ @Test
+ public void testHealthConnectAccessLogsEqualsOne() throws Exception {
+ SystemUtil.runWithShellPermissionIdentity(
+ () -> {
+ assertThat(queryAccessLogs(mHealthConnectManager)).hasSize(1);
+ },
+ "android.permission.MANAGE_HEALTH_DATA");
+ }
+
+ @Test
+ public void testHealthConnectAccessLogsEqualsTwo() throws Exception {
+ SystemUtil.runWithShellPermissionIdentity(
+ () -> {
+ assertThat(queryAccessLogs(mHealthConnectManager)).hasSize(2);
+ },
+ "android.permission.MANAGE_HEALTH_DATA");
}
/**
diff --git a/tests/cts/hostsidetests/healthconnect/HealthConnectTestHelper/src/android/healthconnect/cts/testhelper/HealthConnectServiceLogsTests.java b/tests/cts/hostsidetests/healthconnect/HealthConnectTestHelper/src/android/healthconnect/cts/testhelper/HealthConnectServiceLogsTests.java
index 9eaa71ac..4c55b2b7 100644
--- a/tests/cts/hostsidetests/healthconnect/HealthConnectTestHelper/src/android/healthconnect/cts/testhelper/HealthConnectServiceLogsTests.java
+++ b/tests/cts/hostsidetests/healthconnect/HealthConnectTestHelper/src/android/healthconnect/cts/testhelper/HealthConnectServiceLogsTests.java
@@ -43,6 +43,7 @@ import android.health.connect.changelog.ChangeLogTokenResponse;
import android.health.connect.changelog.ChangeLogsRequest;
import android.health.connect.changelog.ChangeLogsResponse;
import android.health.connect.datatypes.BloodPressureRecord;
+import android.health.connect.datatypes.DataOrigin;
import android.health.connect.datatypes.HeightRecord;
import android.health.connect.datatypes.Record;
import android.health.connect.datatypes.StepsRecord;
@@ -161,6 +162,8 @@ public class HealthConnectServiceLogsTests {
mHealthConnectManager.readRecords(
new ReadRecordsRequestUsingFilters.Builder<>(StepsRecord.class)
.setTimeRangeFilter(getDefaultTimeRangeFilter())
+ .addDataOrigins(
+ new DataOrigin.Builder().setPackageName(MY_PACKAGE_NAME).build())
.setPageSize(1)
.build(),
Executors.newSingleThreadExecutor(),
diff --git a/tests/cts/hostsidetests/healthconnect/HealthConnectTestHelper/src/android/healthconnect/cts/testhelper/HealthConnectTestHelper.java b/tests/cts/hostsidetests/healthconnect/HealthConnectTestHelper/src/android/healthconnect/cts/testhelper/HealthConnectTestHelper.java
index b882b89c..07d4a759 100644
--- a/tests/cts/hostsidetests/healthconnect/HealthConnectTestHelper/src/android/healthconnect/cts/testhelper/HealthConnectTestHelper.java
+++ b/tests/cts/hostsidetests/healthconnect/HealthConnectTestHelper/src/android/healthconnect/cts/testhelper/HealthConnectTestHelper.java
@@ -20,6 +20,7 @@ import static android.healthconnect.cts.lib.MultiAppTestUtils.APP_PKG_NAME_USED_
import static android.healthconnect.cts.lib.MultiAppTestUtils.CHANGE_LOGS_RESPONSE;
import static android.healthconnect.cts.lib.MultiAppTestUtils.CHANGE_LOG_TOKEN;
import static android.healthconnect.cts.lib.MultiAppTestUtils.CLIENT_ID;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.DATA_ORIGIN_FILTER_PACKAGE_NAMES;
import static android.healthconnect.cts.lib.MultiAppTestUtils.DELETE_RECORDS_QUERY;
import static android.healthconnect.cts.lib.MultiAppTestUtils.END_TIME;
import static android.healthconnect.cts.lib.MultiAppTestUtils.EXERCISE_SESSION;
@@ -66,6 +67,7 @@ import android.health.connect.changelog.ChangeLogsResponse;
import android.health.connect.datatypes.DataOrigin;
import android.health.connect.datatypes.ExerciseSessionRecord;
import android.health.connect.datatypes.Record;
+import android.health.connect.datatypes.StepsRecord;
import android.healthconnect.cts.utils.TestUtils;
import android.os.Bundle;
@@ -149,10 +151,19 @@ public class HealthConnectTestHelper extends Activity {
break;
case READ_RECORDS_QUERY:
if (bundle.containsKey(READ_USING_DATA_ORIGIN_FILTERS)) {
+ List<String> dataOriginPackageNames =
+ bundle.containsKey(DATA_ORIGIN_FILTER_PACKAGE_NAMES)
+ ?
+ // if a set of data origin filters is specified, use that
+ bundle.getStringArrayList(DATA_ORIGIN_FILTER_PACKAGE_NAMES)
+ :
+ // otherwise default to this app's package name
+ List.of(context.getPackageName());
returnIntent =
readRecordsUsingDataOriginFilters(
queryType,
bundle.getStringArrayList(READ_RECORD_CLASS_NAME),
+ dataOriginPackageNames,
context);
break;
}
@@ -461,23 +472,25 @@ public class HealthConnectTestHelper extends Activity {
* @return Intent to send back to the main app which is running the tests
*/
private Intent readRecordsUsingDataOriginFilters(
- String queryType, ArrayList<String> recordClassesToRead, Context context) {
+ String queryType,
+ ArrayList<String> recordClassesToRead,
+ List<String> dataOriginPackageNames,
+ Context context) {
final Intent intent = new Intent(queryType);
int recordsSize = 0;
try {
for (String recordClass : recordClassesToRead) {
- List<? extends Record> recordsRead =
- readRecords(
- new ReadRecordsRequestUsingFilters.Builder<>(
- (Class<? extends Record>)
- Class.forName(recordClass))
- .addDataOrigins(
- new DataOrigin.Builder()
- .setPackageName(context.getPackageName())
- .build())
- .build(),
- context);
+ ReadRecordsRequestUsingFilters.Builder requestBuilder =
+ new ReadRecordsRequestUsingFilters.Builder<>(
+ (Class<? extends Record>) Class.forName(recordClass));
+ dataOriginPackageNames.forEach(
+ packageName ->
+ requestBuilder.addDataOrigins(
+ new DataOrigin.Builder()
+ .setPackageName(packageName)
+ .build()));
+ List<? extends Record> recordsRead = readRecords(requestBuilder.build(), context);
recordsSize += recordsRead.size();
}
} catch (Exception e) {
@@ -590,8 +603,15 @@ public class HealthConnectTestHelper extends Activity {
Arrays.asList(
buildStepsRecord(
startTime, endTime, stepsCount, context.getPackageName()));
- insertRecords(recordToInsert, context);
+ List<Record> insertedRecords = insertRecords(recordToInsert, context);
+ List<TestUtils.RecordTypeAndRecordIds> recordTypeAndRecordIdsList =
+ new ArrayList<TestUtils.RecordTypeAndRecordIds>();
+ recordTypeAndRecordIdsList.add(
+ new TestUtils.RecordTypeAndRecordIds(
+ StepsRecord.class.getName(),
+ List.of(insertedRecords.get(0).getMetadata().getId())));
intent.putExtra(SUCCESS, true);
+ intent.putExtra(RECORD_IDS, (Serializable) recordTypeAndRecordIdsList);
} catch (Exception e) {
intent.putExtra(SUCCESS, false);
}
diff --git a/tests/cts/hostsidetests/healthconnect/HealthConnectTestHelper/src/android/healthconnect/cts/testhelper/TestHelperUtils.java b/tests/cts/hostsidetests/healthconnect/HealthConnectTestHelper/src/android/healthconnect/cts/testhelper/TestHelperUtils.java
index 00f93921..6ef33a4e 100644
--- a/tests/cts/hostsidetests/healthconnect/HealthConnectTestHelper/src/android/healthconnect/cts/testhelper/TestHelperUtils.java
+++ b/tests/cts/hostsidetests/healthconnect/HealthConnectTestHelper/src/android/healthconnect/cts/testhelper/TestHelperUtils.java
@@ -24,6 +24,7 @@ import android.health.connect.HealthConnectManager;
import android.health.connect.InsertRecordsResponse;
import android.health.connect.TimeInstantRangeFilter;
import android.health.connect.TimeRangeFilter;
+import android.health.connect.accesslog.AccessLog;
import android.health.connect.datatypes.BloodPressureRecord;
import android.health.connect.datatypes.DataOrigin;
import android.health.connect.datatypes.HeartRateRecord;
@@ -171,6 +172,32 @@ public class TestHelperUtils {
}
}
+ /** Query access logs */
+ public static List<AccessLog> queryAccessLogs(HealthConnectManager healthConnectManager)
+ throws InterruptedException {
+ AtomicReference<List<AccessLog>> response = new AtomicReference<>(new ArrayList<>());
+ CountDownLatch latch = new CountDownLatch(1);
+ assertThat(healthConnectManager).isNotNull();
+
+ healthConnectManager.queryAccessLogs(
+ Executors.newSingleThreadExecutor(),
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(List<AccessLog> accessLogs) {
+ response.set(accessLogs);
+ latch.countDown();
+ }
+
+ @Override
+ public void onError(HealthConnectException exception) {
+ latch.countDown();
+ }
+ });
+
+ assertThat(latch.await(1, TimeUnit.SECONDS)).isTrue();
+ return response.get();
+ }
+
/** Deletes the records added by the test app. */
public static void deleteAllRecordsAddedByTestApp(HealthConnectManager healthConnectManager)
throws InterruptedException {
diff --git a/tests/cts/hostsidetests/healthconnect/device/src/android/healthconnect/cts/device/HealthConnectDeviceTest.java b/tests/cts/hostsidetests/healthconnect/device/src/android/healthconnect/cts/device/HealthConnectDeviceTest.java
index 691b44ac..623998a4 100644
--- a/tests/cts/hostsidetests/healthconnect/device/src/android/healthconnect/cts/device/HealthConnectDeviceTest.java
+++ b/tests/cts/hostsidetests/healthconnect/device/src/android/healthconnect/cts/device/HealthConnectDeviceTest.java
@@ -55,18 +55,24 @@ import static com.android.compatibility.common.util.FeatureUtil.hasSystemFeature
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
import android.app.UiAutomation;
+import android.content.Context;
import android.health.connect.AggregateRecordsRequest;
import android.health.connect.AggregateRecordsResponse;
import android.health.connect.HealthConnectException;
import android.health.connect.HealthDataCategory;
import android.health.connect.HealthPermissions;
import android.health.connect.ReadRecordsRequestUsingFilters;
+import android.health.connect.ReadRecordsRequestUsingIds;
import android.health.connect.RecordIdFilter;
import android.health.connect.TimeInstantRangeFilter;
import android.health.connect.UpdateDataOriginPriorityOrderRequest;
import android.health.connect.changelog.ChangeLogsResponse;
+import android.health.connect.datatypes.AggregationType;
import android.health.connect.datatypes.DataOrigin;
+import android.health.connect.datatypes.ExerciseSessionRecord;
import android.health.connect.datatypes.HeartRateRecord;
import android.health.connect.datatypes.Metadata;
import android.health.connect.datatypes.Record;
@@ -87,9 +93,12 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
+import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
@@ -133,9 +142,33 @@ public class HealthConnectDeviceTest {
false,
"CtsHealthConnectTestAppWithDataManagePermission.apk");
+ private static final String STEPS_1000_CLIENT_ID = "client-id-1";
+ private static final String STEPS_2000_CLIENT_ID = "client-id-2";
+ private static final StepsRecord STEPS_1000 =
+ getStepsRecord(
+ /* stepCount= */ 1000,
+ /* startTime= */ Instant.now().minus(2, ChronoUnit.HOURS),
+ /* durationInHours= */ 1,
+ STEPS_1000_CLIENT_ID);
+ private static final StepsRecord STEPS_2000 =
+ getStepsRecord(
+ /* stepCount= */ 2000,
+ /* startTime= */ Instant.now().minus(4, ChronoUnit.HOURS),
+ /* durationInHours= */ 1,
+ STEPS_2000_CLIENT_ID);
+
+ private static final AggregationType<Long> WRITE_ONLY_PERM_AGGREGATION_STEPS_TOTAL =
+ STEPS_COUNT_TOTAL;
+
+ private static final AggregationType<Long> READ_PERM_AGGREGATION_EXERCISE_DURATION_TOTAL =
+ EXERCISE_DURATION_TOTAL;
+
+ private Context mContext;
+
@Before
public void setUp() {
Assume.assumeFalse(hasSystemFeature(AUTOMOTIVE_FEATURE));
+ mContext = ApplicationProvider.getApplicationContext();
}
@After
@@ -193,16 +226,18 @@ public class HealthConnectDeviceTest {
(List<TestUtils.RecordTypeAndRecordIds>) bundle.getSerializable(RECORD_IDS);
for (TestUtils.RecordTypeAndRecordIds recordTypeAndRecordIds : listOfRecordIdsAndClass) {
+ Class<? extends Record> recordType =
+ (Class<? extends Record>) Class.forName(recordTypeAndRecordIds.getRecordType());
+ if (!recordType.equals(ExerciseSessionRecord.class)) {
+ // skip other record types since we don't have read permissions for these.
+ continue;
+ }
List<Record> records =
(List<Record>)
readRecords(
- new ReadRecordsRequestUsingFilters.Builder<>(
- (Class<? extends Record>)
- Class.forName(
- recordTypeAndRecordIds
- .getRecordType()))
+ new ReadRecordsRequestUsingFilters.Builder<>(recordType)
.build());
-
+ assertThat(records).isNotEmpty();
for (Record record : records) {
assertThat(record.getMetadata().getDataOrigin().getPackageName())
.isEqualTo(APP_A_WITH_READ_WRITE_PERMS.getPackageName());
@@ -211,7 +246,7 @@ public class HealthConnectDeviceTest {
}
@Test
- public void testAppWithWritePermsOnlyCanReadItsOwnEntry() throws Exception {
+ public void testAppWithWritePermsOnly_readOwnData_success() throws Exception {
Bundle bundle = insertRecordAs(APP_WITH_WRITE_PERMS_ONLY);
assertThat(bundle.getBoolean(SUCCESS)).isTrue();
@@ -223,12 +258,17 @@ public class HealthConnectDeviceTest {
recordClassesToRead.add(recordTypeAndRecordIds.getRecordType());
}
- bundle = readRecordsAs(APP_WITH_WRITE_PERMS_ONLY, recordClassesToRead);
- assertThat(bundle.getInt(READ_RECORDS_SIZE)).isNotEqualTo(0);
+ bundle =
+ readRecordsAs(
+ APP_WITH_WRITE_PERMS_ONLY,
+ recordClassesToRead,
+ /* dataOriginFilterPackageNames= */ Optional.of(
+ List.of(APP_WITH_WRITE_PERMS_ONLY.getPackageName())));
+ assertThat(bundle.getInt(READ_RECORDS_SIZE)).isEqualTo(listOfRecordIdsAndClass.size());
}
@Test
- public void testAppWithWritePermsOnlyCantReadAnotherAppEntry() throws Exception {
+ public void testAppWithWritePermsOnly_readDataFromAllApps_throwsError() throws Exception {
Bundle bundle = insertRecordAs(APP_A_WITH_READ_WRITE_PERMS);
assertThat(bundle.getBoolean(SUCCESS)).isTrue();
@@ -240,8 +280,204 @@ public class HealthConnectDeviceTest {
recordClassesToRead.add(recordTypeAndRecordIds.getRecordType());
}
- bundle = readRecordsAs(APP_WITH_WRITE_PERMS_ONLY, recordClassesToRead);
- assertThat(bundle.getInt(READ_RECORDS_SIZE)).isEqualTo(0);
+ try {
+ bundle =
+ readRecordsAs(
+ APP_WITH_WRITE_PERMS_ONLY,
+ recordClassesToRead,
+ // empty data implies all data is requested
+ /* dataOriginFilterPackageNames= */ Optional.of(List.of()));
+ fail("Expected to fail with HealthConnectException but didn't");
+ } catch (Exception e) {
+ assertThat(e).isInstanceOf(HealthConnectException.class);
+ assertThat(((HealthConnectException) e).getErrorCode())
+ .isEqualTo(HealthConnectException.ERROR_SECURITY);
+ }
+ }
+
+ @Test
+ public void testAppWithWritePermsOnly_readDataFromOtherApps_throwsError() throws Exception {
+ Bundle bundle = insertRecordAs(APP_A_WITH_READ_WRITE_PERMS);
+ assertThat(bundle.getBoolean(SUCCESS)).isTrue();
+
+ List<TestUtils.RecordTypeAndRecordIds> listOfRecordIdsAndClass =
+ (List<TestUtils.RecordTypeAndRecordIds>) bundle.getSerializable(RECORD_IDS);
+
+ ArrayList<String> recordClassesToRead = new ArrayList<>();
+ for (TestUtils.RecordTypeAndRecordIds recordTypeAndRecordIds : listOfRecordIdsAndClass) {
+ recordClassesToRead.add(recordTypeAndRecordIds.getRecordType());
+ }
+
+ try {
+ readRecordsAs(
+ APP_WITH_WRITE_PERMS_ONLY,
+ recordClassesToRead,
+ /* dataOriginFilterPackageNames= */ Optional.of(
+ List.of(
+ APP_WITH_WRITE_PERMS_ONLY.getPackageName(),
+ APP_A_WITH_READ_WRITE_PERMS.getPackageName())));
+ fail("Expected to fail with HealthConnectException but didn't");
+ } catch (Exception e) {
+ assertThat(e).isInstanceOf(HealthConnectException.class);
+ assertThat(((HealthConnectException) e).getErrorCode())
+ .isEqualTo(HealthConnectException.ERROR_SECURITY);
+ }
+ }
+
+ @Test
+ public void testAppWithWritePermsOnly_readDataByIdForOwnApp_success() throws Exception {
+ Bundle bundle =
+ insertStepsRecordAs(APP_A_WITH_READ_WRITE_PERMS, "01:00 PM", "03:00 PM", 1000);
+ assertThat(bundle.getBoolean(SUCCESS)).isTrue();
+ List<Record> writtenRecords = TestUtils.insertRecords(List.of(STEPS_1000, STEPS_2000));
+ List<String> recordIds =
+ writtenRecords.stream()
+ .map(record -> record.getMetadata().getId())
+ .collect(Collectors.toList());
+
+ List<Record> readRecords =
+ TestUtils.readRecords(
+ new ReadRecordsRequestUsingIds.Builder(StepsRecord.class)
+ .addId(recordIds.get(0))
+ .addId(recordIds.get(1))
+ .build());
+
+ assertThat(
+ readRecords.stream()
+ .map(record -> record.getMetadata().getClientRecordId())
+ .collect(Collectors.toList()))
+ .containsExactly(STEPS_1000_CLIENT_ID, STEPS_2000_CLIENT_ID);
+ }
+
+ // TODO(b/309778116): Consider throwing an error in this case.
+ @Test
+ public void testAppWithWritePermsOnly_readDataByIdForOtherApps_filtersOutOtherAppData()
+ throws Exception {
+ Bundle bundle =
+ insertStepsRecordAs(APP_A_WITH_READ_WRITE_PERMS, "01:00 PM", "03:00 PM", 1000);
+ assertThat(bundle.getBoolean(SUCCESS)).isTrue();
+ String otherAppRecordId =
+ ((List<TestUtils.RecordTypeAndRecordIds>) bundle.getSerializable(RECORD_IDS))
+ .get(0)
+ .getRecordIds()
+ .get(0);
+ List<Record> writtenRecords = TestUtils.insertRecords(List.of(STEPS_1000, STEPS_2000));
+ List<String> recordIds =
+ writtenRecords.stream()
+ .map(record -> record.getMetadata().getId())
+ .collect(Collectors.toList());
+
+ List<Record> readRecords =
+ TestUtils.readRecords(
+ new ReadRecordsRequestUsingIds.Builder(StepsRecord.class)
+ .addId(recordIds.get(0))
+ .addId(recordIds.get(1))
+ .addId(otherAppRecordId)
+ .build());
+
+ assertThat(
+ readRecords.stream()
+ .map(record -> record.getMetadata().getClientRecordId())
+ .collect(Collectors.toList()))
+ .containsExactly(STEPS_1000_CLIENT_ID, STEPS_2000_CLIENT_ID);
+ }
+
+ @Test
+ public void testAggregateRecords_onlyWritePermissions_requestsOwnDataOnly_succeeds()
+ throws InterruptedException {
+ AggregateRecordsResponse<Long> response =
+ TestUtils.getAggregateResponse(
+ new AggregateRecordsRequest.Builder<Long>(
+ new TimeInstantRangeFilter.Builder()
+ .setStartTime(Instant.ofEpochMilli(0))
+ .setEndTime(Instant.now().plus(1, ChronoUnit.DAYS))
+ .build())
+ .addAggregationType(WRITE_ONLY_PERM_AGGREGATION_STEPS_TOTAL)
+ .addDataOriginsFilter(
+ new DataOrigin.Builder()
+ .setPackageName(mContext.getPackageName())
+ .build())
+ .build(),
+ /* recordsToInsert= */ List.of(STEPS_1000, STEPS_2000));
+ assertThat(response.get(WRITE_ONLY_PERM_AGGREGATION_STEPS_TOTAL))
+ .isEqualTo(STEPS_1000.getCount() + STEPS_2000.getCount());
+ }
+
+ @Test
+ public void testAggregateRecords_onlyWritePermissions_requestsOthersData_throwsHcException()
+ throws InterruptedException {
+ try {
+ TestUtils.getAggregateResponse(
+ new AggregateRecordsRequest.Builder<Long>(
+ new TimeInstantRangeFilter.Builder()
+ .setStartTime(Instant.ofEpochMilli(0))
+ .setEndTime(Instant.now().plus(1, ChronoUnit.DAYS))
+ .build())
+ .addAggregationType(WRITE_ONLY_PERM_AGGREGATION_STEPS_TOTAL)
+ .addDataOriginsFilter(
+ new DataOrigin.Builder()
+ .setPackageName(mContext.getPackageName())
+ .build())
+ .addDataOriginsFilter(
+ new DataOrigin.Builder()
+ .setPackageName(
+ APP_B_WITH_READ_WRITE_PERMS.getPackageName())
+ .build())
+ .build(),
+ /* recordsToInsert= */ List.of(STEPS_1000, STEPS_2000));
+ fail("Expected to fail with HealthConnectException but didn't");
+ } catch (HealthConnectException e) {
+ assertThat(e.getErrorCode()).isEqualTo(HealthConnectException.ERROR_SECURITY);
+ }
+ }
+
+ @Test
+ public void testAggregateRecords_onlyWritePermissions_allDataRequested_throwsHcException()
+ throws InterruptedException {
+ try {
+ TestUtils.getAggregateResponse(
+ new AggregateRecordsRequest.Builder<Long>(
+ new TimeInstantRangeFilter.Builder()
+ .setStartTime(Instant.ofEpochMilli(0))
+ .setEndTime(Instant.now().plus(1, ChronoUnit.DAYS))
+ .build())
+ .addAggregationType(WRITE_ONLY_PERM_AGGREGATION_STEPS_TOTAL)
+ .build(),
+ /* recordsToInsert= */ List.of(STEPS_1000, STEPS_2000));
+ fail("Expected to fail with HealthConnectException but didn't");
+ } catch (HealthConnectException e) {
+ assertThat(e.getErrorCode()).isEqualTo(HealthConnectException.ERROR_SECURITY);
+ }
+ }
+
+ @Test
+ public void
+ testAggregateRecords_someReadAndWritePermissions_requestsOthersData_throwsHcException()
+ throws InterruptedException {
+ try {
+ TestUtils.getAggregateResponse(
+ new AggregateRecordsRequest.Builder<Long>(
+ new TimeInstantRangeFilter.Builder()
+ .setStartTime(Instant.ofEpochMilli(0))
+ .setEndTime(Instant.now().plus(1, ChronoUnit.DAYS))
+ .build())
+ .addAggregationType(WRITE_ONLY_PERM_AGGREGATION_STEPS_TOTAL)
+ .addAggregationType(READ_PERM_AGGREGATION_EXERCISE_DURATION_TOTAL)
+ .addDataOriginsFilter(
+ new DataOrigin.Builder()
+ .setPackageName(mContext.getPackageName())
+ .build())
+ .addDataOriginsFilter(
+ new DataOrigin.Builder()
+ .setPackageName(
+ APP_B_WITH_READ_WRITE_PERMS.getPackageName())
+ .build())
+ .build(),
+ /* recordsToInsert= */ List.of(STEPS_1000, STEPS_2000));
+ fail("Expected to fail with HealthConnectException but didn't");
+ } catch (HealthConnectException e) {
+ assertThat(e.getErrorCode()).isEqualTo(HealthConnectException.ERROR_SECURITY);
+ }
}
@Test
@@ -871,4 +1107,14 @@ public class HealthConnectDeviceTest {
grantPermission(APP_B_WITH_READ_WRITE_PERMS.getPackageName(), perm);
}
}
+
+ private static StepsRecord getStepsRecord(
+ int stepCount, Instant startTime, int durationInHours, String clientId) {
+ return new StepsRecord.Builder(
+ new Metadata.Builder().setClientRecordId(clientId).build(),
+ startTime,
+ startTime.plus(durationInHours, ChronoUnit.HOURS),
+ stepCount)
+ .build();
+ }
}
diff --git a/tests/cts/hostsidetests/healthconnect/host/src/android/healthconnect/cts/dailyjob/DailyDeleteAccessLogTest.java b/tests/cts/hostsidetests/healthconnect/host/src/android/healthconnect/cts/dailyjob/DailyDeleteAccessLogTest.java
new file mode 100644
index 00000000..7d29f050
--- /dev/null
+++ b/tests/cts/hostsidetests/healthconnect/host/src/android/healthconnect/cts/dailyjob/DailyDeleteAccessLogTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.healthconnect.cts.dailyjob;
+
+import static android.healthconnect.cts.HostSideTestUtil.DAILY_LOG_TESTS_ACTIVITY;
+import static android.healthconnect.cts.HostSideTestUtil.clearData;
+import static android.healthconnect.cts.HostSideTestUtil.increaseDeviceTimeByDays;
+import static android.healthconnect.cts.HostSideTestUtil.isHardwareSupported;
+import static android.healthconnect.cts.HostSideTestUtil.resetTime;
+import static android.healthconnect.cts.HostSideTestUtil.triggerDailyJob;
+import static android.healthconnect.cts.HostSideTestUtil.triggerTestInTestApp;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import java.time.Instant;
+
+public class DailyDeleteAccessLogTest extends DeviceTestCase implements IBuildReceiver {
+ private IBuildInfo mCtsBuild;
+ private Instant mTestStartTime;
+ private Instant mDeviceStartTime;
+
+ @Override
+ protected void setUp() throws Exception {
+ if (!isHardwareSupported(getDevice())) {
+ return;
+ }
+ super.setUp();
+ mTestStartTime = Instant.now();
+ mDeviceStartTime = Instant.ofEpochMilli(getDevice().getDeviceDate());
+ assertThat(mCtsBuild).isNotNull();
+ clearData(getDevice());
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ clearData(getDevice());
+ resetTime(getDevice(), mTestStartTime, mDeviceStartTime);
+ super.tearDown();
+ }
+
+ @Override
+ public void setBuild(IBuildInfo buildInfo) {
+ mCtsBuild = buildInfo;
+ }
+
+ public void testAccessLogsAreDeleted() throws Exception {
+ if (!isHardwareSupported(getDevice())) {
+ return;
+ }
+
+ triggerTestInTestApp(getDevice(), DAILY_LOG_TESTS_ACTIVITY, "testInsertRecordsSucceed");
+ triggerTestInTestApp(
+ getDevice(), DAILY_LOG_TESTS_ACTIVITY, "testHealthConnectAccessLogsEqualsOne");
+
+ increaseDeviceTimeByDays(getDevice(), 5);
+ triggerTestInTestApp(getDevice(), DAILY_LOG_TESTS_ACTIVITY, "testInsertRecordsSucceed");
+ triggerTestInTestApp(
+ getDevice(), DAILY_LOG_TESTS_ACTIVITY, "testHealthConnectAccessLogsEqualsTwo");
+
+ // Only the first access log should have been deleted after 5 days.
+ increaseDeviceTimeByDays(getDevice(), 5);
+ triggerDailyJob(getDevice());
+ triggerTestInTestApp(
+ getDevice(), DAILY_LOG_TESTS_ACTIVITY, "testHealthConnectAccessLogsEqualsOne");
+
+ // The other access log should also be deleted after 5 days.
+ increaseDeviceTimeByDays(getDevice(), 5);
+ triggerDailyJob(getDevice());
+ triggerTestInTestApp(
+ getDevice(), DAILY_LOG_TESTS_ACTIVITY, "testHealthConnectAccessLogsEqualsZero");
+ }
+}
diff --git a/tests/cts/hostsidetests/healthconnect/host/src/android/healthconnect/cts/logging/HealthConnectDailyLogsStatsTests.java b/tests/cts/hostsidetests/healthconnect/host/src/android/healthconnect/cts/logging/HealthConnectDailyLogsStatsTests.java
index b27af1e1..c2c26a41 100644
--- a/tests/cts/hostsidetests/healthconnect/host/src/android/healthconnect/cts/logging/HealthConnectDailyLogsStatsTests.java
+++ b/tests/cts/hostsidetests/healthconnect/host/src/android/healthconnect/cts/logging/HealthConnectDailyLogsStatsTests.java
@@ -16,7 +16,7 @@
package android.healthconnect.cts.logging;
-import static android.healthconnect.cts.logging.HostSideTestsUtils.isHardwareSupported;
+import static android.healthconnect.cts.HostSideTestUtil.isHardwareSupported;
import static com.google.common.truth.Truth.assertThat;
@@ -51,6 +51,9 @@ public class HealthConnectDailyLogsStatsTests extends DeviceTestCase implements
private static final String DAILY_LOG_TESTS_ACTIVITY = ".DailyLogsTests";
private static final String HEALTH_CONNECT_SERVICE_LOG_TESTS_ACTIVITY =
".HealthConnectServiceLogsTests";
+ public static final String ENABLE_RATE_LIMITER_FLAG = "enable_rate_limiter";
+ public static final String NAMESPACE_HEALTH_FITNESS = "health_fitness";
+ private String mRateLimiterFeatureFlagDefaultValue;
private IBuildInfo mCtsBuild;
private Instant mTestStartTime;
private Instant mTestStartTimeOnDevice;
@@ -63,6 +66,8 @@ public class HealthConnectDailyLogsStatsTests extends DeviceTestCase implements
super.setUp();
assertThat(mCtsBuild).isNotNull();
assertThat(isHardwareSupported(getDevice())).isTrue();
+ // TODO(b/313055175): Do not disable rate limiting once b/300238889 is resolved.
+ setupRateLimitingFeatureFlag();
mTestStartTime = Instant.now();
mTestStartTimeOnDevice = Instant.ofEpochMilli(getDevice().getDeviceDate());
ConfigUtils.removeConfig(getDevice());
@@ -74,6 +79,8 @@ public class HealthConnectDailyLogsStatsTests extends DeviceTestCase implements
@Override
protected void tearDown() throws Exception {
+ // TODO(b/313055175): Do not disable rate limiting once b/300238889 is resolved.
+ restoreRateLimitingFeatureFlag();
ConfigUtils.removeConfig(getDevice());
ReportUtils.clearReports(getDevice());
clearData();
@@ -115,8 +122,7 @@ public class HealthConnectDailyLogsStatsTests extends DeviceTestCase implements
new int[] {ApiExtensionAtoms.HEALTH_CONNECT_STORAGE_STATS_FIELD_NUMBER});
List<StatsLog.EventMetricData> data =
- getEventMetricDataList(
- /* testName= */ "testHealthConnectDatabaseStats", NUMBER_OF_RETRIES);
+ getEventMetricDataList("testInsertRecordsSucceed", NUMBER_OF_RETRIES);
assertThat(data.size()).isAtLeast(1);
HealthConnectStorageStats atom =
data.get(0).getAtom().getExtension(ApiExtensionAtoms.healthConnectStorageStats);
@@ -353,4 +359,28 @@ public class HealthConnectDailyLogsStatsTests extends DeviceTestCase implements
+ " --user_should_confirm_time false --elapsed_realtime 0");
getDevice().executeShellCommand("am broadcast -a android.intent.action.TIME_SET");
}
+
+ private void setupRateLimitingFeatureFlag() throws Exception {
+ // Store default value of the flag on device for teardown.
+ mRateLimiterFeatureFlagDefaultValue =
+ DeviceUtils.getDeviceConfigFeature(
+ getDevice(), NAMESPACE_HEALTH_FITNESS, ENABLE_RATE_LIMITER_FLAG);
+
+ DeviceUtils.putDeviceConfigFeature(
+ getDevice(), NAMESPACE_HEALTH_FITNESS, ENABLE_RATE_LIMITER_FLAG, "false");
+ }
+
+ private void restoreRateLimitingFeatureFlag() throws Exception {
+ if (mRateLimiterFeatureFlagDefaultValue == null
+ || mRateLimiterFeatureFlagDefaultValue.equals("null")) {
+ DeviceUtils.deleteDeviceConfigFeature(
+ getDevice(), NAMESPACE_HEALTH_FITNESS, ENABLE_RATE_LIMITER_FLAG);
+ } else {
+ DeviceUtils.putDeviceConfigFeature(
+ getDevice(),
+ NAMESPACE_HEALTH_FITNESS,
+ ENABLE_RATE_LIMITER_FLAG,
+ mRateLimiterFeatureFlagDefaultValue);
+ }
+ }
}
diff --git a/tests/cts/hostsidetests/healthconnect/host/src/android/healthconnect/cts/logging/HealthConnectServiceStatsTests.java b/tests/cts/hostsidetests/healthconnect/host/src/android/healthconnect/cts/logging/HealthConnectServiceStatsTests.java
index 59b51e63..12846ae8 100644
--- a/tests/cts/hostsidetests/healthconnect/host/src/android/healthconnect/cts/logging/HealthConnectServiceStatsTests.java
+++ b/tests/cts/hostsidetests/healthconnect/host/src/android/healthconnect/cts/logging/HealthConnectServiceStatsTests.java
@@ -16,7 +16,7 @@
package android.healthconnect.cts.logging;
-import static android.healthconnect.cts.logging.HostSideTestsUtils.isHardwareSupported;
+import static android.healthconnect.cts.HostSideTestUtil.isHardwareSupported;
import static com.google.common.truth.Truth.assertThat;
diff --git a/tests/cts/hostsidetests/healthconnect/host/src/android/healthconnect/cts/logging/HealthConnectUiLogsTests.kt b/tests/cts/hostsidetests/healthconnect/host/src/android/healthconnect/cts/logging/HealthConnectUiLogsTests.kt
index 9c210844..22091373 100644
--- a/tests/cts/hostsidetests/healthconnect/host/src/android/healthconnect/cts/logging/HealthConnectUiLogsTests.kt
+++ b/tests/cts/hostsidetests/healthconnect/host/src/android/healthconnect/cts/logging/HealthConnectUiLogsTests.kt
@@ -20,7 +20,7 @@ import android.cts.statsdatom.lib.AtomTestUtils
import android.cts.statsdatom.lib.ConfigUtils
import android.cts.statsdatom.lib.DeviceUtils
import android.cts.statsdatom.lib.ReportUtils
-import android.healthconnect.cts.logging.HostSideTestsUtils.isHardwareSupported
+import android.healthconnect.cts.HostSideTestUtil.isHardwareSupported
import android.healthfitness.ui.ElementId
import android.healthfitness.ui.PageId
import com.android.os.StatsLog
diff --git a/tests/cts/hostsidetests/healthconnect/host/src/android/healthconnect/cts/logging/HostSideTestsUtils.java b/tests/cts/hostsidetests/healthconnect/host/src/android/healthconnect/cts/logging/HostSideTestsUtils.java
deleted file mode 100644
index 2c098966..00000000
--- a/tests/cts/hostsidetests/healthconnect/host/src/android/healthconnect/cts/logging/HostSideTestsUtils.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.healthconnect.cts.logging;
-
-import android.cts.statsdatom.lib.DeviceUtils;
-
-import com.android.tradefed.device.ITestDevice;
-
-class HostSideTestsUtils {
-
- private static final String FEATURE_TV = "android.hardware.type.television";
- private static final String FEATURE_EMBEDDED = "android.hardware.type.embedded";
- private static final String FEATURE_WATCH = "android.hardware.type.watch";
- private static final String FEATURE_LEANBACK = "android.software.leanback";
- private static final String FEATURE_AUTOMOTIVE = "android.hardware.type.automotive";
-
- public static boolean isHardwareSupported(ITestDevice device) {
- // These UI tests are not optimised for Watches, TVs, Auto;
- // IoT devices do not have a UI to run these UI tests
- try {
- return !DeviceUtils.hasFeature(device, FEATURE_TV)
- && !DeviceUtils.hasFeature(device, FEATURE_EMBEDDED)
- && !DeviceUtils.hasFeature(device, FEATURE_WATCH)
- && !DeviceUtils.hasFeature(device, FEATURE_LEANBACK)
- && !DeviceUtils.hasFeature(device, FEATURE_AUTOMOTIVE);
- } catch (Exception e) {
- return false;
- }
- }
-}
diff --git a/tests/cts/hostsidetests/healthconnect/host/src/util/HostSideTestUtil.java b/tests/cts/hostsidetests/healthconnect/host/src/util/HostSideTestUtil.java
new file mode 100644
index 00000000..b1e1ea3d
--- /dev/null
+++ b/tests/cts/hostsidetests/healthconnect/host/src/util/HostSideTestUtil.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.healthconnect.cts;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.RunUtil;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Date;
+
+public class HostSideTestUtil {
+
+ public static final String TEST_APP_PKG_NAME = "android.healthconnect.cts.testhelper";
+ public static final String DAILY_LOG_TESTS_ACTIVITY = ".DailyLogsTests";
+ private static final int NUMBER_OF_RETRIES = 10;
+
+ private static final String FEATURE_TV = "android.hardware.type.television";
+ private static final String FEATURE_EMBEDDED = "android.hardware.type.embedded";
+ private static final String FEATURE_WATCH = "android.hardware.type.watch";
+ private static final String FEATURE_LEANBACK = "android.software.leanback";
+ private static final String FEATURE_AUTOMOTIVE = "android.hardware.type.automotive";
+
+ /** Clears all data on the device, including access logs. */
+ public static void clearData(ITestDevice device) throws Exception {
+ triggerTestInTestApp(device, DAILY_LOG_TESTS_ACTIVITY, "deleteAllRecordsAddedForTest");
+ // Next two lines will delete newly added Access Logs as all access logs over 7 days are
+ // deleted by the AutoDeleteService which is run by the daily job.
+ increaseDeviceTimeByDays(device, 10);
+ triggerDailyJob(device);
+ }
+
+ /** Triggers a test on the device with the given className and testName. */
+ public static void triggerTestInTestApp(ITestDevice device, String className, String testName)
+ throws Exception {
+
+ if (testName != null) {
+ DeviceUtils.runDeviceTests(device, TEST_APP_PKG_NAME, className, testName);
+ }
+ }
+
+ /** Increases the device clock by the given numberOfDays. */
+ public static void increaseDeviceTimeByDays(ITestDevice device, int numberOfDays)
+ throws DeviceNotAvailableException {
+ Instant deviceDate = Instant.ofEpochMilli(device.getDeviceDate());
+
+ device.setDate(Date.from(deviceDate.plus(numberOfDays, ChronoUnit.DAYS)));
+ device.executeShellCommand(
+ "cmd time_detector set_time_state_for_tests --unix_epoch_time "
+ + deviceDate.plus(numberOfDays, ChronoUnit.DAYS).toEpochMilli()
+ + " --user_should_confirm_time false --elapsed_realtime 0");
+
+ device.executeShellCommand("am broadcast -a android.intent.action.TIME_SET");
+ }
+
+ /** Reset device time to revert all changes made during the test. */
+ public static void resetTime(ITestDevice device, Instant testStartTime, Instant deviceStartTime)
+ throws DeviceNotAvailableException {
+ long timeDiff = Duration.between(testStartTime, Instant.now()).toMillis();
+
+ device.executeShellCommand(
+ "cmd time_detector set_time_state_for_tests --unix_epoch_time "
+ + deviceStartTime.plusMillis(timeDiff).toEpochMilli()
+ + " --user_should_confirm_time false --elapsed_realtime 0");
+ device.executeShellCommand("am broadcast -a android.intent.action.TIME_SET");
+ }
+
+ /** Triggers the Health Connect daily job. */
+ public static void triggerDailyJob(ITestDevice device) throws Exception {
+
+ // There are multiple instances of HealthConnectDailyService. This command finds the one
+ // that needs to be triggered for this test using the job param 'hc_daily_job'.
+ String output =
+ device.executeShellCommand(
+ "dumpsys jobscheduler | grep -m1 -A0 -B10 \"hc_daily_job\"");
+ int indexOfStart = output.indexOf("/") + 1;
+ String jobId = output.substring(indexOfStart, output.indexOf(":", indexOfStart));
+ String jobExecutionCommand =
+ "cmd jobscheduler run --namespace HEALTH_CONNECT_DAILY_JOB -f android " + jobId;
+
+ executeJob(device, jobExecutionCommand, NUMBER_OF_RETRIES);
+ RunUtil.getDefault().sleep(AtomTestUtils.WAIT_TIME_LONG);
+ }
+
+ private static void executeJob(ITestDevice device, String jobExecutionCommand, int retry)
+ throws DeviceNotAvailableException, RuntimeException {
+ if (retry == 0) {
+ throw new RuntimeException("Could not execute job");
+ }
+ if (device.executeShellV2Command(jobExecutionCommand).getStatus()
+ != CommandStatus.SUCCESS) {
+ executeJob(device, jobExecutionCommand, retry - 1);
+ }
+ }
+
+ /** Checks if the hardware supports Health Connect. */
+ public static boolean isHardwareSupported(ITestDevice device) {
+ // These UI tests are not optimised for Watches, TVs, Auto;
+ // IoT devices do not have a UI to run these UI tests
+ try {
+ return !DeviceUtils.hasFeature(device, FEATURE_TV)
+ && !DeviceUtils.hasFeature(device, FEATURE_EMBEDDED)
+ && !DeviceUtils.hasFeature(device, FEATURE_WATCH)
+ && !DeviceUtils.hasFeature(device, FEATURE_LEANBACK)
+ && !DeviceUtils.hasFeature(device, FEATURE_AUTOMOTIVE);
+ } catch (Exception e) {
+ return false;
+ }
+ }
+}
diff --git a/tests/cts/hostsidetests/healthconnect/libs/HealthConnectTestLib/src/android/healthconnect/cts/lib/MultiAppTestUtils.java b/tests/cts/hostsidetests/healthconnect/libs/HealthConnectTestLib/src/android/healthconnect/cts/lib/MultiAppTestUtils.java
index 51903f3a..fd0b3a2c 100644
--- a/tests/cts/hostsidetests/healthconnect/libs/HealthConnectTestLib/src/android/healthconnect/cts/lib/MultiAppTestUtils.java
+++ b/tests/cts/hostsidetests/healthconnect/libs/HealthConnectTestLib/src/android/healthconnect/cts/lib/MultiAppTestUtils.java
@@ -31,6 +31,7 @@ import com.android.cts.install.lib.TestApp;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
+import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -47,6 +48,9 @@ public class MultiAppTestUtils {
public static final String READ_RECORDS_SIZE = "android.healthconnect.cts.readRecordsNumber";
public static final String READ_USING_DATA_ORIGIN_FILTERS =
"android.healthconnect.cts.readUsingDataOriginFilters";
+
+ public static final String DATA_ORIGIN_FILTER_PACKAGE_NAMES =
+ "android.healthconnect.cts.dataOriginFilterPackageNames";
public static final String READ_RECORD_CLASS_NAME =
"android.healthconnect.cts.readRecordsClass";
public static final String READ_CHANGE_LOGS_QUERY = "android.healthconnect.cts.readChangeLogs";
@@ -125,10 +129,24 @@ public class MultiAppTestUtils {
public static Bundle readRecordsAs(TestApp testApp, ArrayList<String> recordClassesToRead)
throws Exception {
+ return readRecordsAs(
+ testApp, recordClassesToRead, /* dataOriginFilterPackageNames= */ Optional.empty());
+ }
+
+ public static Bundle readRecordsAs(
+ TestApp testApp,
+ ArrayList<String> recordClassesToRead,
+ Optional<List<String>> dataOriginFilterPackageNames)
+ throws Exception {
Bundle bundle = new Bundle();
bundle.putString(QUERY_TYPE, READ_RECORDS_QUERY);
bundle.putStringArrayList(READ_RECORD_CLASS_NAME, recordClassesToRead);
-
+ if (!dataOriginFilterPackageNames.isEmpty()) {
+ ArrayList<String> dataOrigins = new ArrayList<>();
+ dataOrigins.addAll(dataOriginFilterPackageNames.get());
+ bundle.putBoolean(READ_USING_DATA_ORIGIN_FILTERS, true);
+ bundle.putStringArrayList(DATA_ORIGIN_FILTER_PACKAGE_NAMES, dataOrigins);
+ }
return getFromTestApp(testApp, bundle);
}
diff --git a/tests/cts/src/android/healthconnect/cts/HealthConnectManagerTest.java b/tests/cts/src/android/healthconnect/cts/HealthConnectManagerTest.java
index 89532755..ae924713 100644
--- a/tests/cts/src/android/healthconnect/cts/HealthConnectManagerTest.java
+++ b/tests/cts/src/android/healthconnect/cts/HealthConnectManagerTest.java
@@ -40,10 +40,17 @@ import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
+import static java.time.ZoneOffset.UTC;
+import static java.time.temporal.ChronoUnit.DAYS;
+import static java.time.temporal.ChronoUnit.HOURS;
+import static java.time.temporal.ChronoUnit.MINUTES;
+
import android.Manifest;
import android.app.UiAutomation;
import android.content.Context;
+import android.health.connect.AggregateRecordsGroupedByDurationResponse;
import android.health.connect.AggregateRecordsRequest;
+import android.health.connect.AggregateRecordsResponse;
import android.health.connect.DeleteUsingFiltersRequest;
import android.health.connect.HealthConnectDataState;
import android.health.connect.HealthConnectException;
@@ -99,7 +106,6 @@ import java.time.Instant;
import java.time.LocalDateTime;
import java.time.Period;
import java.time.ZoneOffset;
-import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -656,7 +662,7 @@ public class HealthConnectManagerTest {
@Test
public void testReadRecords_multiplePagesSameStartTimeRecords_paginatedCorrectly()
throws Exception {
- Instant startTime = Instant.now().minus(1, ChronoUnit.DAYS);
+ Instant startTime = Instant.now().minus(1, DAYS);
insertRecords(
List.of(
@@ -712,6 +718,60 @@ public class HealthConnectManagerTest {
}
@Test
+ public void testAggregation_stepsCountTotal_acrossDST_works() throws Exception {
+ ZoneOffset utcPlusOne = ZoneOffset.ofTotalSeconds(UTC.getTotalSeconds() + 3600);
+
+ Instant midNight = Instant.now().truncatedTo(DAYS);
+
+ Instant t0057 = midNight.plus(57, MINUTES);
+ Instant t0058 = midNight.plus(58, MINUTES);
+ Instant t0059 = midNight.plus(59, MINUTES);
+ Instant t0100 = midNight.plus(1, HOURS);
+ Instant t0300 = midNight.plus(3, HOURS);
+ Instant t0400 = midNight.plus(4, HOURS);
+
+ List<Record> records =
+ Arrays.asList(
+ getStepsRecord(
+ t0057, utcPlusOne, t0058, utcPlusOne, 12), // 1:57-1:58 in test
+ // this will be removed by the workaround
+ getStepsRecord(t0059, utcPlusOne, t0100, UTC, 16), // 1:59-1:00 in test
+ getStepsRecord(t0300, UTC, t0400, UTC, 250));
+ TestUtils.insertRecords(records);
+ LocalDateTime startOfToday = LocalDateTime.now(UTC).truncatedTo(DAYS);
+ AggregateRecordsRequest<Long> aggregateRecordsRequest =
+ new AggregateRecordsRequest.Builder<Long>(
+ new LocalTimeRangeFilter.Builder()
+ .setStartTime(startOfToday.plus(1, HOURS))
+ .setEndTime(startOfToday.plus(4, HOURS))
+ .build())
+ .addAggregationType(STEPS_COUNT_TOTAL)
+ .build();
+ assertThat(aggregateRecordsRequest.getAggregationTypes()).isNotNull();
+ assertThat(aggregateRecordsRequest.getTimeRangeFilter()).isNotNull();
+ assertThat(aggregateRecordsRequest.getDataOriginsFilters()).isNotNull();
+
+ AggregateRecordsResponse<Long> aggregateResponse =
+ TestUtils.getAggregateResponse(aggregateRecordsRequest);
+ assertThat(aggregateResponse.get(STEPS_COUNT_TOTAL)).isEqualTo(262);
+
+ List<AggregateRecordsGroupedByDurationResponse<Long>> groupByResponse =
+ TestUtils.getAggregateResponseGroupByDuration(
+ aggregateRecordsRequest, Duration.ofHours(1));
+ assertThat(groupByResponse.get(0).getStartTime()).isEqualTo(midNight);
+ assertThat(groupByResponse.get(0).getEndTime()).isEqualTo(t0100);
+ assertThat(groupByResponse.get(0).getZoneOffset(STEPS_COUNT_TOTAL)).isEqualTo(utcPlusOne);
+ assertThat(groupByResponse.get(0).get(STEPS_COUNT_TOTAL)).isEqualTo(12);
+ assertThat(groupByResponse.get(1).getStartTime()).isEqualTo(t0100.plus(1, HOURS));
+ assertThat(groupByResponse.get(1).getEndTime()).isEqualTo(t0300);
+ assertThat(groupByResponse.get(1).getZoneOffset(STEPS_COUNT_TOTAL)).isNull();
+ assertThat(groupByResponse.get(2).getStartTime()).isEqualTo(t0300);
+ assertThat(groupByResponse.get(2).getEndTime()).isEqualTo(t0400);
+ assertThat(groupByResponse.get(2).getZoneOffset(STEPS_COUNT_TOTAL)).isEqualTo(UTC);
+ assertThat(groupByResponse.get(2).get(STEPS_COUNT_TOTAL)).isEqualTo(250);
+ }
+
+ @Test
public void testAutoDeleteApis() throws InterruptedException {
UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
@@ -1652,7 +1712,7 @@ public class HealthConnectManagerTest {
AggregateRecordsRequest<Long> aggregateRecordsRequest =
new AggregateRecordsRequest.Builder<Long>(
new TimeInstantRangeFilter.Builder()
- .setStartTime(Instant.now().minus(3, ChronoUnit.DAYS))
+ .setStartTime(Instant.now().minus(3, DAYS))
.setEndTime(Instant.now())
.build())
.addAggregationType(STEPS_COUNT_TOTAL)
@@ -1682,9 +1742,8 @@ public class HealthConnectManagerTest {
TestUtils.getAggregateResponseGroupByPeriod(
new AggregateRecordsRequest.Builder<Long>(
new LocalTimeRangeFilter.Builder()
- .setStartTime(
- LocalDateTime.now(ZoneOffset.UTC).minusDays(2))
- .setEndTime(LocalDateTime.now(ZoneOffset.UTC))
+ .setStartTime(LocalDateTime.now(UTC).minusDays(2))
+ .setEndTime(LocalDateTime.now(UTC))
.build())
.addAggregationType(STEPS_COUNT_TOTAL)
.build(),
@@ -1895,7 +1954,7 @@ public class HealthConnectManagerTest {
.anyMatch(list -> !list.isEmpty());
}
- private void deleteAllStagedRemoteData()
+ private static void deleteAllStagedRemoteData()
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
try {
Context context = ApplicationProvider.getApplicationContext();
@@ -1916,7 +1975,7 @@ public class HealthConnectManagerTest {
}
}
- private void verifyRecordTypeResponse(
+ private static void verifyRecordTypeResponse(
Map<Class<? extends Record>, RecordTypeInfoResponse> responses,
HashMap<Class<? extends Record>, TestUtils.RecordTypeInfoTestResponse>
expectedResponse) {
@@ -1940,14 +1999,14 @@ public class HealthConnectManagerTest {
});
}
- private List<Record> getTestRecords() {
+ private static List<Record> getTestRecords() {
return Arrays.asList(
getStepsRecord(/*clientRecordId=*/ null, /*packageName=*/ ""),
getHeartRateRecord(),
getBasalMetabolicRateRecord());
}
- private Record setTestRecordId(Record record, String id) {
+ private static Record setTestRecordId(Record record, String id) {
Metadata metadata = record.getMetadata();
Metadata metadataWithId =
new Metadata.Builder()
@@ -2018,7 +2077,7 @@ public class HealthConnectManagerTest {
return readRecords;
}
- private StepsRecord getStepsRecord(String clientRecordId, String packageName) {
+ private static StepsRecord getStepsRecord(String clientRecordId, String packageName) {
return getStepsRecord(
clientRecordId,
packageName,
@@ -2027,7 +2086,7 @@ public class HealthConnectManagerTest {
Instant.now().plusMillis(1000));
}
- private StepsRecord getStepsRecord(
+ private static StepsRecord getStepsRecord(
String clientRecordId,
String packageName,
int count,
@@ -2044,7 +2103,24 @@ public class HealthConnectManagerTest {
.build();
}
- private HeartRateRecord getHeartRateRecord() {
+ private static StepsRecord getStepsRecord(
+ Instant startTime,
+ ZoneOffset startOffset,
+ Instant endTime,
+ ZoneOffset endOffset,
+ int count) {
+ StepsRecord.Builder builder =
+ new StepsRecord.Builder(new Metadata.Builder().build(), startTime, endTime, count);
+ if (startOffset != null) {
+ builder.setStartZoneOffset(startOffset);
+ }
+ if (endOffset != null) {
+ builder.setEndZoneOffset(endOffset);
+ }
+ return builder.build();
+ }
+
+ private static HeartRateRecord getHeartRateRecord() {
HeartRateRecord.HeartRateSample heartRateSample =
new HeartRateRecord.HeartRateSample(72, Instant.now().plusMillis(100));
ArrayList<HeartRateRecord.HeartRateSample> heartRateSamples = new ArrayList<>();
@@ -2062,12 +2138,12 @@ public class HealthConnectManagerTest {
.build();
}
- private BasalMetabolicRateRecord getBasalMetabolicRateRecord() {
+ private static BasalMetabolicRateRecord getBasalMetabolicRateRecord() {
return getBasalMetabolicRateRecord(
/*clientRecordId=*/ null, /*bmr=*/ Power.fromWatts(100.0), Instant.now());
}
- private BasalMetabolicRateRecord getBasalMetabolicRateRecord(
+ private static BasalMetabolicRateRecord getBasalMetabolicRateRecord(
String clientRecordId, Power bmr, Instant time) {
Device device = getPhoneDevice();
DataOrigin dataOrigin = getDataOrigin();
@@ -2079,7 +2155,7 @@ public class HealthConnectManagerTest {
return new BasalMetabolicRateRecord.Builder(testMetadataBuilder.build(), time, bmr).build();
}
- private HydrationRecord getHydrationRecord(
+ private static HydrationRecord getHydrationRecord(
String clientRecordId, Instant startTime, Instant endTime, Volume volume) {
Device device = getPhoneDevice();
DataOrigin dataOrigin = getDataOrigin();
@@ -2092,7 +2168,7 @@ public class HealthConnectManagerTest {
.build();
}
- private NutritionRecord getNutritionRecord(
+ private static NutritionRecord getNutritionRecord(
String clientRecordId, Instant startTime, Instant endTime, Mass protein) {
Device device = getPhoneDevice();
DataOrigin dataOrigin = getDataOrigin();
diff --git a/tests/cts/src/android/healthconnect/cts/SharedMemoryTest.java b/tests/cts/src/android/healthconnect/cts/SharedMemoryTest.java
new file mode 100644
index 00000000..04202f0f
--- /dev/null
+++ b/tests/cts/src/android/healthconnect/cts/SharedMemoryTest.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.healthconnect.cts;
+
+import static android.healthconnect.cts.utils.TestUtils.deleteAllStagedRemoteData;
+import static android.healthconnect.cts.utils.TestUtils.deleteRecords;
+import static android.healthconnect.cts.utils.TestUtils.insertRecords;
+import static android.healthconnect.cts.utils.TestUtils.readAllRecords;
+import static android.healthconnect.cts.utils.TestUtils.readRecords;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static java.util.Comparator.comparing;
+
+import android.health.connect.ReadRecordsRequestUsingFilters;
+import android.health.connect.changelog.ChangeLogTokenRequest;
+import android.health.connect.changelog.ChangeLogsRequest;
+import android.health.connect.changelog.ChangeLogsResponse;
+import android.health.connect.changelog.ChangeLogsResponse.DeletedLog;
+import android.health.connect.datatypes.DataOrigin;
+import android.health.connect.datatypes.HeightRecord;
+import android.health.connect.datatypes.InstantRecord;
+import android.health.connect.datatypes.Metadata;
+import android.health.connect.datatypes.Record;
+import android.health.connect.datatypes.WeightRecord;
+import android.health.connect.datatypes.units.Length;
+import android.health.connect.datatypes.units.Mass;
+import android.healthconnect.cts.utils.TestUtils;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.common.truth.Correspondence;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class SharedMemoryTest {
+
+ @Before
+ public void before() {
+ deleteAllStagedRemoteData();
+ }
+
+ @After
+ public void after() {
+ deleteAllStagedRemoteData();
+ }
+
+ @Test
+ public void insertRecordsAndReadRecords_viaSharedMemory_recordsEqual() throws Exception {
+ DataOrigin dataOrigin =
+ new DataOrigin.Builder()
+ .setPackageName(getApplicationContext().getPackageName())
+ .build();
+
+ Metadata metadata = new Metadata.Builder().setDataOrigin(dataOrigin).build();
+ int recordCount = 5000;
+ List<HeightRecord> records = new ArrayList<>(recordCount);
+ Instant now = Instant.now();
+
+ for (int i = 0; i < recordCount; i++) {
+ records.add(
+ new HeightRecord.Builder(
+ metadata,
+ now.minusMillis(i),
+ Length.fromMeters(3.0 * i / recordCount))
+ .build());
+ }
+
+ insertRecords(records);
+
+ List<HeightRecord> readRecords =
+ readRecords(
+ new ReadRecordsRequestUsingFilters.Builder<>(HeightRecord.class)
+ .setPageSize(records.size())
+ .build());
+
+ assertWithMessage("Record list sizes do not match")
+ .that(readRecords.size())
+ .isEqualTo(recordCount);
+
+ readRecords.sort(comparing(InstantRecord::getTime).reversed());
+
+ for (int i = 0; i < recordCount; i++) {
+ assertThat(readRecords.get(i).getHeight()).isEqualTo(records.get(i).getHeight());
+ }
+ }
+
+ @Test
+ public void getChangeLogs_viaSharedMemory_recordsEquals() throws Exception {
+ DataOrigin dataOrigin =
+ new DataOrigin.Builder()
+ .setPackageName(getApplicationContext().getPackageName())
+ .build();
+ Metadata metadata = new Metadata.Builder().setDataOrigin(dataOrigin).build();
+
+ int recordCount = 5000;
+ List<HeightRecord> heightRecords = new ArrayList<>(recordCount);
+ List<WeightRecord> weightRecords = new ArrayList<>(recordCount);
+ Instant now = Instant.now();
+ for (int i = 0; i < recordCount; i++) {
+ Instant time = now.minusMillis(i);
+ heightRecords.add(
+ new HeightRecord.Builder(
+ metadata, time, Length.fromMeters(3.0 * i / recordCount))
+ .build());
+ weightRecords.add(
+ new WeightRecord.Builder(metadata, time, Mass.fromGrams(1000.0 * 70.0 + i * 10))
+ .build());
+ }
+
+ String changeLogToken =
+ TestUtils.getChangeLogToken(new ChangeLogTokenRequest.Builder().build()).getToken();
+ insertRecords(heightRecords);
+ heightRecords = readAllRecords(HeightRecord.class);
+ deleteRecords(heightRecords);
+ insertRecords(weightRecords);
+ weightRecords = readAllRecords(WeightRecord.class);
+
+ List<DeletedLog> deletedLogs = new ArrayList<>();
+ List<Record> upsertedRecords = new ArrayList<>();
+
+ ChangeLogsResponse changeLogsResponse =
+ TestUtils.getChangeLogs(new ChangeLogsRequest.Builder(changeLogToken).build());
+ while (true) {
+ upsertedRecords.addAll(changeLogsResponse.getUpsertedRecords());
+ deletedLogs.addAll(changeLogsResponse.getDeletedLogs());
+ if (!changeLogsResponse.hasMorePages()) {
+ break;
+ }
+ changeLogToken = changeLogsResponse.getNextChangesToken();
+ changeLogsResponse =
+ TestUtils.getChangeLogs(new ChangeLogsRequest.Builder(changeLogToken).build());
+ }
+
+ assertThat(deletedLogs).hasSize(recordCount);
+ assertThat(upsertedRecords).hasSize(recordCount);
+ assertThat(deletedLogs)
+ .comparingElementsUsing(
+ Correspondence.<DeletedLog, Record>from(
+ (deletedLog, record) ->
+ deletedLog
+ .getDeletedRecordId()
+ .equals(record.getMetadata().getId()),
+ "deleted log record id is equal to deleted record id"))
+ .containsExactlyElementsIn(heightRecords);
+ assertThat(changeLogsResponse.getUpsertedRecords())
+ .containsExactlyElementsIn(weightRecords);
+ }
+}
diff --git a/tests/cts/src/android/healthconnect/cts/StepsRecordTest.java b/tests/cts/src/android/healthconnect/cts/StepsRecordTest.java
index 76893a6c..29617f86 100644
--- a/tests/cts/src/android/healthconnect/cts/StepsRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/StepsRecordTest.java
@@ -1366,6 +1366,71 @@ public class StepsRecordTest {
}
@Test
+ public void testAggregateDuration_differentTimeZones_correctBucketTimes() throws Exception {
+ Context context = ApplicationProvider.getApplicationContext();
+ Duration oneHour = Duration.ofHours(1);
+ Instant t1 = Instant.now().minus(Duration.ofDays(1)).truncatedTo(ChronoUnit.MILLIS);
+ Instant t2 = t1.plus(oneHour);
+ Instant t3 = t2.plus(oneHour);
+
+ Metadata metadata =
+ new Metadata.Builder()
+ .setDataOrigin(
+ new DataOrigin.Builder()
+ .setPackageName(context.getPackageName())
+ .build())
+ .build();
+
+ ZoneOffset zonePlusFive = ZoneOffset.ofHours(5);
+ ZoneOffset zonePlusSix = ZoneOffset.ofHours(6);
+ ZoneOffset zonePlusSeven = ZoneOffset.ofHours(7);
+
+ StepsRecord rec1 =
+ new StepsRecord.Builder(metadata, t1, t2, /* count= */ 100)
+ .setStartZoneOffset(zonePlusFive)
+ .setEndZoneOffset(zonePlusFive)
+ .build();
+ StepsRecord rec2 =
+ new StepsRecord.Builder(metadata, t2, t2, /* count= */ 300)
+ .setStartZoneOffset(zonePlusSix)
+ .setEndZoneOffset(zonePlusSeven)
+ .build();
+
+ TestUtils.insertRecords(List.of(rec1, rec2));
+
+ // Aggregating between [t1+5, t2+7] with 1 hour group duration
+ List<AggregateRecordsGroupedByDurationResponse<Long>> result =
+ TestUtils.getAggregateResponseGroupByDuration(
+ new AggregateRecordsRequest.Builder<Long>(
+ new LocalTimeRangeFilter.Builder()
+ .setStartTime(
+ LocalDateTime.ofInstant(t1, zonePlusFive))
+ .setEndTime(
+ LocalDateTime.ofInstant(t2, zonePlusSeven))
+ .build())
+ .addAggregationType(STEPS_COUNT_TOTAL)
+ .build(),
+ oneHour);
+
+ assertThat(result).hasSize(3);
+
+ // Bucket #0: [t1+5, t2+5]
+ AggregateRecordsGroupedByDurationResponse<Long> response0 = result.get(0);
+ assertThat(response0.getStartTime()).isEqualTo(t1);
+ assertThat(response0.getEndTime()).isEqualTo(t2);
+ assertThat(response0.getZoneOffset(STEPS_COUNT_TOTAL)).isEqualTo(zonePlusFive);
+
+ // Empty bucket in the middle
+ assertThat(result.get(1).get(STEPS_COUNT_TOTAL)).isNull();
+
+ // Bucket #2: [t2+6, t3+6]
+ AggregateRecordsGroupedByDurationResponse<Long> response2 = result.get(2);
+ assertThat(response2.getStartTime()).isEqualTo(t2);
+ assertThat(response2.getEndTime()).isEqualTo(t3);
+ assertThat(response2.getZoneOffset(STEPS_COUNT_TOTAL)).isEqualTo(zonePlusSix);
+ }
+
+ @Test
public void testAggregateDuration_withLocalDateTime() throws Exception {
testAggregateDurationWithLocalTimeForZoneOffset(ZoneOffset.MIN);
testAggregateDurationWithLocalTimeForZoneOffset(ZoneOffset.ofHours(-4));
@@ -1393,7 +1458,7 @@ public class StepsRecordTest {
Duration.ofDays(1));
assertThat(responses).hasSize(4);
- Instant groupBoundary = startTimeLocal.toInstant(ZoneOffset.UTC);
+ Instant groupBoundary = startTimeLocal.toInstant(offset);
for (int i = 0; i < 4; i++) {
assertThat(responses.get(i).get(STEPS_COUNT_TOTAL)).isEqualTo(10);
assertThat(responses.get(i).getZoneOffset(STEPS_COUNT_TOTAL)).isEqualTo(offset);
diff --git a/tests/cts/src/android/healthconnect/cts/WeightRecordTest.java b/tests/cts/src/android/healthconnect/cts/WeightRecordTest.java
index 93981aaf..dcd090ad 100644
--- a/tests/cts/src/android/healthconnect/cts/WeightRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/WeightRecordTest.java
@@ -627,6 +627,68 @@ public class WeightRecordTest {
}
@Test
+ public void testAggregateDuration_differentTimeZones_correctBucketTimes() throws Exception {
+ Context context = ApplicationProvider.getApplicationContext();
+ Duration oneHour = Duration.ofHours(1);
+ Instant t1 = Instant.now().minus(Duration.ofDays(1)).truncatedTo(ChronoUnit.MILLIS);
+ Instant t2 = t1.plus(oneHour);
+ Instant t3 = t2.plus(oneHour);
+
+ Metadata metadata =
+ new Metadata.Builder()
+ .setDataOrigin(
+ new DataOrigin.Builder()
+ .setPackageName(context.getPackageName())
+ .build())
+ .build();
+
+ ZoneOffset zonePlusFive = ZoneOffset.ofHours(5);
+ ZoneOffset zonePlusSix = ZoneOffset.ofHours(6);
+
+ WeightRecord rec1 =
+ new WeightRecord.Builder(metadata, t1, Mass.fromGrams(50000))
+ .setZoneOffset(zonePlusFive)
+ .build();
+ WeightRecord rec2 =
+ new WeightRecord.Builder(metadata, t2, Mass.fromGrams(100000))
+ .setZoneOffset(zonePlusSix)
+ .build();
+
+ TestUtils.insertRecords(List.of(rec1, rec2));
+
+ // Aggregating between [t1+5, t3+6] with 1 hour group duration
+ List<AggregateRecordsGroupedByDurationResponse<Mass>> result =
+ TestUtils.getAggregateResponseGroupByDuration(
+ new AggregateRecordsRequest.Builder<Mass>(
+ new LocalTimeRangeFilter.Builder()
+ .setStartTime(
+ LocalDateTime.ofInstant(t1, zonePlusFive))
+ .setEndTime(
+ LocalDateTime.ofInstant(t3, zonePlusSix))
+ .build())
+ .addAggregationType(WEIGHT_AVG)
+ .build(),
+ oneHour);
+
+ assertThat(result).hasSize(3);
+
+ // Bucket #0: [t1+5, t2+5]
+ AggregateRecordsGroupedByDurationResponse<Mass> response0 = result.get(0);
+ assertThat(response0.getStartTime()).isEqualTo(t1);
+ assertThat(response0.getEndTime()).isEqualTo(t2);
+ assertThat(response0.getZoneOffset(WEIGHT_AVG)).isEqualTo(zonePlusFive);
+
+ // Empty bucket in the middle
+ assertThat(result.get(1).get(WEIGHT_AVG)).isNull();
+
+ // Bucket #2: [t2+6, t3+6]
+ AggregateRecordsGroupedByDurationResponse<Mass> response2 = result.get(2);
+ assertThat(response2.getStartTime()).isEqualTo(t2);
+ assertThat(response2.getEndTime()).isEqualTo(t3);
+ assertThat(response2.getZoneOffset(WEIGHT_AVG)).isEqualTo(zonePlusSix);
+ }
+
+ @Test
public void testAggregateDuration_withLocalDateTime_responsesAnswerAndBoundariesCorrect()
throws Exception {
testDurationLocalTimeAggregationZoneOffset(ZoneOffset.ofHours(4));
@@ -654,7 +716,7 @@ public class WeightRecordTest {
Duration.ofDays(1));
assertThat(responses).hasSize(3);
- Instant groupBoundary = endTimeLocal.minusDays(3).toInstant(ZoneOffset.UTC);
+ Instant groupBoundary = endTimeLocal.minusDays(3).toInstant(offset);
for (int i = 0; i < 3; i++) {
assertThat(responses.get(i).get(WEIGHT_MAX)).isEqualTo(Mass.fromGrams(10.0));
assertThat(responses.get(i).getZoneOffset(WEIGHT_MAX)).isEqualTo(offset);
diff --git a/tests/cts/src/android/healthconnect/cts/ratelimiter/RateLimiterTest.java b/tests/cts/src/android/healthconnect/cts/ratelimiter/RateLimiterTest.java
index 0a2b298c..c7a2cdb6 100644
--- a/tests/cts/src/android/healthconnect/cts/ratelimiter/RateLimiterTest.java
+++ b/tests/cts/src/android/healthconnect/cts/ratelimiter/RateLimiterTest.java
@@ -63,7 +63,8 @@ import java.util.List;
@RunWith(AndroidJUnit4.class)
public class RateLimiterTest {
private static final String TAG = "RateLimiterTest";
- private static final int MAX_FOREGROUND_CALL_15M = 1000;
+ private static final int MAX_FOREGROUND_WRITE_CALL_15M = 1000;
+ private static final int MAX_FOREGROUND_READ_CALL_15M = 2000;
private static final Duration WINDOW_15M = Duration.ofMinutes(15);
public static final String ENABLE_RATE_LIMITER_FLAG = "enable_rate_limiter";
private final UiAutomation mUiAutomation =
@@ -90,7 +91,7 @@ public class RateLimiterTest {
@Test
public void testTryAcquireApiCallQuota_writeCallsInLimit() throws InterruptedException {
- tryAcquireCallQuotaNTimesForWrite(MAX_FOREGROUND_CALL_15M);
+ tryAcquireCallQuotaNTimesForWrite(MAX_FOREGROUND_WRITE_CALL_15M);
}
@Test
@@ -176,10 +177,10 @@ public class RateLimiterTest {
private void exceedWriteQuota() throws InterruptedException {
Instant startTime = Instant.now();
- tryAcquireCallQuotaNTimesForWrite(MAX_FOREGROUND_CALL_15M);
+ tryAcquireCallQuotaNTimesForWrite(MAX_FOREGROUND_WRITE_CALL_15M);
Instant endTime = Instant.now();
float quotaAcquired =
- getQuotaAcquired(startTime, endTime, WINDOW_15M, MAX_FOREGROUND_CALL_15M);
+ getQuotaAcquired(startTime, endTime, WINDOW_15M, MAX_FOREGROUND_WRITE_CALL_15M);
List<Record> testRecord = List.of(TestUtils.getCompleteStepsRecord());
while (quotaAcquired > 1) {
@@ -206,7 +207,7 @@ public class RateLimiterTest {
tryAcquireCallQuotaNTimesForRead(testRecord, insertedRecords);
Instant endTime = Instant.now();
float quotaAcquired =
- getQuotaAcquired(startTime, endTime, WINDOW_15M, MAX_FOREGROUND_CALL_15M);
+ getQuotaAcquired(startTime, endTime, WINDOW_15M, MAX_FOREGROUND_READ_CALL_15M);
while (quotaAcquired > 1) {
readStepsRecordUsingIds(insertedRecords);
quotaAcquired--;
@@ -236,7 +237,7 @@ public class RateLimiterTest {
getChangeLog(context);
}
- for (int i = 0; i < MAX_FOREGROUND_CALL_15M - 300; i++) {
+ for (int i = 0; i < MAX_FOREGROUND_READ_CALL_15M - 300; i++) {
readStepsRecordUsingIds(insertedRecords);
}
diff --git a/tests/cts/utils/HealthConnectTestUtils/src/android/healthconnect/cts/utils/TestUtils.java b/tests/cts/utils/HealthConnectTestUtils/src/android/healthconnect/cts/utils/TestUtils.java
index d3109af8..ad8a80fb 100644
--- a/tests/cts/utils/HealthConnectTestUtils/src/android/healthconnect/cts/utils/TestUtils.java
+++ b/tests/cts/utils/HealthConnectTestUtils/src/android/healthconnect/cts/utils/TestUtils.java
@@ -35,11 +35,11 @@ import static android.healthconnect.test.app.TestAppReceiver.EXTRA_SENDER_PACKAG
import static com.android.compatibility.common.util.FeatureUtil.AUTOMOTIVE_FEATURE;
import static com.android.compatibility.common.util.FeatureUtil.hasSystemFeature;
-import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
import static com.google.common.truth.Truth.assertThat;
+import static java.util.Collections.unmodifiableList;
import static java.util.Objects.requireNonNull;
import android.app.UiAutomation;
@@ -61,6 +61,7 @@ import android.health.connect.HealthPermissionCategory;
import android.health.connect.HealthPermissions;
import android.health.connect.InsertRecordsResponse;
import android.health.connect.ReadRecordsRequest;
+import android.health.connect.ReadRecordsRequestUsingFilters;
import android.health.connect.ReadRecordsRequestUsingIds;
import android.health.connect.ReadRecordsResponse;
import android.health.connect.RecordIdFilter;
@@ -122,6 +123,7 @@ import android.health.connect.datatypes.WeightRecord;
import android.health.connect.datatypes.WheelchairPushesRecord;
import android.health.connect.datatypes.units.Length;
import android.health.connect.datatypes.units.Power;
+import android.health.connect.migration.MigrationEntity;
import android.health.connect.migration.MigrationException;
import android.healthconnect.test.app.TestAppReceiver;
import android.os.Bundle;
@@ -219,15 +221,24 @@ public final class TestUtils {
* @param records records to insert
* @return inserted records
*/
- public static List<Record> insertRecords(List<Record> records) throws InterruptedException {
+ public static List<Record> insertRecords(List<? extends Record> records)
+ throws InterruptedException {
return insertRecords(records, ApplicationProvider.getApplicationContext());
}
- public static List<Record> insertRecords(List<Record> records, Context context)
+ /**
+ * Inserts records to the database.
+ *
+ * @param records records to insert.
+ * @param context a {@link Context} to obtain {@link HealthConnectManager}.
+ * @return inserted records.
+ */
+ public static List<Record> insertRecords(List<? extends Record> records, Context context)
throws InterruptedException {
HealthConnectReceiver<InsertRecordsResponse> receiver = new HealthConnectReceiver<>();
getHealthConnectManager(context)
- .insertRecords(records, Executors.newSingleThreadExecutor(), receiver);
+ .insertRecords(
+ unmodifiableList(records), Executors.newSingleThreadExecutor(), receiver);
List<Record> returnedRecords = receiver.getResponse().getRecords();
assertThat(returnedRecords).hasSize(records.size());
return returnedRecords;
@@ -658,6 +669,28 @@ public final class TestUtils {
.isNotEmpty();
}
+ /** Reads all records in the DB for a given {@code recordClass}. */
+ public static <T extends Record> List<T> readAllRecords(Class<T> recordClass)
+ throws InterruptedException {
+ List<T> records = new ArrayList<>();
+ ReadRecordsResponse<T> readRecordsResponse =
+ readRecordsWithPagination(
+ new ReadRecordsRequestUsingFilters.Builder<>(recordClass).build());
+ while (true) {
+ records.addAll(readRecordsResponse.getRecords());
+ long pageToken = readRecordsResponse.getNextPageToken();
+ if (pageToken == -1) {
+ break;
+ }
+ readRecordsResponse =
+ readRecordsWithPagination(
+ new ReadRecordsRequestUsingFilters.Builder<>(recordClass)
+ .setPageToken(pageToken)
+ .build());
+ }
+ return records;
+ }
+
public static <T extends Record> ReadRecordsResponse<T> readRecordsWithPagination(
ReadRecordsRequest<T> request) throws InterruptedException {
HealthConnectReceiver<ReadRecordsResponse<T>> receiver = new HealthConnectReceiver<>();
@@ -717,7 +750,8 @@ public final class TestUtils {
receiver.verifyNoExceptionOrThrow();
}
- public static void deleteRecords(List<Record> records) throws InterruptedException {
+ /** Helper function to delete records from the DB using HealthConnectManager. */
+ public static void deleteRecords(List<? extends Record> records) throws InterruptedException {
List<RecordIdFilter> recordIdFilters =
records.stream()
.map(
@@ -802,6 +836,14 @@ public final class TestUtils {
receiver.verifyNoExceptionOrThrow();
}
+ public static void writeMigrationData(List<MigrationEntity> entities)
+ throws InterruptedException {
+ MigrationReceiver receiver = new MigrationReceiver();
+ getHealthConnectManager()
+ .writeMigrationData(entities, Executors.newSingleThreadExecutor(), receiver);
+ receiver.verifyNoExceptionOrThrow();
+ }
+
public static void finishMigration() throws InterruptedException {
MigrationReceiver receiver = new MigrationReceiver();
getHealthConnectManager().finishMigration(Executors.newSingleThreadExecutor(), receiver);
@@ -1359,7 +1401,7 @@ public final class TestUtils {
}
public static void sendCommandToTestAppReceiver(Context context, String action) {
- sendCommandToTestAppReceiver(context, action, /*extras=*/ null);
+ sendCommandToTestAppReceiver(context, action, /* extras= */ null);
}
public static void sendCommandToTestAppReceiver(Context context, String action, Bundle extras) {
diff --git a/tests/integrationtests/src/android/healthconnect/tests/backgroundread/BackgroundReadTest.java b/tests/integrationtests/src/android/healthconnect/tests/backgroundread/BackgroundReadTest.java
index 3893bdea..53a0eb69 100644
--- a/tests/integrationtests/src/android/healthconnect/tests/backgroundread/BackgroundReadTest.java
+++ b/tests/integrationtests/src/android/healthconnect/tests/backgroundread/BackgroundReadTest.java
@@ -94,11 +94,8 @@ public class BackgroundReadTest {
sendCommandToTestAppReceiver(mContext, ACTION_READ_RECORDS_FOR_OTHER_APP);
- final Bundle result = TestReceiver.getResult();
- assertThat(result).isNotNull();
-
- // Other apps' data is simply not returned when reading in background
- assertThat(result.getInt(EXTRA_RECORD_COUNT)).isEqualTo(0);
+ assertThat(TestReceiver.getResult()).isNull();
+ assertThat(TestReceiver.getErrorCode()).isEqualTo(ERROR_SECURITY);
}
@Test
diff --git a/tests/unittests/src/android/healthconnect/RateLimiterTest.java b/tests/unittests/src/android/healthconnect/RateLimiterTest.java
index d47fc751..236ac67a 100644
--- a/tests/unittests/src/android/healthconnect/RateLimiterTest.java
+++ b/tests/unittests/src/android/healthconnect/RateLimiterTest.java
@@ -16,90 +16,60 @@
package android.healthconnect;
-import static android.health.connect.ratelimiter.RateLimiter.CHUNK_SIZE_LIMIT_IN_BYTES;
-import static android.health.connect.ratelimiter.RateLimiter.RECORD_SIZE_LIMIT_IN_BYTES;
-
import static org.hamcrest.CoreMatchers.containsString;
+import android.Manifest;
+import android.app.UiAutomation;
+import android.content.Context;
import android.health.connect.HealthConnectException;
import android.health.connect.ratelimiter.RateLimiter;
import android.health.connect.ratelimiter.RateLimiter.QuotaCategory;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.modules.utils.testing.ExtendedMockitoRule;
import com.android.server.healthconnect.HealthConnectDeviceConfigManager;
+import com.android.server.healthconnect.TestUtils;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
+import org.mockito.Mock;
+import org.mockito.quality.Strictness;
import java.time.Duration;
import java.time.Instant;
-import java.util.HashMap;
-import java.util.Map;
public class RateLimiterTest {
private static final int UID = 1;
private static final boolean IS_IN_FOREGROUND_TRUE = true;
private static final boolean IS_IN_FOREGROUND_FALSE = false;
- private static final int MAX_FOREGROUND_CALL_15M = 1000;
+ private static final int MAX_FOREGROUND_READ_CALL_15M = 2000;
private static final int MAX_BACKGROUND_CALL_15M = 1000;
private static final Duration WINDOW_15M = Duration.ofMinutes(15);
private static final int MEMORY_COST = 20000;
+ private static final UiAutomation UI_AUTOMATION =
+ InstrumentationRegistry.getInstrumentation().getUiAutomation();
+
@Rule public ExpectedException exception = ExpectedException.none();
+ @Rule
+ public final ExtendedMockitoRule mExtendedMockitoRule =
+ new ExtendedMockitoRule.Builder(this).setStrictness(Strictness.LENIENT).build();
+
+ @Mock Context mContext;
+
@Before
public void setUp() {
- Map<Integer, Integer> quotaBucketToMaxRollingQuotaMap = new HashMap<>();
- Map<String, Integer> quotaBucketToMaxMemoryQuotaMap = new HashMap<>();
- quotaBucketToMaxRollingQuotaMap.put(
- RateLimiter.QuotaBucket.QUOTA_BUCKET_READS_PER_15M_FOREGROUND,
- HealthConnectDeviceConfigManager
- .QUOTA_BUCKET_PER_15M_FOREGROUND_DEFAULT_FLAG_VALUE);
- quotaBucketToMaxRollingQuotaMap.put(
- RateLimiter.QuotaBucket.QUOTA_BUCKET_READS_PER_15M_BACKGROUND,
- HealthConnectDeviceConfigManager
- .QUOTA_BUCKET_PER_15M_BACKGROUND_DEFAULT_FLAG_VALUE);
- quotaBucketToMaxRollingQuotaMap.put(
- RateLimiter.QuotaBucket.QUOTA_BUCKET_WRITES_PER_15M_FOREGROUND,
- HealthConnectDeviceConfigManager
- .QUOTA_BUCKET_PER_15M_FOREGROUND_DEFAULT_FLAG_VALUE);
- quotaBucketToMaxRollingQuotaMap.put(
- RateLimiter.QuotaBucket.QUOTA_BUCKET_WRITES_PER_15M_BACKGROUND,
- HealthConnectDeviceConfigManager
- .QUOTA_BUCKET_PER_15M_BACKGROUND_DEFAULT_FLAG_VALUE);
- quotaBucketToMaxRollingQuotaMap.put(
- RateLimiter.QuotaBucket.QUOTA_BUCKET_READS_PER_24H_FOREGROUND,
- HealthConnectDeviceConfigManager
- .QUOTA_BUCKET_PER_24H_FOREGROUND_DEFAULT_FLAG_VALUE);
- quotaBucketToMaxRollingQuotaMap.put(
- RateLimiter.QuotaBucket.QUOTA_BUCKET_READS_PER_24H_BACKGROUND,
- HealthConnectDeviceConfigManager
- .QUOTA_BUCKET_PER_24H_BACKGROUND_DEFAULT_FLAG_VALUE);
- quotaBucketToMaxRollingQuotaMap.put(
- RateLimiter.QuotaBucket.QUOTA_BUCKET_WRITES_PER_24H_FOREGROUND,
- HealthConnectDeviceConfigManager
- .QUOTA_BUCKET_PER_24H_FOREGROUND_DEFAULT_FLAG_VALUE);
- quotaBucketToMaxRollingQuotaMap.put(
- RateLimiter.QuotaBucket.QUOTA_BUCKET_WRITES_PER_24H_BACKGROUND,
- HealthConnectDeviceConfigManager
- .QUOTA_BUCKET_PER_24H_BACKGROUND_DEFAULT_FLAG_VALUE);
- quotaBucketToMaxRollingQuotaMap.put(
- RateLimiter.QuotaBucket.QUOTA_BUCKET_DATA_PUSH_LIMIT_PER_APP_15M,
- HealthConnectDeviceConfigManager.DATA_PUSH_LIMIT_PER_APP_15M_DEFAULT_FLAG_VALUE);
- quotaBucketToMaxRollingQuotaMap.put(
- RateLimiter.QuotaBucket.QUOTA_BUCKET_DATA_PUSH_LIMIT_ACROSS_APPS_15M,
- HealthConnectDeviceConfigManager
- .DATA_PUSH_LIMIT_ACROSS_APPS_15M_DEFAULT_FLAG_VALUE);
- quotaBucketToMaxMemoryQuotaMap.put(
- CHUNK_SIZE_LIMIT_IN_BYTES,
- HealthConnectDeviceConfigManager.CHUNK_SIZE_LIMIT_IN_BYTES_DEFAULT_FLAG_VALUE);
- quotaBucketToMaxMemoryQuotaMap.put(
- RECORD_SIZE_LIMIT_IN_BYTES,
- HealthConnectDeviceConfigManager.RECORD_SIZE_LIMIT_IN_BYTES_DEFAULT_FLAG_VALUE);
- RateLimiter.updateMaxRollingQuotaMap(quotaBucketToMaxRollingQuotaMap);
- RateLimiter.updateMemoryQuotaMap(quotaBucketToMaxMemoryQuotaMap);
- RateLimiter.updateEnableRateLimiterFlag(true);
+ TestUtils.runWithShellPermissionIdentity(
+ () -> {
+ HealthConnectDeviceConfigManager.initializeInstance(mContext);
+ HealthConnectDeviceConfigManager.getInitialisedInstance()
+ .updateRateLimiterValues();
+ },
+ Manifest.permission.READ_DEVICE_CONFIG);
}
@Test
@@ -116,7 +86,7 @@ public class RateLimiterTest {
RateLimiter.clearCache();
@QuotaCategory.Type int quotaCategory = 1;
tryAcquireCallQuotaNTimes(
- quotaCategory, IS_IN_FOREGROUND_TRUE, MAX_FOREGROUND_CALL_15M + 1);
+ quotaCategory, IS_IN_FOREGROUND_TRUE, MAX_FOREGROUND_READ_CALL_15M + 1);
}
@Test
@@ -132,7 +102,7 @@ public class RateLimiterTest {
RateLimiter.clearCache();
@QuotaCategory.Type int quotaCategoryRead = 2;
tryAcquireCallQuotaNTimes(
- quotaCategoryRead, IS_IN_FOREGROUND_TRUE, MAX_FOREGROUND_CALL_15M);
+ quotaCategoryRead, IS_IN_FOREGROUND_TRUE, MAX_FOREGROUND_READ_CALL_15M);
}
@Test
@@ -149,10 +119,10 @@ public class RateLimiterTest {
@QuotaCategory.Type int quotaCategoryRead = 2;
Instant startTime = Instant.now();
tryAcquireCallQuotaNTimes(
- quotaCategoryRead, IS_IN_FOREGROUND_TRUE, MAX_FOREGROUND_CALL_15M);
+ quotaCategoryRead, IS_IN_FOREGROUND_TRUE, MAX_FOREGROUND_READ_CALL_15M);
Instant endTime = Instant.now();
int ceilQuotaAcquired =
- getCeilQuotaAcquired(startTime, endTime, WINDOW_15M, MAX_FOREGROUND_CALL_15M);
+ getCeilQuotaAcquired(startTime, endTime, WINDOW_15M, MAX_FOREGROUND_READ_CALL_15M);
exception.expect(HealthConnectException.class);
exception.expectMessage(containsString("API call quota exceeded"));
tryAcquireCallQuotaNTimes(quotaCategoryRead, IS_IN_FOREGROUND_TRUE, ceilQuotaAcquired);
diff --git a/tests/unittests/src/com/android/server/healthconnect/TestUtils.java b/tests/unittests/src/com/android/server/healthconnect/TestUtils.java
index 5084836d..b9c3ec38 100644
--- a/tests/unittests/src/com/android/server/healthconnect/TestUtils.java
+++ b/tests/unittests/src/com/android/server/healthconnect/TestUtils.java
@@ -16,8 +16,12 @@
package com.android.server.healthconnect;
+import android.app.UiAutomation;
import android.os.UserHandle;
+import androidx.annotation.NonNull;
+import androidx.test.platform.app.InstrumentationRegistry;
+
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeoutException;
@@ -70,4 +74,19 @@ public final class TestUtils {
.getCompletedTaskCount()),
15);
}
+
+ /** Runs a {@link Runnable} adopting a subset of Shell's permissions. */
+ public static void runWithShellPermissionIdentity(
+ @NonNull Runnable runnable, String... permissions) {
+ final UiAutomation uiAutomation =
+ InstrumentationRegistry.getInstrumentation().getUiAutomation();
+ uiAutomation.adoptShellPermissionIdentity(permissions);
+ try {
+ runnable.run();
+ } catch (Exception e) {
+ throw new RuntimeException("Caught exception", e);
+ } finally {
+ uiAutomation.dropShellPermissionIdentity();
+ }
+ }
}
diff --git a/tests/unittests/src/com/android/server/healthconnect/permission/FirstGrantTimeUnitTest.java b/tests/unittests/src/com/android/server/healthconnect/permission/FirstGrantTimeUnitTest.java
index 12e55140..4af633c2 100644
--- a/tests/unittests/src/com/android/server/healthconnect/permission/FirstGrantTimeUnitTest.java
+++ b/tests/unittests/src/com/android/server/healthconnect/permission/FirstGrantTimeUnitTest.java
@@ -47,6 +47,7 @@ import com.android.server.healthconnect.TestUtils;
import org.junit.After;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
@@ -115,6 +116,7 @@ public class FirstGrantTimeUnitTest {
}
@Test
+ @Ignore("b/312712918 this test is flaky")
public void testCurrentPackage_intentSupported_grantTimeIsNotNull() {
assertThat(mGrantTimeManager.getFirstGrantTime(SELF_PACKAGE_NAME, CURRENT_USER))
.isNotNull();
@@ -144,6 +146,7 @@ public class FirstGrantTimeUnitTest {
}
@Test
+ @Ignore("b/312712918 this test is flaky")
public void testCurrentPackage_noBackup_useRecordedTime() {
Instant stateTime = Instant.now().minusSeconds((long) 1e5);
UserGrantTimeState stagedState = setupGrantTimeState(stateTime, null);
@@ -156,6 +159,7 @@ public class FirstGrantTimeUnitTest {
}
@Test
+ @Ignore("b/312712918 this test is flaky")
public void testCurrentPackage_noBackup_grantTimeEqualToStaged() {
Instant backupTime = Instant.now().minusSeconds((long) 1e5);
Instant stateTime = backupTime.plusSeconds(10);
diff --git a/tests/unittests/src/com/android/server/healthconnect/storage/datatypehelpers/HealthDataCategoryPriorityHelperTest.java b/tests/unittests/src/com/android/server/healthconnect/storage/datatypehelpers/HealthDataCategoryPriorityHelperTest.java
index 8d4b270d..58f27795 100644
--- a/tests/unittests/src/com/android/server/healthconnect/storage/datatypehelpers/HealthDataCategoryPriorityHelperTest.java
+++ b/tests/unittests/src/com/android/server/healthconnect/storage/datatypehelpers/HealthDataCategoryPriorityHelperTest.java
@@ -745,6 +745,52 @@ public class HealthDataCategoryPriorityHelperTest {
}
@Test
+ public void testOldReSyncHealthDataPriorityTable_maintainsExistingOrdering() {
+ when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+ .thenReturn(false);
+ // Setup current priority list
+ Map<Integer, List<Long>> priorityList = new HashMap<>();
+ priorityList.put(
+ HealthDataCategory.ACTIVITY,
+ List.of(APP_PACKAGE_ID_3, APP_PACKAGE_ID, APP_PACKAGE_ID_2));
+ setupPriorityList(priorityList);
+
+ // Setup contributor apps
+ Map<Integer, Set<String>> recordTypesToContributorPackages = new HashMap<>();
+ recordTypesToContributorPackages.put(
+ RecordTypeIdentifier.RECORD_TYPE_STEPS,
+ Set.of(APP_PACKAGE_NAME, APP_PACKAGE_NAME_2, APP_PACKAGE_NAME_3));
+ when(mAppInfoHelper.getRecordTypesToContributingPackagesMap())
+ .thenReturn(recordTypesToContributorPackages);
+
+ // Setup apps with write permissions
+ mPackageInfo1.packageName = APP_PACKAGE_NAME;
+ mPackageInfo1.requestedPermissions = new String[] {HealthPermissions.WRITE_STEPS};
+ mPackageInfo1.requestedPermissionsFlags =
+ new int[] {PackageInfo.REQUESTED_PERMISSION_GRANTED};
+
+ mPackageInfo2.packageName = APP_PACKAGE_NAME_2;
+ mPackageInfo2.requestedPermissions = new String[] {HealthPermissions.WRITE_STEPS};
+ mPackageInfo2.requestedPermissionsFlags =
+ new int[] {PackageInfo.REQUESTED_PERMISSION_GRANTED};
+
+ mPackageInfo3.packageName = APP_PACKAGE_NAME_3;
+ mPackageInfo3.requestedPermissions = new String[] {HealthPermissions.WRITE_STEPS};
+ mPackageInfo3.requestedPermissionsFlags =
+ new int[] {PackageInfo.REQUESTED_PERMISSION_GRANTED};
+ when(HealthConnectManager.isHealthPermission(any(), any())).thenReturn(true);
+ when(mPackageInfoUtils.getPackagesHoldingHealthPermissions(any(), any()))
+ .thenReturn(List.of(mPackageInfo1, mPackageInfo2, mPackageInfo3));
+
+ mHealthDataCategoryPriorityHelper.reSyncHealthDataPriorityTable(mContext);
+
+ assertThat(
+ mHealthDataCategoryPriorityHelper.getAppIdPriorityOrder(
+ HealthDataCategory.ACTIVITY))
+ .isEqualTo(List.of(APP_PACKAGE_ID_3, APP_PACKAGE_ID, APP_PACKAGE_ID_2));
+ }
+
+ @Test
public void testOldReSyncHealthDataPriorityTable_addsNewApps_withWritePermission() {
when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
.thenReturn(false);