Snap for 11041982 from 96314d2c8b5c36286b096d0dbdeb7690ae1067f2 to mainline-networking-release

Change-Id: I1450a306e32b786efc983c6549899bdbd9ae4070
diff --git a/OWNERS b/OWNERS
index 6f4ebc7..5f41742 100644
--- a/OWNERS
+++ b/OWNERS
@@ -6,10 +6,13 @@
 jstembridge@google.com
 magdi@google.com
 nandana@google.com
+nickelc@google.com
 pratyushmore@google.com
 ronish@google.com
 sameerj@google.com
 shikhamalhotra@google.com
+itsleo@google.com
+junioraw@google.com
 
 
 include platform/packages/modules/common:/MODULES_OWNERS  # see go/mainline-owners-policy
\ No newline at end of file
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 3ab36d9..521bc66 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -34,6 +34,9 @@
       "name": "CtsHealthConnectControllerTestCases"
     },
     {
+      "name": "HealthFitnessIntegrationBackupRestoreTests"
+    },
+    {
       "name": "HealthFitnessIntegrationTests"
     },
     {
diff --git a/apk/HealthPermissionsManifest.xml b/apk/HealthPermissionsManifest.xml
index 1063e31..30c85b3 100644
--- a/apk/HealthPermissionsManifest.xml
+++ b/apk/HealthPermissionsManifest.xml
@@ -18,6 +18,12 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.android.healthconnect.controller">
 
+    <permission
+        android:name="android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND"
+        android:label="@string/background_read_description"
+        android:protectionLevel="dangerous"
+        android:permissionGroup="android.permission-group.HEALTH" />
+
     <!-- Read permissions for activities -->
     <!-- Allows an application to read calories burnt.
          <p>Protection level: dangerous
@@ -47,6 +53,12 @@
         android:permissionGroup="android.permission-group.HEALTH" />
 
     <permission
+        android:name="android.permission.health.READ_EXERCISE_ROUTES_ALL"
+        android:label="@string/exercise_routes_all_read_content_description"
+        android:protectionLevel="dangerous"
+        android:permissionGroup="android.permission-group.HEALTH" />
+
+    <permission
         android:name="android.permission.health.READ_EXERCISE_ROUTE"
         android:label="@string/exercise_route_read_content_description"
         android:protectionLevel="signature" />
diff --git a/apk/res/drawable/ic_add.xml b/apk/res/drawable/ic_add.xml
new file mode 100644
index 0000000..f2145e8
--- /dev/null
+++ b/apk/res/drawable/ic_add.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0"
+    android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M20,13h-7v7h-2v-7H4v-2h7V4h2v7h7v2z"/>
+</vector>
\ No newline at end of file
diff --git a/apk/res/drawable/ic_arrow_down.xml b/apk/res/drawable/ic_arrow_down.xml
new file mode 100644
index 0000000..e61a48e
--- /dev/null
+++ b/apk/res/drawable/ic_arrow_down.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="12dp"
+    android:height="7dp"
+    android:viewportWidth="12"
+    android:viewportHeight="7">
+    <path
+        android:pathData="M1.607,0.06L0.667,1L6,6.333L11.333,1L10.393,0.06L6,4.447"
+        android:fillColor="#1B1C17"/>
+</vector>
\ No newline at end of file
diff --git a/apk/res/drawable/ic_edit.xml b/apk/res/drawable/ic_edit.xml
new file mode 100644
index 0000000..522dfc0
--- /dev/null
+++ b/apk/res/drawable/ic_edit.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0"
+    android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M20.41,4.94l-1.35,-1.35c-0.78,-0.78 -2.05,-0.78 -2.83,0L3,16.82L3,21h4.18L20.41,7.77c0.79,-0.78 0.79,-2.05 0,-2.83zM6.41,19.06L5,19v-1.36l9.82,-9.82 1.41,1.41 -9.82,9.83z"/>
+</vector>
\ No newline at end of file
diff --git a/apk/res/drawable/tab_background.xml b/apk/res/drawable/tab_background.xml
new file mode 100644
index 0000000..114adf8
--- /dev/null
+++ b/apk/res/drawable/tab_background.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2022 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.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@drawable/tab_background_selected" android:state_selected="true"/>
+    <item android:drawable="@drawable/tab_background_unselected" />
+</selector>
\ No newline at end of file
diff --git a/apk/res/drawable/tab_background_selected.xml b/apk/res/drawable/tab_background_selected.xml
new file mode 100644
index 0000000..bd23d62
--- /dev/null
+++ b/apk/res/drawable/tab_background_selected.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2022 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.
+-->
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+    android:insetTop="@dimen/spacing_xsmall"
+    android:insetBottom="@dimen/spacing_xsmall"
+    android:insetLeft="@dimen/spacing_small"
+    android:insetRight="@dimen/spacing_small" >
+    <shape android:shape="rectangle">
+        <corners android:radius="@dimen/tab_corner_radius"/>
+        <solid android:color="@color/settingslib_colorAccentPrimary" />
+    </shape>
+</inset>
\ No newline at end of file
diff --git a/apk/res/drawable/tab_background_unselected.xml b/apk/res/drawable/tab_background_unselected.xml
new file mode 100644
index 0000000..27ab9a8
--- /dev/null
+++ b/apk/res/drawable/tab_background_unselected.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2022 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.
+-->
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+    android:insetTop="@dimen/spacing_xsmall"
+    android:insetBottom="@dimen/spacing_xsmall"
+    android:insetLeft="@dimen/spacing_small"
+    android:insetRight="@dimen/spacing_small" >
+    <shape android:shape="rectangle">
+        <corners android:radius="@dimen/tab_corner_radius" />
+        <solid android:color="@color/settingslib_colorSurfaceVariant" />
+    </shape>
+</inset>
\ No newline at end of file
diff --git a/apk/res/layout/date_navigation_spinner_item.xml b/apk/res/layout/date_navigation_spinner_item.xml
new file mode 100644
index 0000000..59e2831
--- /dev/null
+++ b/apk/res/layout/date_navigation_spinner_item.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/* //device/apps/common/assets/res/any/layout/simple_spinner_item.xml
+**
+** Copyright 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.
+*/
+-->
+<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/spinner_item_text"
+    style="?android:attr/spinnerDropDownItemStyle"
+    android:textAppearance="?attr/textAppearanceLabel"
+    android:textColor="?android:attr/textColorPrimary"
+    android:singleLine="true"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="?android:attr/listPreferredItemHeightSmall"
+    android:ellipsize="marquee"/>
diff --git a/apk/res/layout/fragment_entries.xml b/apk/res/layout/fragment_entries.xml
new file mode 100644
index 0000000..932e275
--- /dev/null
+++ b/apk/res/layout/fragment_entries.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationView
+        android:id="@+id/date_navigation_view"
+        android:paddingTop="@dimen/spacing_xsmall"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
+    <ProgressBar
+        android:id="@+id/loading"
+        android:layout_gravity="center_horizontal"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"/>
+
+    <TextView
+        android:id="@+id/no_data_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center"
+        android:text="@string/no_data"
+        android:visibility="gone" />
+
+    <TextView
+        android:id="@+id/error_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center"
+        android:text="@string/default_error"
+        android:visibility="gone" />
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/data_entries_list"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:fillViewport="true"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/apk/res/layout/fragment_entries_access.xml b/apk/res/layout/fragment_entries_access.xml
new file mode 100644
index 0000000..e0cf839
--- /dev/null
+++ b/apk/res/layout/fragment_entries_access.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:theme="@style/Theme.MaterialComponents.DayNight"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:layout_alignParentTop="true"
+        android:id="@+id/tab_container"
+        android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+        android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+        android:paddingBottom="@dimen/spacing_small">
+
+        <com.google.android.material.tabs.TabLayout
+            android:background="@android:color/transparent"
+            android:id="@+id/tab_layout"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:tabMaxWidth="0dp"
+            app:tabGravity="fill"
+            app:tabMode="fixed"
+            app:tabIndicatorHeight="0dp"
+            app:tabSelectedTextColor="?android:attr/textColorPrimary"
+            app:tabTextColor="?android:attr/textColorSecondary"
+            app:tabBackground="@drawable/tab_background"
+            app:tabTextAppearance="?attr/textAppearanceSubheader" />
+    </LinearLayout>
+
+    <androidx.viewpager2.widget.ViewPager2
+        android:id="@+id/view_pager"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_below="@+id/tab_container"/>
+</RelativeLayout>
\ No newline at end of file
diff --git a/apk/res/layout/priority_item.xml b/apk/res/layout/priority_item.xml
index c3b55da..beca47c 100644
--- a/apk/res/layout/priority_item.xml
+++ b/apk/res/layout/priority_item.xml
@@ -52,7 +52,7 @@
         android:textAppearance="?attr/textAppearanceSubheader" />
 
     <FrameLayout
-        android:id="@+id/drag_icon"
+        android:id="@+id/action_icon"
         android:layout_width="wrap_content"
         android:layout_height="match_parent"
         android:minWidth="@dimen/button_size"
diff --git a/apk/res/layout/section_title.xml b/apk/res/layout/section_title.xml
new file mode 100644
index 0000000..db98b85
--- /dev/null
+++ b/apk/res/layout/section_title.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingTop="@dimen/spacing_normal"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:background="?android:attr/selectableItemBackground"
+    android:baselineAligned="false"
+    android:layout_marginTop="@dimen/spacing_normal"
+    android:gravity="center_vertical">
+
+    <TextView
+        android:id="@android:id/title"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="start"
+        android:textAlignment="viewStart"
+        style="?attr/textAppearancePreferenceCategory"/>
+
+</LinearLayout>
diff --git a/apk/res/layout/widget_aggregation_data_card.xml b/apk/res/layout/widget_aggregation_data_card.xml
new file mode 100644
index 0000000..7ffc40c
--- /dev/null
+++ b/apk/res/layout/widget_aggregation_data_card.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  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.
+-->
+<!-- If this is not the root then it should fill the parent -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/aggregation_data_card"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    android:gravity="center_vertical"
+    android:padding="@dimen/spacing_banner"
+    android:background="@drawable/banner_background" >
+
+    <ImageView
+        android:id="@+id/card_icon"
+        android:layout_width="@dimen/icon_size"
+        android:layout_height="@dimen/icon_size"
+        android:layout_marginEnd="@dimen/spacing_small"
+        app:tint="?android:attr/textColorPrimary"
+        android:contentDescription="@string/data_totals_header" />
+    <!-- TODO (b/299940574) dynamically set content description using category of aggregation -->
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/title_date_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+
+        <TextView
+            android:id="@+id/card_title_number"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginEnd="@dimen/spacing_xsmall"
+            android:maxLines="1"
+            android:ellipsize="end"
+            android:textAppearance="?attr/textAppearanceItem"/>
+
+        <TextView
+            android:id="@+id/card_date"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:maxLines="1"
+            android:ellipsize="end"
+            android:textAppearance="?attr/textAppearanceSummary"/>
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/apk/res/layout/widget_app_source_layout.xml b/apk/res/layout/widget_app_source_layout.xml
index 96a2a10..e07aa6c 100644
--- a/apk/res/layout/widget_app_source_layout.xml
+++ b/apk/res/layout/widget_app_source_layout.xml
@@ -1,4 +1,19 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+-->
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
@@ -56,7 +71,7 @@
     </LinearLayout>
 
     <FrameLayout
-        android:id="@+id/drag_icon"
+        android:id="@+id/action_icon"
         android:layout_width="wrap_content"
         android:layout_height="match_parent"
         android:minWidth="@dimen/button_size"
@@ -66,6 +81,7 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_gravity="center_vertical|center_horizontal"
+            android:id="@+id/action_icon_background"
             android:minHeight="@dimen/icon_size"
             android:minWidth="@dimen/icon_size"
             android:background="?attr/priorityItemDragIcon"
diff --git a/apk/res/layout/widget_banner_preference.xml b/apk/res/layout/widget_banner_preference.xml
index 5724206..f6a80b6 100644
--- a/apk/res/layout/widget_banner_preference.xml
+++ b/apk/res/layout/widget_banner_preference.xml
@@ -1,4 +1,18 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="utf-8"?><!--
+  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.
+-->
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
diff --git a/apk/res/layout/widget_card_preference.xml b/apk/res/layout/widget_card_preference.xml
new file mode 100644
index 0000000..e264c32
--- /dev/null
+++ b/apk/res/layout/widget_card_preference.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  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.
+-->
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:id="@+id/card_container"
+    android:layout_marginHorizontal="@dimen/spacing_normal"
+    android:layout_marginVertical="@dimen/spacing_normal">
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/apk/res/layout/widget_date_navigation_with_spinner.xml b/apk/res/layout/widget_date_navigation_with_spinner.xml
new file mode 100644
index 0000000..1e7aec6
--- /dev/null
+++ b/apk/res/layout/widget_date_navigation_with_spinner.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  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.
+-->
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layoutDirection="ltr"
+    android:orientation="horizontal">
+
+    <ImageButton
+        android:id="@+id/navigation_previous_day"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentStart="true"
+        android:background="@null"
+        android:contentDescription="@string/navigation_previous_day"
+        android:padding="@dimen/spacing_normal"
+        android:src="@drawable/ic_left_arrow"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <ImageButton
+        android:id="@+id/navigation_next_day"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentEnd="true"
+        android:background="@null"
+        android:contentDescription="@string/navigation_next_day"
+        android:padding="@dimen/spacing_normal"
+        android:src="@drawable/ic_right_arrow"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_centerInParent="true"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="@+id/navigation_next_day"
+        app:layout_constraintStart_toStartOf="@+id/navigation_previous_day"
+        app:layout_constraintTop_toTopOf="parent"
+        android:layoutDirection="locale"
+        android:orientation="horizontal">
+
+        <Spinner
+            android:id="@+id/date_picker_spinner"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:layout_centerInParent="true"
+            android:spinnerMode="dropdown"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            android:contentDescription="@string/navigation_selected_day"
+            android:textAppearance="?attr/textAppearanceLabel"
+            style="?attr/spinnerStyle" />
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/apk/res/layout/widget_loading_preference.xml b/apk/res/layout/widget_loading_preference.xml
new file mode 100644
index 0000000..8c4bd41
--- /dev/null
+++ b/apk/res/layout/widget_loading_preference.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  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.
+-->
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/loading_container"
+    android:layout_marginHorizontal="@dimen/spacing_normal"
+    android:layout_marginVertical="@dimen/spacing_normal"
+    app:layout_constraintStart_toStartOf="parent"
+    app:layout_constraintEnd_toEndOf="parent"
+    app:layout_constraintTop_toTopOf="parent"
+    app:layout_constraintBottom_toBottomOf="parent">
+
+    <ProgressBar
+        android:id="@+id/loading"
+        android:layout_gravity="center_horizontal"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/apk/res/layout/widget_no_data.xml b/apk/res/layout/widget_no_data.xml
new file mode 100644
index 0000000..d620a84
--- /dev/null
+++ b/apk/res/layout/widget_no_data.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/no_data_layout"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:clipToPadding="false">
+
+    <TextView
+        android:id="@+id/no_data"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:paddingHorizontal="@dimen/spacing_normal"
+        android:text="@string/no_data"
+        android:textAppearance="?attr/textAppearanceHeadline6" />
+</LinearLayout>
diff --git a/apk/res/layout/widget_preference_category_title.xml b/apk/res/layout/widget_preference_category_title.xml
new file mode 100644
index 0000000..0e8141d
--- /dev/null
+++ b/apk/res/layout/widget_preference_category_title.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingTop="@dimen/spacing_normal"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:baselineAligned="false"
+    android:layout_marginTop="@dimen/spacing_normal"
+    android:gravity="center_vertical">
+    <TextView
+        android:id="@android:id/title"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="start"
+        android:textAlignment="viewStart"
+        style="?attr/textAppearancePreferenceCategory"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/apk/res/menu/data_sources.xml b/apk/res/menu/data_sources.xml
new file mode 100644
index 0000000..3429489
--- /dev/null
+++ b/apk/res/menu/data_sources.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:id="@+id/menu_edit"
+        android:icon="?attr/editIcon"
+        android:showAsAction="always|collapseActionView"
+        android:title="@string/edit_data_sources"/>
+    <item
+        android:id="@+id/menu_send_feedback"
+        android:title="@string/send_feedback" />
+    <item
+        android:id="@+id/menu_help"
+        android:title="@string/help" />
+</menu>
\ No newline at end of file
diff --git a/apk/res/navigation/data_nav_graph_new_ia.xml b/apk/res/navigation/data_nav_graph_new_ia.xml
new file mode 100644
index 0000000..784a8db
--- /dev/null
+++ b/apk/res/navigation/data_nav_graph_new_ia.xml
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  ~
+  ~
+  -->
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/permissions_nav_graph"
+    app:startDestination="@id/allDataFragment">
+
+    <fragment
+        android:id="@+id/appDataFragment"
+        android:label="@string/app_data_title"
+        android:name="com.android.healthconnect.controller.data.appdata.AppDataFragment">
+        <argument
+            android:name="android.intent.extra.PACKAGE_NAME"
+            app:argType="string" />
+        <action
+            android:id="@+id/action_appData_to_appEntries"
+            app:destination="@id/appEntriesFragment" />
+    </fragment>
+
+    <fragment
+        android:id="@+id/appEntriesFragment"
+        android:label="@string/app_data_title"
+        android:name="com.android.healthconnect.controller.data.entries.AppEntriesFragment">
+        <argument
+            android:name="android.intent.extra.PACKAGE_NAME"
+            app:argType="string" />
+        <action
+            android:id="@+id/action_appEntriesFragment_to_dataEntryDetailsFragment"
+            app:destination="@id/dataEntryDetailsFragment" />
+    </fragment>
+
+    <fragment
+        android:id="@+id/allDataFragment"
+        android:label="@string/data_title"
+        android:name="com.android.healthconnect.controller.data.alldata.AllDataFragment">
+        <action
+            android:id="@+id/action_allData_to_entriesAndAccess"
+            app:destination="@id/entriesAndAccessFragment" />
+        <action
+            android:id="@+id/action_allDataFragment_to_unitFragment"
+            app:destination="@id/unitFragment" />
+    </fragment>
+
+    <fragment
+        android:id="@+id/entriesAndAccessFragment"
+        android:label="@string/data_title"
+        android:name="com.android.healthconnect.controller.data.entriesandaccess.EntriesAndAccessFragment">
+        <action
+            android:id="@+id/action_entriesAndAccessFragment_to_dataEntryDetailsFragment"
+            app:destination="@id/dataEntryDetailsFragment" />
+        <action
+            android:id="@+id/action_entriesAndAccessFragment_to_appAccess"
+            app:destination="@id/connectedAppFragment" />
+        <action
+            android:id="@+id/action_entriesAndAccessFragment_to_unitFragment"
+            app:destination="@id/unitFragment" />
+    </fragment>
+
+    <fragment
+        android:id="@+id/unitFragment"
+        android:name="com.android.healthconnect.controller.data.entries.units.UnitsFragment"
+        android:label="@string/units_title" />
+
+    <fragment
+        android:id="@+id/dataEntryDetailsFragment"
+        android:name="com.android.healthconnect.controller.data.entrydetails.DataEntryDetailsFragment"
+        android:label="@string/entry_details_title" >
+        <action
+            android:id="@+id/action_dataEntryDetailsFragment_to_unitFragment"
+            app:destination="@id/unitFragment" />
+    </fragment>
+
+    <fragment
+        android:id="@+id/connectedAppFragment"
+        android:label="@string/app_access_title"
+        android:name="com.android.healthconnect.controller.permissions.connectedapp.ConnectedAppFragment">
+        <argument
+            android:name="android.intent.extra.PACKAGE_NAME"
+            app:argType="string" />
+        <action
+            android:id="@+id/action_connectedApp_to_appData"
+            app:destination="@id/appDataFragment" />
+    </fragment>
+
+    <activity
+        android:id="@+id/manageAppPermissions"
+        app:action="android.health.connect.action.MANAGE_HEALTH_PERMISSIONS">
+    </activity>
+</navigation>
\ No newline at end of file
diff --git a/apk/res/navigation/nav_graph.xml b/apk/res/navigation/nav_graph.xml
index 3c9dd92..be8c009 100644
--- a/apk/res/navigation/nav_graph.xml
+++ b/apk/res/navigation/nav_graph.xml
@@ -92,6 +92,38 @@
         <argument
             android:name="android.intent.extra.PACKAGE_NAME"
             app:argType="string" />
+        <action
+            android:id="@+id/action_connectedApp_to_appData"
+            app:destination="@id/appDataFragment" />
+    </fragment>
+
+    <fragment
+        android:id="@+id/appDataFragment"
+        android:label="@string/app_data_title"
+        android:name="com.android.healthconnect.controller.data.appdata.AppDataFragment">
+        <argument
+            android:name="android.intent.extra.PACKAGE_NAME"
+            app:argType="string" />
+        <action
+            android:id="@+id/action_appData_to_appEntries"
+            app:destination="@id/appEntriesFragment" />
+    </fragment>
+
+    <fragment
+        android:id="@+id/appEntriesFragment"
+        android:label="App entries"
+        android:name="com.android.healthconnect.controller.data.entries.AppEntriesFragment">
+        <argument
+            android:name="android.intent.extra.PACKAGE_NAME"
+            app:argType="string" />
+        <action
+            android:id="@+id/action_appEntriesFragment_to_dataEntryDetailsFragment"
+            app:destination="@id/dataEntryDetailsFragment" />
+    </fragment>
+
+    <fragment
+        android:id="@+id/dataEntryDetailsFragment" android:name="com.android.healthconnect.controller.entrydetails.DataEntryDetailsFragment"
+        android:label="@string/entry_details_title" >
     </fragment>
 
     <fragment
@@ -132,7 +164,22 @@
     <fragment
         android:id="@+id/dataSourcesFragment"
         android:name="com.android.healthconnect.controller.datasources.DataSourcesFragment"
-        android:label="@string/data_sources_and_priority_title"/>
+        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>
 
     <fragment
         android:id="@+id/autoDeleteFragment"
diff --git a/apk/res/values-af/strings.xml b/apk/res/values-af/strings.xml
index 8b9ff2b..fb602d2 100644
--- a/apk/res/values-af/strings.xml
+++ b/apk/res/values-af/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"toegang tot jou gesondheiddata"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Lees kalorieë verbrand"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Laat die app toe om die kalorieë verbrand te lees"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Agtergrondlees"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"agtergrondlees"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Lees gesondheidsdata in die agtergrond"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Aktiewe kalorieë verbrand"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"aktiewe kalorieë verbrand"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Lees aktiewe kalorieë verbrand"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"oefenroete"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Skryf oefenroete"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Lees oefenroete"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Lees alle oefenroetes"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Afstand"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"afstand"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Lees afstand"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Geen van jou apps sal toegang tot nuwe data kan kry of dit by Health Connect kan voeg nie. Dit vee nie enige bestaande data uit nie.\n\nAs hierdie app enige ander toestemmings soos ligging, kamera of mikrofoon het, sal dit nie geraak word nie."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Verwyder alles"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Bestuur app"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Sien appdata"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Vee appdata uit"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Onaktiewe apps"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Hierdie apps het nie meer toegang nie, maar het steeds data wat in Health Connect geberg is"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Geen appbronne nie"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Bronne sal hier gewys word nadat jy apptoestemmings gegee het om <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>-data te skryf."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Week"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Maand"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Hierdie week"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Verlede week"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Hierdie maand"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Verlede maand"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Inskrywings"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Toegang"</string>
 </resources>
diff --git a/apk/res/values-am/strings.xml b/apk/res/values-am/strings.xml
index 4717676..f39dbd2 100644
--- a/apk/res/values-am/strings.xml
+++ b/apk/res/values-am/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"የጤና ውሂብዎን ይድረሱበት"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"የተቃጠሉ ካሎሪዎችን ያነባል"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"መተግበሪያዎቹ የተቃጠሉትን ካሎሪዎች እንዲያነቡ ያስችላቸዋል"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"የዳራ ንባብ"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"የዳራ ንባብ"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"በዳራ ውስጥ የጤና ውሂብ ማንበብ"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"የተቃጠሉ ገቢር ካሎሪዎች"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"የተቃጠሉ ገቢር ካሎሪዎች"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"የተቃጠሉ ገቢር ካሎሪዎችን ያነባል"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"የልምምድ መስመር"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"የልምምድ አቅጣጫን መጻፍ"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"የአካላዊ ልምምድ አቅጣጫን ያንብቡ"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"ሁሉንም የልምምድ መስመሮች ያንብቡ"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"ርቀት"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"ርቀት"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"ርቀትን ያነባል"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"ማናቸውም መተግበሪያዎችዎ አዲስ ውሂብን መድረስ ወይም ወደ የጤና አገናኝ ማከል አይችሉም። ይህ ማንኛውንም ነባር ውሂብ አይሰርዝም።\n\nይህ እንደ አካባቢ፣ ካሜራ ወይም ማይክሮፎን ያሉ ይህ መተግበሪያ ሊኖሩት የሚችሉት ሌሎች ፈቃዶች ላይ ተጽዕኖ አያሳድርም።"</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"ሁሉንም አስወግድ"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"መተግበሪያን ያስተዳድሩ"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"የመተግበሪያ ውሂብን አሳይ"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"የመተግበሪያ ውሂብን ሰርዝ"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"የቦዘኑ መተግበሪያዎች"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"እነዚህ መተግበሪያዎች ከእንግዲህ መዳረሻ የላቸውም ነገር ግን አሁንም በጤና አገናኝ ውስጥ የተከማቸ ውሂብ አላቸው"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"ምንም የመተግበሪያ ምንጮች የሉም"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"አንዴ የ<xliff:g id="CATEGORY_NAME">%1$s</xliff:g> ውሂብን እንዲጽፍ የመተግበሪያ ፈቃዶችን ከሰጡ ምንጮች እዚህ ይታያሉ።"</string>
     <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="app_data_title" msgid="6499967982291000837">"የመተግበሪያ ውሂብ"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"ወደ የጤና አገናኝ መዳረሻ ካላቸው መተግበሪያዎች ያለ ውሂብ እዚህ ይታያል"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"ቀን"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"ሳምንት"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"ወር"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"በዚህ ሳምንት"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"ያለፈው ሳምንት"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"በዚህ ወር"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"ያለፈው ወር"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"ግቤቶች"</string>
+    <string name="tab_access" msgid="7818197975407243701">"መዳረሻ"</string>
 </resources>
diff --git a/apk/res/values-ar/strings.xml b/apk/res/values-ar/strings.xml
index c550a21..e544c1b 100644
--- a/apk/res/values-ar/strings.xml
+++ b/apk/res/values-ar/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"الوصول إلى البيانات الصحية"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"قراءة السعرات الحرارية المحروقة"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"يسمح هذا الإذن للتطبيق بقراءة السعرات الحرارية المحروقة."</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"القراءة في الخلفية"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"القراءة في الخلفية"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"قراءة البيانات الصحية في الخلفية"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"السعرات الحرارية المحروقة نتيجة ممارسة نشاط بدني"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"السعرات الحرارية المحروقة نتيجة ممارسة نشاط بدني"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"قراءة بيانات السعرات الحرارية المحروقة نتيجة ممارسة نشاط بدني"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"مسار التمرين الرياضي"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"كتابة مسار التمرين الرياضي"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"قراءة مسار التمرين الرياضي"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"قراءة جميع مسارات التمارين الرياضية"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"المسافة"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"المسافة"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"قراءة بيانات المسافة"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"لن يتمكّن أي من تطبيقاتك من الوصول إلى بيانات تطبيق Health Connect أو إضافة بيانات جديدة إليه. لا يؤدي هذا الإجراء إلى حذف أي بيانات حالية.\n\nلا يؤثر هذا الإجراء في الأذونات الأخرى التي قد يمتلكها هذا التطبيق، مثلاً الموقع الجغرافي أو الكاميرا أو الميكروفون."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"إزالة الكل"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"إدارة التطبيق"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"عرض بيانات التطبيق"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"حذف بيانات التطبيق"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"التطبيقات غير النشطة"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"لن تتمكن هذه التطبيقات من الوصول إلى البيانات بعد ذلك، ولكن لا يزال لديها بيانات مخزَّنة في تطبيق Health Connect."</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"ما مِن مصادر للتطبيقات."</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"بعد أن يتم منح أذونات التطبيقات لكتابة بيانات <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>، ستظهر لك المصادر هنا."</string>
     <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="app_data_title" msgid="6499967982291000837">"بيانات التطبيق"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"ستظهر هنا البيانات الواردة من التطبيقات التي يمكنها الوصول إلى Health Connect."</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"يوم"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"أسبوع"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"شهر"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"الأسبوع الحالي"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"الأسبوع الماضي"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"هذا الشهر"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"الشهر الماضي"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"الإدخالات"</string>
+    <string name="tab_access" msgid="7818197975407243701">"إذن الوصول"</string>
 </resources>
diff --git a/apk/res/values-as/strings.xml b/apk/res/values-as/strings.xml
index 799aa0c..2e816b5 100644
--- a/apk/res/values-as/strings.xml
+++ b/apk/res/values-as/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"আপোনাৰ স্বাস্থ্য সম্পৰ্কীয় ডেটাৰ এক্সেছ"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"কেল’ৰি খৰচৰ ডেটা পঢ়ক"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"এপ্‌টোক কেল’ৰি খৰচৰ পৰিমাণ পঢ়িবলৈ দিয়ে"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"নেপথ্যত পঢ়াৰ সুবিধা"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"নেপথ্যত পঢ়াৰ সুবিধা"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"নেপথ্যত স্বাস্থ্য সম্পৰ্কীয় ডেটা পঢ়ক"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"খৰচ হোৱা সক্ৰিয় কেল’ৰি"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"খৰচ হোৱা সক্ৰিয় কেল’ৰি"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"খৰচ হোৱা সক্ৰিয় কেল’ৰিৰ ডেটা পঢ়ক"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"ব্যায়াম কৰাৰ পথ"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"ব্যায়াম কৰাৰ পথটো লিখক"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"ব্যায়াম কৰাৰ পদ্ধতি পঢ়ক"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"আপোনাৰ আটাইবোৰ ব্যায়ামৰ পথ পঢ়ক"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"দূৰত্ব"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"দূৰত্ব"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"দূৰত্বৰ ডেটা পঢ়ক"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"আপোনাৰ কোনো এপে Health Connectৰ কোনো ডেটা এক্সেছ কৰিব নোৱাৰিব অথবা তাত নতুন ডেটা যোগ দিব নোৱাৰিব। ই ইতিমধ্যে থকা কোনো ডেটা নমচে।\n\nএইটোৱে এই এপ্‌টোৰ ওচৰত থাকিব পৰা অৱস্থান, কেমেৰা অথবা মাইক্ৰ’ফ’নৰ দৰে অন্য অনুমতিসমূহত প্ৰভাৱ নেপেলায়।"</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"আটাইবোৰ আঁতৰাওক"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"এপ্ পৰিচালনা কৰক"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"এপৰ ডেটা চাওক"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"এপৰ ডেটা মচক"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"নিষ্ক্ৰিয় এপ্‌সমূহ"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"এই এপ্‌সমূহৰ ওচৰত আৰু এক্সেছ নাই, কিন্তু তথাপি Health Connectত এইসমূহৰ ডেটা ষ্ট’ৰ কৰা আছে"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"এপৰ কোনো উৎস নাই"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"আপুনি এপক <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>ৰ ডেটা লিখিবলৈ অনুমতি দিলে, তাৰ উৎসসমূহ ইয়াত দেখা পোৱা যাব।"</string>
     <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="app_data_title" msgid="6499967982291000837">"এপৰ ডেটা"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"Health Connectৰ এক্সেছ থকা এপ্‌সমূহৰ ডেটা ইয়াত দেখুওৱা হ’ব"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"দিন"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"সপ্তাহ"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"মাহ"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"এই সপ্তাহ"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"যোৱা সপ্তাহ"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"এই মাহ"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"যোৱা মাহ"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"প্ৰৱিষ্টি"</string>
+    <string name="tab_access" msgid="7818197975407243701">"এক্সেছ"</string>
 </resources>
diff --git a/apk/res/values-az/strings.xml b/apk/res/values-az/strings.xml
index 3781f41..04be0b9 100644
--- a/apk/res/values-az/strings.xml
+++ b/apk/res/values-az/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"sağlamlıq datanıza giriş"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Sərf edilmiş kalorini oxumaq"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Tətbiqə sərf edilmiş kaloriləri oxumağa imkan verir"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Arxa fonda oxuma"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"arxa fonda oxuma"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Arxa fonda sağlamlıq datasının oxunması"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Sərf edilmiş aktiv kalori"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"sərf edilmiş aktiv kalori"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Sərf edilmiş aktiv kalorini oxumaq"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"məşq marşrutu"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Məşq marşrutunu yazmaq"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Məşq marşrutunu oxuyun"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Bütün məşq marşrutlarının oxunması"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Məsafə"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"məsafə"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Məsafəni oxumaq"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Heç bir tətbiq Health Connect-ə giriş edə və ya yeni data əlavə edə bilməyəcək. Mövcud data silinməyəcək.\n\nTətbiqin məkan, kamera və ya mikrofon kimi digər icazələrinə təsir etmir."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Hamısını silin"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Tətbiqi idarə edin"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Tətbiq datasına baxın"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Tətbiq datasını silin"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"İnaktiv tətbiqlər"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Bu tətbiqlərin artıq girişi yoxdur, lakin hələ də Health Connect\'də data saxlayırlar"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Tətbiq mənbəyi yoxdur"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"<xliff:g id="CATEGORY_NAME">%1$s</xliff:g> datasını yazmaq üçün tətbiq icazələri verdikdən sonra mənbələr burada görünəcək."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Həftə"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Ay"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Bu həftə"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Keçən həftə"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Bu ay"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Keçən ay"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Daxiletmələr"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Giriş"</string>
 </resources>
diff --git a/apk/res/values-b+sr+Latn/strings.xml b/apk/res/values-b+sr+Latn/strings.xml
index 4adeb59..2a6878e 100644
--- a/apk/res/values-b+sr+Latn/strings.xml
+++ b/apk/res/values-b+sr+Latn/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"pristup podacima o zdravlju"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Čitanje potrošenih kalorija"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Dozvoljava aplikaciji da čita potrošene kalorije"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Čitanje u pozadini"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"čitanje u pozadini"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Čitaj podatke o zdravlju u pozadini"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Utrošene aktivne kalorije"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"utrošene aktivne kalorije"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Čitanje utrošenih aktivnih kalorija"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"ruta vežbanja"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Upisivanje rute vežbanja"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Čitanje rute vežbanja"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Čitaj sve rute vežbanja"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Razdaljina"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"razdaljina"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Čitanje podataka o razdaljini"</string>
@@ -227,10 +231,10 @@
     <string name="oxygen_saturation_lowercase_label" msgid="7264179897533866327">"zasićenost kiseonikom"</string>
     <string name="oxygen_saturation_read_content_description" msgid="4756434113425028212">"Čitanje podataka o zasićenosti kiseonikom"</string>
     <string name="oxygen_saturation_write_content_description" msgid="7189901097196830875">"Upisivanje podataka o zasićenosti kiseonikom"</string>
-    <string name="respiratory_rate_uppercase_label" msgid="4609498171205294389">"Respiratorna brzina"</string>
-    <string name="respiratory_rate_lowercase_label" msgid="8138249029197360098">"respiratorna brzina"</string>
-    <string name="respiratory_rate_read_content_description" msgid="8545898979648419722">"Čitanje podataka o respiratornoj brzini"</string>
-    <string name="respiratory_rate_write_content_description" msgid="7689533746809591931">"Upisivanje podataka o respiratornoj brzini"</string>
+    <string name="respiratory_rate_uppercase_label" msgid="4609498171205294389">"Brzina disanja"</string>
+    <string name="respiratory_rate_lowercase_label" msgid="8138249029197360098">"brzina disanja"</string>
+    <string name="respiratory_rate_read_content_description" msgid="8545898979648419722">"Čitanje podataka o brzini disanja"</string>
+    <string name="respiratory_rate_write_content_description" msgid="7689533746809591931">"Upisivanje podataka o brzini disanja"</string>
     <string name="resting_heart_rate_uppercase_label" msgid="5700827752396195453">"Puls u mirovanju"</string>
     <string name="resting_heart_rate_lowercase_label" msgid="4533866739695973169">"puls u mirovanju"</string>
     <string name="resting_heart_rate_read_content_description" msgid="1068160055773401020">"Čitanje podataka o pulsu u mirovanju"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Nijedna aplikacija neće moći da pristupa podacima Povezivanja zdravlja ni da dodaje nove podatke u njega. Time se ne brišu postojeći podaci.\n\nTo ne utiče na druge dozvole koje ova aplikacija može da ima, poput lokacije, kamere ili mikrofona."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Ukloni sve"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Upravljajte aplikacijom"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Prikaži podatke aplikacija"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Izbriši podatke aplikacija"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Neaktivne aplikacije"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Ove aplikacije više nemaju pristup, ali i dalje imaju sačuvane podatke u Povezivanju zdravlja"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Nema izvora aplikacija"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Kada date dozvole za aplikacije za upisivanje podataka iz kategorije <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>, izvori će se prikazivati ovde."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Nedelja"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Mesec"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Ove nedelje"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Prošla nedelja"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Ovog meseca"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Prošli mesec"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Unosi"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Pristup"</string>
 </resources>
diff --git a/apk/res/values-be/strings.xml b/apk/res/values-be/strings.xml
index fa3f2df..2dea323 100644
--- a/apk/res/values-be/strings.xml
+++ b/apk/res/values-be/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"доступ да даных, датычных здароўя"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Счытваць даныя пра калорыі"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Дазваляе праграме счытваць даныя пра зрасходаваныя калорыі"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Чытанне ў фонавым рэжыме"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"чытанне ў фонавым рэжыме"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Чытаць даныя пра здароўе ў фонавым рэжыме"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Зрасходаваныя актыўныя калорыі"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"зрасходаваныя актыўныя калорыі"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Счытваць даныя пра зрасходаваныя актыўныя калорыі"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"маршрут трэніроўкі"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Запісваць маршрут трэніроўкі"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Азнаёміцца з праграмай трэніроўкі"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Чытаць усе маршруты трэніровак"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Адлегласць"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"адлегласць"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Счытваць даныя пра адлегласць"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Ні адна з вашых праграм не зможа атрымліваць доступ да даных у праграме \"Здароўе і спорт\" ці дадаваць іх. Даныя, якія ўжо існуюць, застануцца.\n\nГэта не паўплывае на іншыя дазволы, якія можа мець гэтая праграма, напрыклад, на доступ да геаданых, камеры ці мікрафона."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Выдаліць усе"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Кіраванне праграмай"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Паглядзець даныя праграмы"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Выдаліць даныя праграмы"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Неактыўныя праграмы"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"У гэтых праграм больш няма доступу да праграмы \"Здароўе і спорт\", аднак іх даныя па-ранейшаму там захоўваюцца"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Няма праграм – крыніц даных"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Тут у якасці крыніц будуць паказвацца праграмы, якім вы дазволіце запісваць даныя катэгорыі \"<xliff:g id="CATEGORY_NAME">%1$s</xliff:g>\"."</string>
     <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="app_data_title" msgid="6499967982291000837">"Даныя праграмы"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"Тут будуць паказвацца даныя з праграм, у якіх ёсць доступ да \"Здароўя і спорта\""</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"Дзень"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"Тыдзень"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Месяц"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Гэты тыдзень"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Мінулы тыдзень"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Гэты месяц"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Апошні месяц"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Запісы"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Доступ"</string>
 </resources>
diff --git a/apk/res/values-bg/strings.xml b/apk/res/values-bg/strings.xml
index 093646e..6b18898 100644
--- a/apk/res/values-bg/strings.xml
+++ b/apk/res/values-bg/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"достъп до здравните ви данни"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Четене на изгорените калории"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Разрешава на приложението да чете изгорените калории"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Четене на заден план"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"четене на заден план"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Четене на здравните данни на заден план"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Изгорени калории от активност"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"изгорени калории от активност"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Четене на изгорените калории от активност"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"маршрут за упражненията"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Записване на маршрута за упражненията"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Четене на маршрута за упражненията"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Четене на всички маршрути за упражнения"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Разстояние"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"разстояние"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Четене на разстоянието"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Нито едно от тези приложения няма да има достъп до Health Connect, нито ще може да добавя нови данни. Това действие няма да изтрие съществуващите данни.\n\nТова няма да засегне другите разрешения, с които приложението може да разполага, като например за достъп до местоположението, камерата или микрофона."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Премахване на всички"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Управление на приложението"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Вижте данните от приложението"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Изтриване на данните от приложението"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Неактивни приложения"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Тези приложения вече нямат достъп, но техни данни все още се съхраняват в Health Connect"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Няма приложения източници"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"След като разрешите на приложения да записват данни за <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>, източниците ще се показват тук."</string>
     <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="app_data_title" msgid="6499967982291000837">"Данни от приложението"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"Тук ще се показват данните от приложенията, които имат достъп до Health Connect"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"Ден"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"Седмица"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Месец"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Тази седмица"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Последната седмица"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Този месец"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Последният месец"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Записи"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Достъп"</string>
 </resources>
diff --git a/apk/res/values-bn/strings.xml b/apk/res/values-bn/strings.xml
index 239deed..001d1c3 100644
--- a/apk/res/values-bn/strings.xml
+++ b/apk/res/values-bn/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"আপনার স্বাস্থ্য সংক্রান্ত ডেটা অ্যাক্সেস করুন"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"ক্যালোরি খরচের ডেটা দেখা"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"কত ক্যালোরি খরচ হয়েছে সেই সংক্রান্ত ডেটা"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"ব্যাকগ্রাউন্ড পড়া"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"ব্যাকগ্রাউন্ড পড়া"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"ব্যাকগ্রাউন্ডে স্বাস্থ্য সম্পর্কিত ডেটা পড়ুন"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"অ্যাক্টিভিটির সময় খরচ হওয়া ক্যালোরি সংক্রান্ত ডেটা"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"অ্যাক্টিভিটির সময় খরচ হওয়া ক্যালোরি সংক্রান্ত ডেটা"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"অ্যাক্টিভিটির সময় কতটা ক্যালোরি খরচ হয়েছে সেই সংক্রান্ত ডেটা দেখার অনুমতি দিন"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"ব্যায়ামের পদ্ধতি"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"ব্যায়ামের পদ্ধতি লিখুন"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"ব্যায়ামের রুটিন দেখা"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"আপনার ব্যায়াম করার সমস্ত উপায় পড়ার অনুমতি দিন"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"দূরত্ব"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"দূরত্ব"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"দূরত্ব সংক্রান্ত ডেটা দেখুন"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"আপনার কোনও অ্যাপ আর Health Connect-এর ডেটা অ্যাক্সেস বা এতে নতুন ডেটা যোগ করতে পারবে না। এটি আগে সেভ থাকা কোনও ডেটা মুছে ফেলে না।\n\nএই অ্যাপের থাকতে পারে এমন অন্যান্য অনুমতির উপরে এটি প্রভাব ফেলে না, যেমন লোকেশন, ক্যামেরা বা মাইক্রোফোন।"</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"সবকটি সরান"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"অ্যাপ ম্যানেজ করা"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"অ্যাপ ডেটা দেখুন"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"অ্যাপ ডেটা মুছে দিন"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"বন্ধ থাকা অ্যাপ"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"এইসব অ্যাপের আর অ্যাক্সেস না থাকলেও, এখনও এদের ডেটা Health Connect-এ সেভ করা আছে"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"কোনও অ্যাপের সোর্স নেই"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"<xliff:g id="CATEGORY_NAME">%1$s</xliff:g> ডেটা লেখার জন্য আপনি অ্যাপকে অনুমতি দিলে, এখানে সোর্স দেখা যাবে।"</string>
     <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="app_data_title" msgid="6499967982291000837">"অ্যাপ ডেটা"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"Health Connect-এর অ্যাক্সেস সহ অ্যাপ থেকে পাওয়া ডেটা এখানে দেখানো হবে"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"দিনের"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"সপ্তাহের"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"মাসের"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"এই সপ্তাহ"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"গত সপ্তাহের"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"এই মাস"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"গত মাসের"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"এন্ট্রি"</string>
+    <string name="tab_access" msgid="7818197975407243701">"অ্যাক্সেস রয়েছে"</string>
 </resources>
diff --git a/apk/res/values-bs/strings.xml b/apk/res/values-bs/strings.xml
index e8f3fc2..523270a 100644
--- a/apk/res/values-bs/strings.xml
+++ b/apk/res/values-bs/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"pristupanje podacima o zdravlju"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Čitanje potrošenih kalorija"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Dozvoljava aplikaciji da čita potrošene kalorije"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Čitanje u pozadini"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"čitanje u pozadini"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Čitanje podataka o zdravlju u pozadini"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Kalorije potrošene kroz aktivnost"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"kalorije potrošene kroz aktivnost"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Čitanje kalorija potrošenih kroz aktivnost"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"ruta za vježbanje"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Zapisivanje rute za vježbanje"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Čitanje rute vježbe"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Čitanje svih ruta vježbanja"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Udaljenost"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"udaljenost"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Čitanje udaljenosti"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Nijedna od vaših aplikacija neće moći dodati nove podatke u Health Connect niti će im moći pristupiti. Ovim se ne brišu postojeći podaci.\n\nOvo ne utiče na druga odobrenja koja ova aplikacija može imati, kao što su lokacija, kamera ili mikrofon."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Ukloni sve"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Upravljajte aplikacijom"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Pogledajte podatke aplikacije"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Izbriši podatke aplikacije"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Neaktivne aplikacije"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Ove aplikacije više nemaju pristup, ali su im podaci i dalje pohranjeni u Health Connectu"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Nema nijednog izvora aplikacija"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Nakon što dodate odobrenja za aplikaciju da zapisuje podatke iz kategorije <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>, izvori će se prikazivati ovdje."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Sedmica"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Mjesec"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Ova sedmica"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Prošla sedmica"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Ovaj mjesec"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Prošli mjesec"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Unosi"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Pristup"</string>
 </resources>
diff --git a/apk/res/values-ca/strings.xml b/apk/res/values-ca/strings.xml
index 03b59dc..b9d99b9 100644
--- a/apk/res/values-ca/strings.xml
+++ b/apk/res/values-ca/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"accedir a les teves dades de salut"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Llegir les calories cremades"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Permet que l\'aplicació llegeixi les calories cremades"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Lectura en segon pla"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"lectura en segon pla"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Llegeix dades de salut en segon pla"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Calories actives cremades"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"calories actives cremades"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Llegir calories actives cremades"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"ruta d\'exercici"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Escriu la ruta d\'exercici"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Llegeix la ruta d\'exercici"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Llegeix totes les rutes d\'exercici"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Distància"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"distància"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Llegir la distància"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Cap de les teves aplicacions no podrà afegir noves dades a Salut connectada ni accedir-hi. Aquesta acció no suprimeix cap de les dades existents.\n\nAixò no afecta els altres permisos que aquesta aplicació pugui tenir, com ara els d\'accés a la ubicació, a la càmera o al micròfon."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Suprimeix-ho tot"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Gestiona l\'aplicació"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Mostra les dades de l\'aplicació"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Suprimeix les dades de l\'aplicació"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Aplicacions inactives"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Aquestes aplicacions ja no tenen accés, però encara tenen dades emmagatzemades a Salut connectada"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"No hi ha cap font d\'aplicacions"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Quan concedeixis permisos a les aplicacions per escriure dades de <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>, les fonts es mostraran aquí."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Setmana"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Mes"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Aquesta setmana"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"La setmana passada"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Aquest mes"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"El mes passat"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Entrades"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Accedeix"</string>
 </resources>
diff --git a/apk/res/values-cs/strings.xml b/apk/res/values-cs/strings.xml
index 7913cea..bb6cf7f 100644
--- a/apk/res/values-cs/strings.xml
+++ b/apk/res/values-cs/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"přístup ke zdravotním údajům"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Číst spálené kalorie"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Umožňuje aplikaci číst spálené kalorie"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Čtení na pozadí"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"čtení na pozadí"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Čtení zdravotních údajů na pozadí"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Kalorie spálené aktivitou"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"kalorie spálené aktivitou"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Číst kalorie spálené aktivitou"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"tréninková trasa"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Zapsat tréninkovou trasu"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Čtení průběhu cvičení"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Číst všechny tréninkové trasy"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Vzdálenost"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"vzdálenost"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Číst vzdálenost"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Žádné z vašich aplikací nebudou mít přístup k datům služby Health Connect a nebudou moci přidávat nová. Touto akcí nesmažete žádná existující data.\n\nTato akce nebude mít vliv na ostatní oprávnění, která aplikace má, jako je přístup k poloze, fotoaparátu nebo mikrofonu."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Odstranit vše"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Spravovat aplikaci"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Zobrazit data aplikace"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Smazat data aplikace"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Neaktivní aplikace"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Tyto aplikace už nemají přístup, ale stále mají uložená data ve službě Health Connect"</string>
@@ -674,7 +679,7 @@
     <string name="help_and_feedback" msgid="4772169905005369871">"Nápověda a zpětná vazba"</string>
     <string name="cant_see_all_your_apps_description" msgid="7344859063463536472">"Pokud nějakou nainstalovanou aplikaci nevidíte, zřejmě ještě není kompatibilní se službou Health Connect"</string>
     <string name="things_to_try" msgid="8200374691546152703">"Co byste mohli vyzkoušet"</string>
-    <string name="check_for_updates" msgid="3841090978657783101">"Kontrola dostupnosti aktualizací"</string>
+    <string name="check_for_updates" msgid="3841090978657783101">"Zkontrolovat dostupnost aktualizací"</string>
     <string name="check_for_updates_description" msgid="1347667778199095160">"Ověřte, zda jsou nainstalované aplikace aktuální"</string>
     <string name="see_all_compatible_apps" msgid="6791146164535475726">"Zobrazit všechny kompatibilní aplikace"</string>
     <string name="see_all_compatible_apps_description" msgid="2092325337403254491">"Najít aplikace na Google Play"</string>
@@ -757,7 +762,7 @@
     <string name="app_update_needed_banner_title" msgid="4724335956851853802">"Je vyžadována aktualizace"</string>
     <string name="app_update_needed_banner_description_single" msgid="2229935331303234217">"Aplikace <xliff:g id="APP_NAME">%1$s</xliff:g> musí být aktualizovaná, aby fungovala se službou Health Connect"</string>
     <string name="app_update_needed_banner_description_multiple" msgid="1523113182062764912">"Některé aplikace musí být aktualizované, aby fungovaly se službou Health Connect"</string>
-    <string name="app_update_needed_banner_button" msgid="8223115764065649627">"Kontrola dostupnosti aktualizací"</string>
+    <string name="app_update_needed_banner_button" msgid="8223115764065649627">"Zkontrolovat dostupnost aktualizací"</string>
     <string name="migration_pending_permissions_dialog_title" msgid="6019552841791757048">"Integrace Health Connect"</string>
     <string name="migration_pending_permissions_dialog_content" msgid="6350115816948005466">"Služba Health Connect je připravena na integraci se systémem Android. Pokud aplikaci <xliff:g id="APP_NAME">%1$s</xliff:g> dáte přístup teď, některé funkce budou k dispozici až po dokončení integrace."</string>
     <string name="migration_pending_permissions_dialog_content_apps" msgid="6417173899016940664">"Služba Health Connect je připravena na integraci se systémem Android. Pokud aplikacím dáte přístup teď, některé funkce budou k dispozici až po dokončení integrace."</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Žádné zdroje aplikací"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Jakmile aplikacím udělíte oprávnění k zápisu dat z kategorie <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>, zobrazí se zde zdroje."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Týden"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Měsíc"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Tento týden"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Minulý týden"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Tento měsíc"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Minulý měsíc"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Záznamy"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Přístup"</string>
 </resources>
diff --git a/apk/res/values-da/strings.xml b/apk/res/values-da/strings.xml
index d8bda92..98c58b4 100644
--- a/apk/res/values-da/strings.xml
+++ b/apk/res/values-da/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"få adgang til dine sundhedsdata"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Læs forbrændte kalorier"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Tillader, at appen læser de forbrændte kalorier"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Læsning i baggrunden"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"læsning i baggrunden"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Læs sundhedsdata i baggrunden"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Aktive forbrændte kalorier"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"aktive forbrændte kalorier"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Læs aktive forbrændte kalorier"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"træningsrute"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Skriv træningsrute"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Læs træningsrute"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Giv tilladelse til at læse alle træningsruter"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Afstand"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"afstand"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Læs afstand"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Ingen af dine apps kan få adgang til eller føje nye data til Health Connect. Dette sletter ikke eksisterende data.\n\nDette påvirker ikke evt. andre tilladelser, som appen har, f.eks. adgang til lokation, kamera eller mikrofon."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Fjern alle"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Administrer app"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Se appdata"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Slet appdata"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Inaktive apps"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Disse apps har ikke længere adgang, men har stadig data, der er gemt i Health Connect"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Ingen appkilder"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Når du har givet apptilladelser til at skrive <xliff:g id="CATEGORY_NAME">%1$s</xliff:g> data, vises kilderne her."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Uge"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Måned"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Denne uge"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Sidste uge"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Denne måned"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Sidste måned"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Poster"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Adgang"</string>
 </resources>
diff --git a/apk/res/values-de/strings.xml b/apk/res/values-de/strings.xml
index b54a5c6..73c875d 100644
--- a/apk/res/values-de/strings.xml
+++ b/apk/res/values-de/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"Auf meine Gesundheitsdaten zugreifen"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Verbrannte Kalorien lesen"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Ermöglicht der App, die verbrannten Kalorien zu lesen"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Daten im Hintergrund abrufen"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"Daten im Hintergrund abrufen"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Gesundheitsdaten im Hintergrund abrufen"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Aktiv verbrannte Kalorien"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"Aktiv verbrannte Kalorien"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Aktiv verbrannte Kalorien lesen"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"Trainingsroute"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Trainingsroute schreiben"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Trainingsroute ansehen"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Alle Trainingsrouten einsehen"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Strecke"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"Strecke"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Strecke lesen"</string>
@@ -242,7 +246,7 @@
     <string name="request_permissions_allow_all" msgid="3419414351406638770">"Alle zulassen"</string>
     <string name="request_permissions_dont_allow" msgid="6375307410951549030">"Nicht erlauben"</string>
     <string name="request_permissions_header_desc" msgid="5561173070722750153">"Du kannst festlegen, welche Daten diese App in Health Connect lesen und schreiben darf"</string>
-    <string name="request_permissions_header_time_frame_desc" msgid="4617392728203291453">"Wenn du der App Schreibzugriff gibst, kann sie neue Daten und solche aus den letzten 30 Tagen lesen"</string>
+    <string name="request_permissions_header_time_frame_desc" msgid="4617392728203291453">"Wenn du der App Lesezugriff gibst, kann sie neue Daten und solche aus den letzten 30 Tagen lesen"</string>
     <string name="request_permissions_header_title" msgid="4264236128614363479">"<xliff:g id="APP_NAME">%1$s</xliff:g> Zugriff auf Health Connect erlauben?"</string>
     <string name="request_permissions_rationale" msgid="6154280355215802538">"Wie <xliff:g id="APP_NAME">%1$s</xliff:g> deine Daten verarbeitet, erfährst du in der <xliff:g id="PRIVACY_POLICY_LINK">%2$s</xliff:g> des Entwicklers"</string>
     <string name="request_permissions_privacy_policy" msgid="228503452643555737">"Datenschutzerklärung"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Deine Apps können dann weder Daten von Health Connect abrufen noch neue hinzufügen. Dadurch werden jedoch keine vorhandenen Daten gelöscht.\n\nDies hat keine Auswirkungen auf andere Berechtigungen, die diese App möglicherweise hat, wie z. B. Standort, Kamera oder Mikrofon."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Alle entfernen"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"App verwalten"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"App-Daten ansehen"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"App-Daten löschen"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Inaktive Apps"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Diese Apps haben keinen Zugriff mehr, aber es sind noch Daten von ihnen in Health Connect gespeichert"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Keine App-Quellen"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Wenn du App-Berechtigungen zum Schreiben von „<xliff:g id="CATEGORY_NAME">%1$s</xliff:g>“-Daten erteilst, werden die Quellen hier angezeigt."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Woche"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Monat"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Diese Woche"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Letzte Woche"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Dieser Monat"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Letzter Monat"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Einträge"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Zugriff"</string>
 </resources>
diff --git a/apk/res/values-el/strings.xml b/apk/res/values-el/strings.xml
index fdf6d62..ae441f5 100644
--- a/apk/res/values-el/strings.xml
+++ b/apk/res/values-el/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"πρόσβαση σε δεδομένα υγείας"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Ανάγνωση θερμίδων που κάηκαν"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Επιτρέπει στην εφαρμογή να διαβάζει τις θερμίδες που καίγονται"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Ανάγνωση στο παρασκήνιο"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"ανάγνωση στο παρασκήνιο"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Ανάγνωση δεδομένων υγείας στο παρασκήνιο"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Ενεργές θερμίδες που έχετε κάψει"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"ενεργές θερμίδες που έχετε κάψει"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Ανάγνωση ενεργών θερμίδων που έχετε κάψει"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"διαδρομή άσκησης"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Εγγραφή διαδρομής άσκησης"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Ανάγνωση διαδρομής άσκησης"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Ανάγνωση όλων των διαδρομών άσκησης"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Απόσταση"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"απόσταση"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Ανάγνωση δεδομένων απόστασης"</string>
@@ -254,7 +258,7 @@
     <string name="navigation_selected_day" msgid="2510843479734091348">"Επιλεγμένη ημέρα"</string>
     <string name="navigation_previous_day" msgid="718353386484938584">"Προηγούμενη ημέρα"</string>
     <string name="default_error" msgid="7966868260616403475">"Παρουσιάστηκε κάποιο πρόβλημα. Δοκιμάστε ξανά."</string>
-    <string name="health_permission_header_description" msgid="7497601695462373927">"Οι εφαρμογές με αυτήν την άδεια μπορούν να διαβάζουν και να γράφουν δεδομένα υγείας και φυσικής κατάστασης."</string>
+    <string name="health_permission_header_description" msgid="7497601695462373927">"Οι εφαρμογές με αυτή την άδεια μπορούν να διαβάζουν και να γράφουν δεδομένα υγείας και φυσικής κατάστασης."</string>
     <string name="connected_apps_text" msgid="1177626440966855831">"Ελέγξτε ποιες εφαρμογές μπορούν να έχουν πρόσβαση στα δεδομένα που είναι αποθηκευμένα στο Health Connect. Πατήστε μια εφαρμογή για να ελέγξετε τα δεδομένα για τα οποία έχει πρόσβαση εγγραφής και ανάγνωσης."</string>
     <string name="connected_apps_section_title" msgid="2415288099612126258">"Η πρόσβαση επιτρέπεται"</string>
     <string name="not_connected_apps_section_title" msgid="452718769894103039">"Η πρόσβαση δεν επιτρέπεται"</string>
@@ -265,9 +269,10 @@
     <string name="no_apps_allowed" msgid="5794833581324128108">"Δεν επιτρέπονται εφαρμογές"</string>
     <string name="no_apps_denied" msgid="743327680286446017">"Δεν υπάρχουν εφαρμογές που απαγορεύονται"</string>
     <string name="permissions_disconnect_all_dialog_title" msgid="27474286046207122">"Κατάργηση πρόσβασης όλων των εφαρμογών;"</string>
-    <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Καμία από τις εφαρμογές σας δεν θα μπορεί να αποκτήσει πρόσβαση στα δεδομένα του Health Connect ή να προσθέσει νέα δεδομένα. Με αυτήν την ενέργεια δεν διαγράφονται τυχόν υπάρχοντα δεδομένα.\n\nΔεν επηρεάζονται άλλες άδειες που ενδέχεται να έχει αυτή η εφαρμογή, όπως οι άδειες για την τοποθεσία, την κάμερα ή το μικρόφωνο."</string>
+    <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Καμία από τις εφαρμογές σας δεν θα μπορεί να αποκτήσει πρόσβαση στα δεδομένα του Health Connect ή να προσθέσει νέα δεδομένα. Με αυτή την ενέργεια δεν διαγράφονται τυχόν υπάρχοντα δεδομένα.\n\nΔεν επηρεάζονται άλλες άδειες που ενδέχεται να έχει αυτή η εφαρμογή, όπως οι άδειες για την τοποθεσία, την κάμερα ή το μικρόφωνο."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Κατάργηση όλων"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Διαχείριση εφαρμογής"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Εμφάνιση δεδομένων εφαρμογών"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Διαγραφή δεδομένων εφαρμογής"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Ανενεργές εφαρμογές"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Αυτές οι εφαρμογές δεν έχουν πλέον πρόσβαση, αλλά έχουν ακόμη αποθηκευμένα δεδομένα στο Health Connect"</string>
@@ -327,7 +332,7 @@
     <string name="confirming_question_data_type_from_app_all" msgid="8361163993548510509">"Οριστική διαγραφή όλων των δεδομένων <xliff:g id="DATA_TYPE">%1$s</xliff:g> που προστέθηκαν από την εφαρμογή <xliff:g id="APP_DATA">%2$s</xliff:g>;"</string>
     <string name="confirming_question_single_entry" msgid="330919962071369305">"Οριστική διαγραφή αυτής της καταχώρισης;"</string>
     <string name="confirming_question_message" msgid="2934249835529079545">"Οι συνδεδεμένες εφαρμογές δεν θα έχουν πλέον πρόσβαση σε αυτά τα δεδομένα από το Health Connect"</string>
-    <string name="confirming_question_message_menstruation" msgid="5286956266565962430">"Με αυτήν την ενέργεια θα διαγραφούν όλες οι καταχωρίσεις εμμηνόρροιας από τις <xliff:g id="START_DATE">%1$s</xliff:g> έως τις <xliff:g id="END_DATE">%2$s</xliff:g>."</string>
+    <string name="confirming_question_message_menstruation" msgid="5286956266565962430">"Με αυτή την ενέργεια θα διαγραφούν όλες οι καταχωρίσεις εμμηνόρροιας από τις <xliff:g id="START_DATE">%1$s</xliff:g> έως τις <xliff:g id="END_DATE">%2$s</xliff:g>."</string>
     <string name="confirming_question_delete_button" msgid="1999996759507959985">"Διαγραφή"</string>
     <string name="confirming_question_go_back_button" msgid="9037523726124648221">"Επιστροφή"</string>
     <string name="delete_dialog_success_got_it_button" msgid="8047812840310612293">"Τέλος"</string>
@@ -729,7 +734,7 @@
     <string name="migration_update_needed_screen_title" msgid="3260466598312877429">"Απαιτείται ενημέρωση"</string>
     <string name="migration_update_needed_screen_details" msgid="7984745102006782603">"Γίνεται ενσωμάτωση του Health Connect με το σύστημα Android, έτσι ώστε να μπορείτε να αποκτήσετε πρόσβαση σε αυτό απευθείας από τις ρυθμίσεις σας."</string>
     <string name="update_button" msgid="4544529019832009496">"Ενημέρωση"</string>
-    <string name="migration_update_needed_notification_content" msgid="478899618719297517">"Ξεκινήστε αυτήν την ενημέρωση, έτσι ώστε το Health Connect να συνεχίσει την ενσωμάτωση με τις ρυθμίσεις συστήματός σας"</string>
+    <string name="migration_update_needed_notification_content" msgid="478899618719297517">"Ξεκινήστε αυτή την ενημέρωση, έτσι ώστε το Health Connect να συνεχίσει την ενσωμάτωση με τις ρυθμίσεις συστήματός σας"</string>
     <string name="migration_update_needed_notification_action" msgid="1219223694165492000">"Ενημέρωση τώρα"</string>
     <string name="migration_module_update_needed_notification_title" msgid="5428523284357105379">"Απαιτείται ενημέρωση συστήματος"</string>
     <string name="migration_module_update_needed_action" msgid="7211167950758064289">"Πριν συνεχίσετε, ενημερώστε το σύστημα του τηλεφώνου σας."</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Δεν υπάρχουν πηγές εφαρμογών"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Μόλις παραχωρήσετε άδειες εφαρμογών για την εγγραφή δεδομένων <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>, οι πηγές θα εμφανίζονται εδώ."</string>
     <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="app_data_title" msgid="6499967982291000837">"Δεδομένα εφαρμογών"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"Τα δεδομένα από εφαρμογές με πρόσβαση στο Health Connect θα εμφανίζονται εδώ"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"Ημέρα"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"Εβδομάδα"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Μήνας"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Αυτή την εβδομάδα"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Προηγούμενη εβδομάδα"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Αυτόν τον μήνα"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Προηγούμενος μήνας"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Καταχωρίσεις"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Πρόσβαση"</string>
 </resources>
diff --git a/apk/res/values-en-rAU/strings.xml b/apk/res/values-en-rAU/strings.xml
index 4b0c5c8..e835a41 100644
--- a/apk/res/values-en-rAU/strings.xml
+++ b/apk/res/values-en-rAU/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"access your health data"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Read calories burned"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Allows the app to read the calories burned"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Background reading"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"background reading"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Read health data in the background"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Active calories burned"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"active calories burned"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Read active calories burned"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"exercise route"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Write exercise route"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Read exercise route"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Read all exercise routes"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Distance"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"distance"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Read distance"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"None of your apps will be able to access or add new data to Health Connect. This doesn’t delete any existing data.\n\nThis doesn\'t affect other permissions that this app may have, like location, camera or microphone."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Remove all"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Manage app"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"See app data"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Delete app data"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Inactive apps"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"These apps no longer have access, but still have data stored in Health Connect"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"No app sources"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Once you give app permissions to write <xliff:g id="CATEGORY_NAME">%1$s</xliff:g> data, sources will be shown here."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Week"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Month"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"This week"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Last week"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"This month"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Last month"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Entries"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Access"</string>
 </resources>
diff --git a/apk/res/values-en-rCA/strings.xml b/apk/res/values-en-rCA/strings.xml
index c7d9038..c707d17 100644
--- a/apk/res/values-en-rCA/strings.xml
+++ b/apk/res/values-en-rCA/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"access your health data"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Read calories burned"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Allows the app to read the calories burned"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Background reading"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"background reading"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Read health data in the background"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Active calories burned"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"active calories burned"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Read active calories burned"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"exercise route"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Write exercise route"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Read exercise route"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Read all exercise routes"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Distance"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"distance"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Read distance"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"None of your apps will be able to access or add new data to Health Connect. This doesn\'t delete any existing data.\n\nThis doesn\'t affect other permissions this app may have, like location, camera, or microphone."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Remove all"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Manage app"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"See app data"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Delete app data"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Inactive apps"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"These apps no longer have access, but still have data stored in Health Connect"</string>
@@ -783,4 +788,18 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"No app sources"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Once you give app permissions to write <xliff:g id="CATEGORY_NAME">%1$s</xliff:g> data, sources will show here."</string>
     <string name="data_sources_help_link" msgid="7740264923634947915">"How sources &amp; prioritization 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>
+    <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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Week"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Month"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"This week"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Last week"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"This month"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Last month"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Entries"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Access"</string>
 </resources>
diff --git a/apk/res/values-en-rGB/strings.xml b/apk/res/values-en-rGB/strings.xml
index 4b0c5c8..e835a41 100644
--- a/apk/res/values-en-rGB/strings.xml
+++ b/apk/res/values-en-rGB/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"access your health data"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Read calories burned"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Allows the app to read the calories burned"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Background reading"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"background reading"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Read health data in the background"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Active calories burned"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"active calories burned"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Read active calories burned"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"exercise route"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Write exercise route"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Read exercise route"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Read all exercise routes"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Distance"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"distance"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Read distance"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"None of your apps will be able to access or add new data to Health Connect. This doesn’t delete any existing data.\n\nThis doesn\'t affect other permissions that this app may have, like location, camera or microphone."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Remove all"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Manage app"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"See app data"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Delete app data"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Inactive apps"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"These apps no longer have access, but still have data stored in Health Connect"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"No app sources"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Once you give app permissions to write <xliff:g id="CATEGORY_NAME">%1$s</xliff:g> data, sources will be shown here."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Week"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Month"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"This week"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Last week"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"This month"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Last month"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Entries"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Access"</string>
 </resources>
diff --git a/apk/res/values-en-rIN/strings.xml b/apk/res/values-en-rIN/strings.xml
index 4b0c5c8..e835a41 100644
--- a/apk/res/values-en-rIN/strings.xml
+++ b/apk/res/values-en-rIN/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"access your health data"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Read calories burned"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Allows the app to read the calories burned"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Background reading"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"background reading"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Read health data in the background"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Active calories burned"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"active calories burned"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Read active calories burned"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"exercise route"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Write exercise route"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Read exercise route"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Read all exercise routes"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Distance"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"distance"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Read distance"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"None of your apps will be able to access or add new data to Health Connect. This doesn’t delete any existing data.\n\nThis doesn\'t affect other permissions that this app may have, like location, camera or microphone."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Remove all"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Manage app"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"See app data"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Delete app data"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Inactive apps"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"These apps no longer have access, but still have data stored in Health Connect"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"No app sources"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Once you give app permissions to write <xliff:g id="CATEGORY_NAME">%1$s</xliff:g> data, sources will be shown here."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Week"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Month"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"This week"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Last week"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"This month"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Last month"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Entries"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Access"</string>
 </resources>
diff --git a/apk/res/values-en-rXC/strings.xml b/apk/res/values-en-rXC/strings.xml
index b7dcbe6..1e2d87b 100644
--- a/apk/res/values-en-rXC/strings.xml
+++ b/apk/res/values-en-rXC/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‎‏‎‏‏‎‏‏‏‏‏‏‏‏‎‎‏‎‎‎‏‏‏‎‏‏‎‏‏‏‎‏‏‎‎‎‏‎‏‏‏‏‎‎‏‎‎‎‏‏‏‏‎‎‎‏‎‏‎‎‏‎access your health data‎‏‎‎‏‎"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‏‏‏‏‏‏‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‎‎‏‎‎‎‎‎‎‎‎‏‎‏‏‏‎‏‏‏‏‎‎‏‏‏‎‏‎‎‏‏‎‎‏‏‎‏‏‎‏‎‎‎Read calories burned‎‏‎‎‏‎"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‏‏‏‏‏‏‎‏‎‎‎‏‎‎‏‏‎‎‏‎‏‎‏‏‏‎‏‏‏‎‎‏‏‎‎‏‎‏‏‎‎‏‎‏‏‎‎‏‎‎‏‏‏‎‏‎‎‎‎‏‏‎‏‎‎Allows the app to read the calories burned‎‏‎‎‏‎"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‏‏‎‎‏‏‏‏‎‏‏‏‏‏‎‎‏‎‏‎‏‏‎‏‏‏‎‎‏‏‎‎‎‏‎‎‎‎‏‏‏‏‎‎‎‏‎‏‏‏‎‏‏‏‏‏‏‏‏‏‏‎‎‎‎Background reading‎‏‎‎‏‎"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‎‏‎‎‎‏‎‏‎‎‎‏‏‏‏‎‎‎‎‏‎‎‎‏‎‎‎‎‎‏‏‎‏‏‏‏‎‏‏‎‏‎‎‎‏‏‏‎‎‏‏‏‏‎‎‎‏‎‏‎‎‎‎‎background reading‎‏‎‎‏‎"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‏‏‎‎‏‎‎‏‎‎‎‎‏‏‎‎‏‎‎‏‎‏‎‎‎‏‎‏‎‏‎‏‎‏‎‎‎‏‎‏‎‎‏‏‏‎‎‎‎‏‏‎‎‏‎‏‏‎‏‏‏‏‏‎‎Read health data in the background‎‏‎‎‏‎"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‏‏‎‏‎‏‏‎‎‏‏‏‏‎‏‏‎‏‏‎‎‏‎‎‎‏‎‏‏‎‎‏‎‎‎‎‎‏‏‏‎‎‏‎‏‏‎‏‏‏‏‎‎‏‎‎‏‎‎‏‎‎‎‎‎Active calories burned‎‏‎‎‏‎"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‏‏‎‏‏‏‎‏‏‎‎‏‎‏‎‎‎‏‎‎‎‏‎‏‎‏‏‎‏‏‎‏‎‏‏‎‎‏‏‎‎‏‏‏‎‎‏‏‏‏‎‏‏‏‏‏‎‎‏‎‎‎‎‏‎active calories burned‎‏‎‎‏‎"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‏‏‎‏‏‎‎‏‏‎‎‎‎‎‎‏‎‎‎‎‎‏‎‏‏‏‏‎‏‏‎‏‏‏‎‏‎‏‎‎‏‏‏‎‏‎‏‎‎‎‎‎‎‎‎‎‎‎‎‎‏‎‏‎‎Read active calories burned‎‏‎‎‏‎"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‎‏‏‎‏‏‏‎‏‏‏‏‎‏‎‏‏‏‎‎‎‎‏‏‎‏‎‏‎‏‎‎‏‎‏‎‏‏‎‏‏‎‏‎‎‎‎‎‎‎‏‏‎‏‎‎‎‏‏‎‏‎‎‎exercise route‎‏‎‎‏‎"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‎‏‎‏‏‏‎‎‏‎‎‏‏‏‏‏‎‏‏‎‎‏‏‎‎‎‏‏‎‏‏‏‏‎‏‎‏‎‏‎‎‏‏‎‎‎‏‎‏‏‏‎‏‏‎‏‎‎‎‏‏‎Write exercise route‎‏‎‎‏‎"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‏‏‏‏‎‏‎‎‎‏‏‏‏‏‎‏‏‎‎‏‎‏‎‎‎‏‏‏‏‎‏‏‎‏‎‎‎‏‏‎‏‎‎‏‎‎‏‏‏‏‏‏‏‎‎‎‏‏‎‏‎‎‎‎‎Read exercise route‎‏‎‎‏‎"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‏‏‎‏‎‎‎‎‎‏‏‏‏‎‏‎‎‏‎‏‎‎‎‎‎‏‎‏‎‏‏‏‏‏‏‏‎‏‏‎‏‏‎‏‏‏‎‎‏‏‎‏‎‎‎‏‎‏‎‎‏‏‎‏‎Read all exercise routes‎‏‎‎‏‎"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‎‏‏‎‎‏‏‏‎‏‏‎‏‏‏‎‏‎‏‏‏‎‎‎‎‎‎‏‏‏‎‎‏‏‎‏‎‏‏‏‎‎‎‏‎‎‎‎‏‎‏‏‏‎‎‏‏‏‏‎‏‏‎‎Distance‎‏‎‎‏‎"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‎‏‏‏‏‏‏‏‎‏‏‏‏‎‏‏‎‎‏‏‎‏‎‎‏‏‏‎‎‏‎‎‎‏‏‏‏‎‎‏‎‏‏‎‎‏‎‏‏‎‏‎‎‏‎‎‎‎‎‎‎‏‏‎distance‎‏‎‎‏‎"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‏‏‏‏‏‎‎‏‏‏‏‏‎‎‏‎‏‎‎‎‏‎‎‎‎‏‎‎‎‎‏‎‎‏‏‏‏‎‎‎‎‏‎‎‎‏‎‏‎‏‎‎‏‏‎‏‎‏‎‏‏‏‎‏‎Read distance‎‏‎‎‏‎"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‎‏‎‏‎‏‏‏‎‏‏‏‎‏‎‏‏‏‏‏‏‎‏‏‎‎‎‎‎‏‎‏‏‎‎‏‎‎‎‏‏‏‎‎‏‏‏‎‏‏‏‎‎‏‎‏‏‏‎‎‎‏‏‎None of your apps will be able to access or add new data to Health Connect. This doesn\'t delete any existing data.‎‏‎‎‏‏‎\n‎‏‎‎‏‏‏‎‎‏‎‎‏‏‎\n‎‏‎‎‏‏‏‎This doesn\'t affect other permissions this app may have, like location, camera, or microphone.‎‏‎‎‏‎"</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‎‏‏‏‏‎‏‏‎‎‏‏‏‎‏‏‏‏‏‏‎‎‏‏‏‎‏‎‏‎‏‎‏‏‎‎‎‎‏‏‏‏‏‎‏‎‎‏‎‏‎‏‎‏‎‎‏‎‏‎‎‏‎‎Remove all‎‏‎‎‏‎"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‏‏‎‏‏‎‎‎‎‎‏‏‎‏‏‎‎‏‎‎‏‎‎‎‏‏‏‎‎‎‏‎‏‎‎‏‎‏‎‏‏‎‎‎‎‏‎‎‏‏‏‎‏‎‏‏‎‏‏‎‏‎‎‏‎Manage app‎‏‎‎‏‎"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‎‏‏‎‏‏‎‏‏‎‏‎‏‎‎‏‏‏‎‎‎‎‎‏‏‏‎‏‎‏‏‏‎‏‏‏‏‎‎‎‏‏‏‏‏‏‎‏‎‏‎‎‎‎‏‎‏‏‏‎‏‎‎‎See app data‎‏‎‎‏‎"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‏‏‎‏‏‏‏‏‏‎‎‏‏‏‏‏‎‏‏‏‎‏‏‏‏‏‏‎‎‎‏‏‏‎‏‏‏‎‏‎‏‎‏‏‎‏‎‏‎‎‎‏‎‏‏‏‎‏‏‏‎‎‎‎‎Delete app data‎‏‎‎‏‎"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‏‏‏‎‎‏‏‏‏‏‏‏‏‎‏‏‏‏‎‏‎‎‎‏‏‎‏‏‎‎‏‏‎‏‏‏‎‎‎‏‏‎‎‏‏‏‎‎‎‏‏‎‎‏‏‏‎‏‎‎‎‎‏‎‎Inactive apps‎‏‎‎‏‎"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‎‏‎‎‏‎‎‎‎‏‏‏‎‏‏‎‏‏‎‎‎‏‏‎‎‎‎‎‎‎‎‎‏‎‏‎‏‎‎‎‏‎‎‎‎‎‎‎‎‏‎‎‏‏‏‎‏‎‏‎‎‏‏‎These apps no longer have access, but still have data stored in Health Connect‎‏‎‎‏‎"</string>
@@ -783,4 +788,18 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‎‏‏‏‎‏‎‎‏‎‏‏‏‎‎‏‏‏‎‏‏‎‎‎‎‎‏‏‏‏‎‏‎‏‎‏‎‎‏‎‎‏‎‏‎‏‎‎‏‎‏‏‎‎‏‎‎‏‏‎‏‎‎‎No app sources‎‏‎‎‏‎"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‏‏‏‏‏‎‏‏‏‏‏‏‏‎‏‏‏‏‎‎‎‏‎‎‏‎‎‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‎‎‎‏‎‎‏‎‏‏‏‎‏‏‏‏‏‎‎‏‏‎‎Once you give app permissions to write ‎‏‎‎‏‏‎<xliff:g id="CATEGORY_NAME">%1$s</xliff:g>‎‏‎‎‏‏‏‎ data, sources will show here.‎‏‎‎‏‎"</string>
     <string name="data_sources_help_link" msgid="7740264923634947915">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‏‏‏‎‏‎‏‏‎‏‏‎‏‎‏‎‏‏‏‏‎‎‎‏‏‏‏‏‎‎‎‎‎‎‏‏‏‎‎‎‏‎‏‎‏‏‎‏‎‏‎‏‎‏‏‏‎‏‎‎‏‎‏‏‎How sources &amp; prioritization 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>
+    <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>
+    <string name="date_picker_week" msgid="1038805538316142229">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‎‎‏‏‏‎‎‏‏‎‏‎‏‎‏‎‎‏‎‏‎‎‎‎‏‎‎‎‏‎‎‎‎‏‏‏‏‎‎‎‎‎‏‎‏‎‏‏‎‎‎‏‏‎‏‎‎‏‎‏‎‏‎Week‎‏‎‎‏‎"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‎‏‏‎‎‎‏‎‏‏‎‏‎‏‎‎‎‎‏‏‏‏‎‏‏‎‏‏‏‎‏‎‏‎‎‏‏‏‏‏‎‎‎‏‎‎‎‏‏‏‏‎‎‎‎‎‏‎‎‎‎‎‎‎Month‎‏‎‎‏‎"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‎‏‏‎‎‎‏‏‏‎‎‎‎‏‏‏‏‏‎‏‎‎‎‎‎‎‏‏‎‏‏‎‎‏‎‏‏‏‎‏‎‏‎‏‏‎‏‎‎‏‏‎‏‎‎‏‎‎‎‏‏‎‏‎This week‎‏‎‎‏‎"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‏‏‎‎‏‎‎‎‎‎‎‏‎‏‏‎‎‏‏‎‎‎‏‏‏‏‎‎‏‏‏‏‏‎‎‎‎‏‎‎‎‏‎‎‎‎‎‏‏‎‏‎‎‎‎‎‏‎‎‏‎‏‏‎‎Last week‎‏‎‎‏‎"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‎‏‎‎‎‏‎‎‎‎‎‏‎‎‎‏‎‏‎‏‎‎‎‏‎‎‎‎‎‎‎‏‏‏‏‎‎‏‏‎‏‎‏‎‎‎‎‏‏‎‏‏‎‎‎‏‏‎‎‎‏‎‏‎This month‎‏‎‎‏‎"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‎‏‏‎‎‏‎‏‏‎‏‏‏‎‎‏‎‏‏‏‎‎‏‎‎‏‎‏‏‏‎‏‎‎‏‎‎‏‎‏‎‏‎‏‎‎‎‎‏‏‎‎‏‏‎‏‏‎‏‎‏‎‏‎Last month‎‏‎‎‏‎"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‎‏‎‏‏‏‏‎‎‏‏‏‎‎‎‏‏‎‏‎‎‏‎‎‏‏‏‏‏‎‎‏‎‏‏‏‎‎‏‎‎‎‏‏‏‏‏‎‏‏‎‎‎‏‏‏‎‏‏‎‏‎‏‎Entries‎‏‎‎‏‎"</string>
+    <string name="tab_access" msgid="7818197975407243701">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‏‏‏‏‎‏‏‎‎‎‏‏‏‏‏‏‏‏‏‎‏‎‎‎‏‏‎‏‎‎‎‏‎‏‏‎‏‎‎‏‏‏‏‎‏‎‎‏‏‏‎‏‏‎‎‎‏‏‎‏‏‎‏‎‏‎Access‎‏‎‎‏‎"</string>
 </resources>
diff --git a/apk/res/values-es-rUS/strings.xml b/apk/res/values-es-rUS/strings.xml
index 16cc3c4..05eeff4 100644
--- a/apk/res/values-es-rUS/strings.xml
+++ b/apk/res/values-es-rUS/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"Acceder a tus datos de salud"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Medir calorías quemadas"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Permite que la app lea las calorías quemadas"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Lectura en segundo plano"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"lectura en segundo plano"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Leer los datos de salud en segundo plano"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Calorías activas quemadas"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"Calorías activas quemadas"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Medir las calorías activas quemadas"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"ruta de ejercicio"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Grabar ruta de ejercicio"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Leer la ruta de ejercicio"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Leer todas las rutas de ejercicio"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Distancia"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"distancia"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Medir distancia"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Ninguna de las apps podrá acceder a Health Connect ni agregar datos nuevos en ella. No se borrarán los datos existentes.\n\nEsto no afecta los otros permisos que puede tener esta app, como a la ubicación, la cámara o el micrófono."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Quitar todo"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Administrar app"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Ver datos de app"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Borrar datos de la app"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Apps inactivas"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Estas apps ya no tienen acceso, pero todavía tienen datos almacenados en Health Connect"</string>
@@ -677,7 +682,7 @@
     <string name="check_for_updates" msgid="3841090978657783101">"Comprobar si hay actualizaciones"</string>
     <string name="check_for_updates_description" msgid="1347667778199095160">"Asegúrate de que las apps instaladas estén actualizadas"</string>
     <string name="see_all_compatible_apps" msgid="6791146164535475726">"Ver todas las apps compatibles"</string>
-    <string name="see_all_compatible_apps_description" msgid="2092325337403254491">"Buscar apps en Google Play"</string>
+    <string name="see_all_compatible_apps_description" msgid="2092325337403254491">"Busca apps en Google Play"</string>
     <string name="send_feedback" msgid="7756927746070096780">"Enviar comentarios"</string>
     <string name="send_feedback_description" msgid="2887207112856240778">"Dinos qué apps de salud y fitness quieres usar con Health Connect"</string>
     <string name="playstore_app_title" msgid="4138464328693481809">"Play Store"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Sin fuentes de app"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Cuando otorgues permiso a las apps para escribir datos de <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>, las fuentes se mostrarán aquí."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Semana"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Mes"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Esta semana"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"La semana pasada"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Este mes"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"El mes pasado"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Entradas"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Acceso"</string>
 </resources>
diff --git a/apk/res/values-es/strings.xml b/apk/res/values-es/strings.xml
index f811aae..84217b4 100644
--- a/apk/res/values-es/strings.xml
+++ b/apk/res/values-es/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"acceder a tus datos de salud"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Leer calorías quemadas"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Permite que la aplicación lea las calorías quemadas"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Leer en segundo plano"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"leer en segundo plano"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Leer datos de salud en segundo plano"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Calorías quemadas en actividad"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"calorías quemadas en actividad"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Leer calorías quemadas en actividad"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"ruta de ejercicio"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Escribir ruta de ejercicio"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Leer ruta de ejercicio"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Leer todas las rutas de ejercicio"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Distancia"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"distancia"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Leer distancia"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Ninguna de tus aplicaciones podrá acceder ni añadir nuevos datos a Salud conectada. No se eliminarán los datos ya almacenados.\n\nEsta acción no afecta a otros permisos que pueda tener esta aplicación, como los de ubicación, cámara o micrófono."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Quitar todos"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Gestionar aplicación"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Ver datos de aplicaciones"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Eliminar datos de la aplicación"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Aplicaciones inactivas"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Estas aplicaciones ya no tienen acceso, pero siguen teniendo datos almacenados en Salud conectada"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"No hay fuentes de aplicaciones"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Cuando des permisos a las aplicaciones para escribir datos de <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>, las fuentes se mostrarán aquí."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Semana"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Mes"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Esta semana"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Semana pasada"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Este mes"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Mes pasado"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Entradas"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Acceso"</string>
 </resources>
diff --git a/apk/res/values-et/strings.xml b/apk/res/values-et/strings.xml
index 5cd1240..4854ec8 100644
--- a/apk/res/values-et/strings.xml
+++ b/apk/res/values-et/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"juurdepääs teie terviseandmetele"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Põletatud kalorite arvu lugemine"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Võimaldab rakendusel lugeda põletatud kalorite arvu"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Taustal lugemine"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"taustal lugemine"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Terviseandmete lugemine taustal"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Aktiivse tegevuse käigus põletatud kalorite arv"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"aktiivse tegevuse käigus põletatud kalorite arv"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Aktiivse tegevuse käigus põletatud kalorite arvu lugemine"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"treeningu marsruut"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Treeningu marsruudi kirjutamine"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Treeningu marsruudi lugemine"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Kõigi treeningu marsruutide lugemine"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Vahemaa"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"vahemaa"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Vahemaa lugemine"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Ükski teie rakendus ei pääse Health Connecti andmetele juurde ega saa uusi andmeid lisada. Olemasolevaid andmeid see ei kustuta.\n\nSee ei mõjuta muid selle rakenduse õiguseid, nagu asukoha, kaamera või mikrofoni kasutamise luba."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Eemalda kõik"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Rakenduse haldamine"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Kuva rakenduse andmed"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Kustuta rakenduse andmed"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Inaktiivsed rakendused"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Nendel rakendustel pole enam juurdepääsu, kuid nende andmed on endiselt Health Connecti salvestatud."</string>
@@ -674,9 +679,9 @@
     <string name="help_and_feedback" msgid="4772169905005369871">"Abi ja tagasiside"</string>
     <string name="cant_see_all_your_apps_description" msgid="7344859063463536472">"Kui te installitud rakendust ei näe, ei pruugi see veel Health Connectiga ühilduda"</string>
     <string name="things_to_try" msgid="8200374691546152703">"Asjad, mida proovida"</string>
-    <string name="check_for_updates" msgid="3841090978657783101">"Otsi värskendusi"</string>
+    <string name="check_for_updates" msgid="3841090978657783101">"Otsige värskendusi"</string>
     <string name="check_for_updates_description" msgid="1347667778199095160">"Veenduge, et installitud rakendused oleksid ajakohased"</string>
-    <string name="see_all_compatible_apps" msgid="6791146164535475726">"Kuva kõik ühilduvad rakendused"</string>
+    <string name="see_all_compatible_apps" msgid="6791146164535475726">"Vaadake kõiki ühilduvaid rakendusi"</string>
     <string name="see_all_compatible_apps_description" msgid="2092325337403254491">"Otsige rakendusi Google Playst"</string>
     <string name="send_feedback" msgid="7756927746070096780">"Tagasiside saatmine"</string>
     <string name="send_feedback_description" msgid="2887207112856240778">"Öelge meile, milliseid tervise- ja treeningurakendusi soovite Health Connectiga kasutada"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Rakendusallikaid ei ole"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Kui annate rakendusele loa kirjutada kategooria <xliff:g id="CATEGORY_NAME">%1$s</xliff:g> andmeid, kuvatakse siin allikad."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Nädal"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Kuu"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"See nädal"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Eelmine nädal"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"See kuu"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Eelmine kuu"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Kirjed"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Juurdepääs"</string>
 </resources>
diff --git a/apk/res/values-eu/strings.xml b/apk/res/values-eu/strings.xml
index 7eeaab5..51eea9b 100644
--- a/apk/res/values-eu/strings.xml
+++ b/apk/res/values-eu/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"atzitu osasunari buruzko datuak"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Irakurri erretako kaloriak"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Erretako kaloriak irakurtzeko baimena ematen dio aplikazioari"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Atzeko planoko irakurketa"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"atzeko planoko irakurketa"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Irakurri osasunari buruzko datuak atzeko planoan"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Aktibo egonda erretako kaloriak"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"aktibo egonda erretako kaloriak"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Irakurri aktibo egonda erretako kaloriei buruzko datuak"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"ariketari dagokion ibilbidea"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Idatzi ariketari dagokion ibilbidea"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Irakurri ariketaren ibilbidea"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Irakurri ariketei dagozkien ibilbideak"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Distantzia"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"distantzia"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Irakurri distantziari buruzko datuak"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Ezein aplikaziok ezingo ditu atzitu Health Connect-en gordetako datuak, ezta beste daturik gehitu ere. Dena den, ez dira ezabatuko lehendik dauden datuak.\n\nAldaketak ez die eragingo aplikazioaren beste baimen batzuei (esaterako, kokapena, kamera edo mikrofonoa erabiltzeari buruzkoei)."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Kendu guztiak"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Kudeatu aplikazioa"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Ikusi aplikazioko datuak"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Ezabatu aplikazioko datuak"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Aplikazio inaktiboak"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Aplikazio hauek ez dute jada sarbiderik, baina oraindik datuak gordeta dauzkate Health Connect-en"</string>
@@ -477,16 +482,16 @@
     <string name="arm_curl" msgid="1737456878333201848">"Besoa tolestutako pisu-ariketa"</string>
     <string name="ball_slam" msgid="5996773678701283169">"Baloi slam-a"</string>
     <string name="double_arm_triceps_extension" msgid="4010735719203872078">"Bi besoen trizeps-luzaketa"</string>
-    <string name="dumbbell_row" msgid="181791808359752158">"Ukondoa altxatutako haltera-ariketa"</string>
+    <string name="dumbbell_row" msgid="181791808359752158">"Ukondoa altxatuz egindako haltera-ariketa"</string>
     <string name="front_raise" msgid="1030939481482621384">"Aurrealdeko altxatzeen pisu-ariketa"</string>
     <string name="hip_thrust" msgid="8490916766767408053">"Aldakak altxatzea"</string>
     <string name="hula_hoop" msgid="1651914953207761226">"Uztaia"</string>
     <string name="kettlebell_swing" msgid="364783119882246413">"Errusiar pisua kulunkaraztea"</string>
     <string name="lateral_raise" msgid="1037404943175363734">"Alboko altxatzeen pisu-ariketa"</string>
-    <string name="leg_curl" msgid="5327470513599472344">"Hankak tolestutako pisu-ariketa"</string>
+    <string name="leg_curl" msgid="5327470513599472344">"Hankak tolestuz egindako pisu-ariketa"</string>
     <string name="leg_extension" msgid="1843556289395164421">"Hanken luzaketa"</string>
     <string name="leg_press" msgid="4544551493384600086">"Hanken pisu-ariketa"</string>
-    <string name="leg_raise" msgid="3206754140765952088">"Hankak jasotako pisu-ariketa"</string>
+    <string name="leg_raise" msgid="3206754140765952088">"Hankak jasoz egindako pisu-ariketa"</string>
     <string name="mountain_climber" msgid="6666288676718010900">"Eskalatzailea"</string>
     <string name="pull_up" msgid="4056233737860296184">"Gora tiratzea"</string>
     <string name="punch" msgid="7915247952566217050">"Ukabilkadak"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Ez dago aplikazio gisako iturbururik"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Datuak (<xliff:g id="CATEGORY_NAME">%1$s</xliff:g>) idazteko aplikazio-baimenak ematen dituzunean, hemen agertuko dira iturburuak."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Astekoak"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Hilabetekoak"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Aste honetakoak"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Joan den astekoak"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Hilabete honetakoak"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Joan den hilabetekoak"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Sarrerak"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Erabiltzeko baimena dutenak"</string>
 </resources>
diff --git a/apk/res/values-fa/strings.xml b/apk/res/values-fa/strings.xml
index 4a2f07c..4306fc5 100644
--- a/apk/res/values-fa/strings.xml
+++ b/apk/res/values-fa/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"دسترسی به داده‌های سلامت شما"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"خواندن داده کالری سوزانده‌شده"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"به برنامه اجازه می‌دهد کالری سوزانده‌شده را بخواند"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"خواندن در پس‌زمینه"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"خواندن در پس‌زمینه"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"خواندن داده‌های سلامتی در پس‌زمینه"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"کالری سوزانده‌شده درطول فعالیت"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"کالری سوزانده‌شده درطول فعالیت"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"خواندن کالری سوزانده‌شده درطول فعالیت"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"مسیر تمرین"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"نوشتن مسیر تمرین"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"خواندن مسیر تمرین"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"خواندن همه مسیرهای تمرین"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"مسافت"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"مسافت"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"خواندن داده مسافت"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"هیچ‌یک از برنامه‌هایتان دیگر نمی‌تواند به Health Connect دسترسی داشته باشد یا داده‌های جدید به آن اضافه کند. این موضوع باعث حذف داده‌های موجود نمی‌شود.\n\nاین تنظیم بر دیگر اجازه‌هایی که این برنامه ممکن است داشته باشد (مثلاً مکان، دوربین، یا میکروفون) اثر نمی‌گذارد."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"برداشتن همه موارد"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"مدیریت برنامه"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"دیدن داده‌های برنامه"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"حذف داده‌های برنامه"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"برنامه‌های غیرفعال"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"این برنامه‌ها دیگر دسترسی ندارند، اما همچنان داده ذخیره‌شده در Health Connect دارند"</string>
@@ -757,7 +762,7 @@
     <string name="app_update_needed_banner_title" msgid="4724335956851853802">"به‌روزرسانی برنامه لازم است"</string>
     <string name="app_update_needed_banner_description_single" msgid="2229935331303234217">"<xliff:g id="APP_NAME">%1$s</xliff:g> باید به‌روز باشد تا همچنان با Health Connect کار کند"</string>
     <string name="app_update_needed_banner_description_multiple" msgid="1523113182062764912">"برخی‌از برنامه‌ها باید به‌روز باشند تا همچنان با Health Connect کار کنند"</string>
-    <string name="app_update_needed_banner_button" msgid="8223115764065649627">"بررسی وجود به‌روزرسانی"</string>
+    <string name="app_update_needed_banner_button" msgid="8223115764065649627">"به‌روزرسانی‌ها را بررسی کنید"</string>
     <string name="migration_pending_permissions_dialog_title" msgid="6019552841791757048">"یکپارچه‌سازی Health Connect"</string>
     <string name="migration_pending_permissions_dialog_content" msgid="6350115816948005466">"‫Health Connect برای ادغام شدن با سیستم Android آماده است. اگر اکنون به <xliff:g id="APP_NAME">%1$s</xliff:g> اجازه دسترسی بدهید، ممکن است برخی‌از ویژگی‌ها تا زمان تکمیل یکپارچه‌سازی کار نکنند."</string>
     <string name="migration_pending_permissions_dialog_content_apps" msgid="6417173899016940664">"‫Health Connect برای ادغام شدن با سیستم Android آماده است. اگر اکنون به برنامه‌ها اجازه دسترسی بدهید، ممکن است برخی‌از ویژگی‌ها تا زمان تکمیل فرایند ادغام کار نکنند."</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"منبع برنامه‌ای وجود ندارد"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"پس‌از اینکه به برنامه اجازه نوشتن داده‌های <xliff:g id="CATEGORY_NAME">%1$s</xliff:g> می‌دهید، منابع در اینجا نشان داده می‌شود."</string>
     <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="app_data_title" msgid="6499967982291000837">"داده‌های برنامه"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"داده‌ها از برنامه‌هایی که به Health Connect دسترسی دارند اینجا نشان داده خواهد شد"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"روز"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"هفته"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"ماه"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"این هفته"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"هفته گذشته"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"این ماه"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"ماه گذشته"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"ورودی‌ها"</string>
+    <string name="tab_access" msgid="7818197975407243701">"دسترسی"</string>
 </resources>
diff --git a/apk/res/values-fi/strings.xml b/apk/res/values-fi/strings.xml
index 22a0294..c70ce17 100644
--- a/apk/res/values-fi/strings.xml
+++ b/apk/res/values-fi/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"pääsy terveysdataan"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Lukea poltetut kalorit"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Sallii sovelluksen lukea dataa poltetuista kaloreista"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Lukeminen taustalla"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"lukeminen taustalla"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Lue terveysdataa taustalla"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Liikunnan aikana poltetut kalorit"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"liikunnan aikana poltetut kalorit"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Lukee liikunnan aikana poltetut kalorit"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"liikuntareitti"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Kirjoittaa liikuntareitin"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Lue liikuntareitti"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Lue kaikki harjoitusreitit"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Etäisyys"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"etäisyys"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Lukee etäisyysdataa"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Mikään sovelluksesi ei pääse käsiksi Health Connectin dataan tai voi lisätä sinne uutta dataa. Aiempaa dataa ei poisteta.\n\nTämä ei vaikuta sovelluksen muihin lupiin, esimerkiksi sijainti-, kamera- tai mikrofonilupaan."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Poista kaikki"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Muuta sovelluksen asetuksia"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Näytä sovellusdata"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Poista sovellusdata"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Epäaktiiviset sovellukset"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Näillä sovelluksilla ei ole enää pääsyoikeuksia, mutta niillä on edelleen Health Connectiin tallennettua dataa"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Ei sovelluslähteitä"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Kun annat sovellukselle luvan kirjoittaa kategorian <xliff:g id="CATEGORY_NAME">%1$s</xliff:g> dataa, lähteet näkyvät täällä."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Viikko"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Kuukausi"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Tämä viikko"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Viime viikko"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Tämä kuukausi"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Viime kuukausi"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Merkinnät"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Pääsy"</string>
 </resources>
diff --git a/apk/res/values-fr-rCA/strings.xml b/apk/res/values-fr-rCA/strings.xml
index 70ca9ca..5d49d2d 100644
--- a/apk/res/values-fr-rCA/strings.xml
+++ b/apk/res/values-fr-rCA/strings.xml
@@ -20,7 +20,7 @@
     <string name="app_label" msgid="4768580772453324183">"Connexion santé"</string>
     <string name="health_connect_summary" msgid="6401520186678972547">"Gérez l\'accès des applications aux données relatives à la santé"</string>
     <string name="permissions_and_data_header" msgid="4406105506837487805">"Autorisations et données"</string>
-    <string name="home_subtitle" msgid="1750033322147357163">"Gérez les données de santé et d\'activité physique sur votre téléphone, et contrôlez les applications qui peuvent y accéder"</string>
+    <string name="home_subtitle" msgid="1750033322147357163">"Gérez les données de santé et d\'activité physique sur votre téléphone et contrôlez les applications qui peuvent y accéder"</string>
     <string name="data_title" msgid="4456619761533380816">"Données et accès"</string>
     <string name="all_categories_title" msgid="1446410643217937926">"Toutes les catégories"</string>
     <string name="see_all_categories" msgid="5599882403901010434">"Afficher toutes les catégories"</string>
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"accéder à vos données de santé"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Lire les calories brûlées"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Autorise l\'application à lire les calories brûlées"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Lecture en arrière-plan"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"lecture en arrière-plan"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Lire données relatives à la santé en arrière-plan"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Calories actives brûlées"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"les calories actives brûlées"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Lecture des données relatives aux calories actives brûlées"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"l\'itinéraire d\'exercice"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Écriture des données relatives à l\'itinéraire d\'exercice"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Lecture des données relatives à l\'itinéraire d\'exercice"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Lire tous les itinéraires d\'exercice"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Distance"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"distance"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Lecture des données relatives à la distance"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Aucune de vos applications ne pourra accéder aux données stockées dans Connexion santé ni en ajouter de nouvelles. Les données existantes ne seront pas supprimées.\n\nCela n\'a aucune incidence sur d\'autres autorisations dont peut disposer cette application, comme la position, l\'appareil photo ou le microphone."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Tout retirer"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Gérer l\'application"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Afficher les données de l\'application"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Supprimer les données de l\'application"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Applications inactives"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Ces applications n\'ont plus accès à Connexion santé, mais leurs données y sont toujours stockées"</string>
@@ -677,7 +682,7 @@
     <string name="check_for_updates" msgid="3841090978657783101">"Rechercher des mises à jour"</string>
     <string name="check_for_updates_description" msgid="1347667778199095160">"Assurez-vous que les applications installées sont à jour"</string>
     <string name="see_all_compatible_apps" msgid="6791146164535475726">"Voir toutes les applications compatibles"</string>
-    <string name="see_all_compatible_apps_description" msgid="2092325337403254491">"Trouver des applications sur Google Play"</string>
+    <string name="see_all_compatible_apps_description" msgid="2092325337403254491">"Trouvez des applications sur Google Play"</string>
     <string name="send_feedback" msgid="7756927746070096780">"Envoyer des commentaires"</string>
     <string name="send_feedback_description" msgid="2887207112856240778">"Dites-nous quelles applications de santé et de mise en forme vous aimeriez utiliser avec Connexion santé"</string>
     <string name="playstore_app_title" msgid="4138464328693481809">"Play Store"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Aucune source d\'applications"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Une fois que vous aurez autorisé l\'application à écrire les données de catégorie <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>, les sources s\'afficheront ici."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Semaine"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Mois"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Cette semaine"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"La semaine dernière"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Ce mois-ci"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Le mois dernier"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Entrées"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Accès"</string>
 </resources>
diff --git a/apk/res/values-fr/strings.xml b/apk/res/values-fr/strings.xml
index 1ae186d..e713f98 100644
--- a/apk/res/values-fr/strings.xml
+++ b/apk/res/values-fr/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"accéder à vos données de forme physique"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Lire les calories brûlées"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Autorise l\'appli à répondre à lire les calories brûlées"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Lecture en arrière-plan"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"lecture en arrière-plan"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Lire les données de forme physique en arrière-plan"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Calories brûlées en activité"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"calories brûlées en activité"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Lire les calories brûlées en activité"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"parcours sportif"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Écrire le parcours sportif"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Lire le parcours sportif"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Lire tous les parcours sportifs"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Distance"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"distance"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Lire la distance"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Aucune de vos applis ne pourra accéder à vos données ni en ajouter dans Santé Connect. Les données existantes ne seront pas supprimées.\n\nCela n\'affecte pas les autres autorisations de cette appli, comme l\'accès à la position, à l\'appareil photo ou au micro."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Tout supprimer"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Gérer l\'appli"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Consulter les données d\'application"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Supprimer les données de l\'appli"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Applis inactives"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Ces applis n\'ont plus accès à Santé Connect, mais ont encore des données qui y sont stockées"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Aucune source d\'application"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Une fois que vous aurez accordé des autorisations d\'application pour écrire des données <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>, des sources s\'afficheront ici."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Semaine"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Mois"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Cette semaine"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Semaine dernière"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Ce mois-ci"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Mois dernier"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Entrées"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Accès"</string>
 </resources>
diff --git a/apk/res/values-gl/strings.xml b/apk/res/values-gl/strings.xml
index a31d8a5..61196ad 100644
--- a/apk/res/values-gl/strings.xml
+++ b/apk/res/values-gl/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"acceder aos datos sobre a túa saúde"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Ler calorías queimadas"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Permite que a aplicación lea os datos de calorías queimadas"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Lectura en segundo plano"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"lectura en segundo plano"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Ler os datos de saúde en segundo plano"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Calorías queimadas durante a actividade"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"calorías queimadas durante a actividade"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Ler datos de calorías queimadas durante a actividade"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"ruta do exercicio"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Escribir datos da ruta do exercicio"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Ler ruta do exercicio"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Ler todas as rutas de exercicio"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Distancia"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"distancia"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Ler datos de distancia"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Ningunha das aplicacións poderá acceder aos datos de Saúde conectada nin engadir nova información. Non se eliminarán os datos que teñas.\n\nEsta acción non lles afecta a outros permisos que poida ter esta aplicación, como os de acceso á localización, á cámara ou ao micrófono."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Quitar todos"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Xestionar aplicación"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Ver datos da aplicación"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Eliminar datos da aplicación"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Aplicacións inactivas"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Estas aplicacións xa non teñen acceso a Saúde conectada, pero aínda teñen datos almacenados alí"</string>
@@ -780,9 +785,22 @@
     <string name="data_totals_header" msgid="8316977153276216025">"Totais dos datos"</string>
     <string name="app_sources_header" msgid="6343062519512947665">"Fontes de aplicacións"</string>
     <string name="data_sources_footer" msgid="6414387142919741183">"Engade fontes de aplicacións á lista para ver como poden cambiar os totais dos datos. Se quitas unha aplicación da lista, deixará de contribuír aos totais, pero seguirá tendo permisos de escritura."</string>
-    <!-- no translation found for data_sources_empty_state (1899652759274805556) -->
-    <skip />
-    <!-- no translation found for data_sources_empty_state_footer (8933950342291569638) -->
-    <skip />
+    <string name="data_sources_empty_state" msgid="1899652759274805556">"Non hai ningunha aplicación como fonte de datos"</string>
+    <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"As fontes mostraranse aquí en canto lles concedas permisos ás aplicacións para escribir datos sobre <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Semana"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Mes"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Esta semana"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Última semana"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Este mes"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Último mes"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Entradas"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Acceso"</string>
 </resources>
diff --git a/apk/res/values-gu/strings.xml b/apk/res/values-gu/strings.xml
index 64c6d19..e4de9df 100644
--- a/apk/res/values-gu/strings.xml
+++ b/apk/res/values-gu/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"તમારો આરોગ્ય સંબંધિત ડેટા ઍક્સેસ કરો"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"વપરાયેલી કૅલરીનો ડેટા વાંચો"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"ઍપને વપરાયેલી કૅલરોની ડેટા વાંચવાની મંજૂરી આપે છે"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"બૅકગ્રાઉન્ડમાં વાંચવું"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"બૅકગ્રાઉન્ડમાં વાંચવું"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"બૅકગ્રાઉન્ડમાં આરોગ્યનો ડેટા વાંચો"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"વપરાયેલી સક્રિય કૅલરી"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"વપરાયેલી સક્રિય કૅલરી"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"વપરાયેલી સક્રિય કૅલરીનો ડેટા વાંચો"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"વ્યાયામનો રસ્તો"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"વ્યાયામનો રસ્તો બનાવો"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"વ્યાયામ કરવાની રીત વિશેની માહિતી વાંચવી"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"વ્યાયામના તમામ રસ્તા વાંચો"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"અંતર"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"અંતર"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"અંતરનો ડેટા વાંચો"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"તમારી કોઈપણ ઍપ Health Connectમાં નવો ડેટા ઍક્સેસ કરી કે ઉમેરી શકશે નહીં. આનાથી હાલનો કોઈપણ ડેટા ડિલીટ થતો નથી.\n\nઆનાથી ઍપ ધરાવતી હોઈ શકે એવી, જેમ કે લોકેશન, કૅમેરા કે માઇક્રોફોનની પરવાનગીઓ પર કોઈ અસર પડશે નહીં."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"બધી કાઢી નાખો"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"ઍપ મેનેજ કરો"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"ઍપનો ડેટા જુઓ"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"ઍપનો ડેટા ડિલીટ કરો"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"નિષ્ક્રિય ઍપ"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"આ બધી ઍપ હવે કોઈ ઍક્સેસ ધરાવતી નથી, પરંતુ હજી પણ Health Connectમાં તેનો ડેટા સ્ટોર કરેલો છે"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"કોઈ ઍપ સૉર્સ નથી"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"એકવાર તમે ઍપને <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>નો ડેટા લખવાની પરવાનગીઓ આપો, ત્યાર બાદ સૉર્સ અહીં દેખાશે."</string>
     <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="app_data_title" msgid="6499967982291000837">"ઍપનો ડેટા"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"Health Connectનો ઍક્સેસ ધરાવતી ઍપનો ડેટા અહીં દેખાશે"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"દિવસ"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"અઠવાડિયું"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"મહિનો"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"આ અઠવાડિયે"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"ગયા અઠવાડિયે"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"આ મહિને"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"ગયા મહિને"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"એન્ટ્રી"</string>
+    <string name="tab_access" msgid="7818197975407243701">"ઍક્સેસ"</string>
 </resources>
diff --git a/apk/res/values-hi/strings.xml b/apk/res/values-hi/strings.xml
index 58b8f06..2e3df20 100644
--- a/apk/res/values-hi/strings.xml
+++ b/apk/res/values-hi/strings.xml
@@ -20,7 +20,7 @@
     <string name="app_label" msgid="4768580772453324183">"Health Connect"</string>
     <string name="health_connect_summary" msgid="6401520186678972547">"स्वास्थ्य की जानकारी से जुड़े डेटा के लिए, ऐप्लिकेशन का ऐक्सेस मैनेज करें"</string>
     <string name="permissions_and_data_header" msgid="4406105506837487805">"अनुमतियां और डेटा"</string>
-    <string name="home_subtitle" msgid="1750033322147357163">"अपने फ़ोन पर, सेहत और फ़िटनेस से जुड़ा डेटा मैनेज करें. साथ ही, यह भी कंट्रोल करें कि कौनसे ऐप्लिकेशन इसे ऐक्सेस कर सकते हैं"</string>
+    <string name="home_subtitle" msgid="1750033322147357163">"अपने फ़ोन पर, सेहत और फ़िटनेस से जुड़ा डेटा मैनेज करें. साथ ही, यह भी कंट्रोल करें कि कौनसे ऐप्लिकेशन इस डेटा को ऐक्सेस कर सकते हैं"</string>
     <string name="data_title" msgid="4456619761533380816">"डेटा और उसका ऐक्सेस"</string>
     <string name="all_categories_title" msgid="1446410643217937926">"सभी कैटगरी"</string>
     <string name="see_all_categories" msgid="5599882403901010434">"सभी कैटगरी देखें"</string>
@@ -36,7 +36,7 @@
     <string name="data_sources_and_priority_title" msgid="3871795785418187899">"डेटा सोर्स और प्राथमिकता की सेटिंग"</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>
+    <string name="no_recent_access" msgid="4724297929902441784">"हाल ही में, किसी भी ऐप्लिकेशन ने Health Connect के डेटा का इस्तेमाल नहीं किया है"</string>
     <string name="show_recent_access_entries_button_title" msgid="3483460066767350419">"हाल में डेटा ऐक्सेस करने वाले सभी ऐप्लिकेशन देखें"</string>
     <string name="recent_access_screen_description" msgid="331101209889185402">"देखें कि पिछले 24 घंटों में किन ऐप्लिकेशन ने आपका डेटा ऐक्सेस किया है"</string>
     <string name="today_header" msgid="1006837293203834373">"आज"</string>
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"आपके स्वास्थ्य की जानकारी से जुड़ा डेटा ऐक्सेस करने की अनुमति दें"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"खर्च हुई कैलोरी का डेटा पढ़ने की अनुमति दें"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"इससे ऐप्लिकेशन को खर्च की गई कैलोरी का डेटा पढ़ने की अनुमति मिलती है"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"बैकग्राउंड में पढ़ने की सुविधा"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"बैकग्राउंड में पढ़ने की सुविधा"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"बैकग्राउंड में स्वास्थ्य की जानकारी का डेटा पढें"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"शारीरिक गतिविधि के दौरान खर्च हुई कैलोरी का डेटा"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"शारीरिक गतिविधि के दौरान खर्च हुई कैलोरी का डेटा"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"शारीरिक गतिविधि के दौरान खर्च हुई कैलोरी का डेटा पढ़ने की अनुमति दें"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"कसरत का रूट"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"कसरत का रूट बनाएं"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"कसरत का रूट पढ़ें"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"कसरत के सभी रूट पढ़ें"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"दूरी का डेटा"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"दूरी का डेटा"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"तय की गई दूरी का डेटा पढ़ने की अनुमति दें"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"आपके किसी भी ऐप्लिकेशन को Health Connect में नया डेटा जोड़ने या उसमें मौजूद डेटा को ऐक्सेस करने की अनुमति नहीं होगी. हालांकि, ऐसा करने से कोई मौजूदा डेटा नहीं मिटेगा.\n\nइससे ऐप्लिकेशन की दूसरी अनुमतियों पर असर नहीं पड़ेगा. जैसे, जगह की जानकारी, कैमरे या माइक्रोफ़ोन के इस्तेमाल की अनुमतियां."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"सभी अनुमतियां हटाएं"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"ऐप्लिकेशन मैनेज करें"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"ऐप्लिकेशन का डेटा देखें"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"ऐप्लिकेशन का डेटा मिटाएं"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"इनऐक्टिव ऐप्लिकेशन"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"ये ऐप्लिकेशन अब Health Connect को ऐक्सेस नहीं कर सकते. हालांकि, जो डेटा इन्होंने पहले सेव किया था वह अब भी Health Connect में मौजूद है"</string>
@@ -681,7 +686,7 @@
     <string name="send_feedback" msgid="7756927746070096780">"सुझाव दें या शिकायत करें"</string>
     <string name="send_feedback_description" msgid="2887207112856240778">"यह बताएं कि आपको Health Connect के साथ, स्वास्थ्य और फ़िटनेस वाले कौनसे ऐप्लिकेशन इस्तेमाल करने हैं"</string>
     <string name="playstore_app_title" msgid="4138464328693481809">"Play Store"</string>
-    <string name="auto_delete_button" msgid="8536451792268513619">"ऑटोमैटिकली मिट जाए"</string>
+    <string name="auto_delete_button" msgid="8536451792268513619">"अपने-आप मिटने की सुविधा"</string>
     <string name="auto_delete_title" msgid="8761742828224207826">"ऑटोमैटिकली मिटाएं"</string>
     <string name="auto_delete_header" msgid="4258649705159293715">"यह कंट्रोल करें कि Health Connect में आपका डेटा कितने समय तक सेव करके रखा जाए. इसके लिए, आपको डेटा को तय समय के बाद मिटाने का शेड्यूल सेट करना होगा"</string>
     <string name="auto_delete_learn_more" msgid="7416469042791307994">"अपने-आप मिटने की सुविधा के बारे में ज़्यादा जानें"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"डेटा सोर्स के तौर पर कोई ऐप्लिकेशन मौजूद नहीं है"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"ऐप्लिकेशन को <xliff:g id="CATEGORY_NAME">%1$s</xliff:g> के डेटा में बदलाव करने की अनुमति देने पर, सोर्स यहां दिखेंगे."</string>
     <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="app_data_title" msgid="6499967982291000837">"ऐप्लिकेशन का डेटा"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"Health Connect का ऐक्सेस रखने वाले ऐप्लिकेशन का डेटा यहां दिखेगा"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"दिन"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"हफ़्ता"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"महीना"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"इस हफ़्ते"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"पिछला हफ़्ता"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"इस महीने"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"पिछला महीना"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"एंट्री"</string>
+    <string name="tab_access" msgid="7818197975407243701">"ऐक्सेस"</string>
 </resources>
diff --git a/apk/res/values-hr/strings.xml b/apk/res/values-hr/strings.xml
index 919cbe8..9743acf 100644
--- a/apk/res/values-hr/strings.xml
+++ b/apk/res/values-hr/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"pristup podacima o zdravlju"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Očitavanje potrošenih kalorija"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Aplikaciji omogućuje očitavanje podataka o potrošenim kalorijama"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Čitanje u pozadini"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"čitanje u pozadini"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Čitanje zdravstvenih podataka u pozadini"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Aktivna potrošnja kalorija"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"aktivna potrošnja kalorija"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Očitavanje aktivne potrošnje kalorija"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"ruta za vježbanje"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Zapiši rutu za vježbanje"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Očitaj rutu vježbe"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Čitaj sve rute za vježbu"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Udaljenost"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"udaljenost"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Očitavanje udaljenosti"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Nijedna vaša aplikacija neće moći pristupati Health Connectu niti mu dodavati nove podatke. Time se ne brišu postojeći podaci.\n\nTo ne utječe na druga dopuštenja koja ova aplikacija može imati, kao što su lokacija, fotoaparat ili mikrofon."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Ukloni sve"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Upravljanje aplikacijom"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Pregledajte podatke aplikacije"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Izbriši podatke aplikacije"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Neaktivne aplikacije"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Ove aplikacije više nemaju pristup, no i dalje imaju pohranjene podatke u Health Connectu"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Nema izvora aplikacije"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Nakon što date dopuštenja aplikacije da biste pisali podatke <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>, izvori će se prikazati ovdje."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Tjedan"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Mjesec"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Ovaj tjedan"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Prošli tjedan"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Ovaj mjesec"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Prošli mjesec"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Unosi"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Pristup"</string>
 </resources>
diff --git a/apk/res/values-hu/strings.xml b/apk/res/values-hu/strings.xml
index a580fe2..39ead84 100644
--- a/apk/res/values-hu/strings.xml
+++ b/apk/res/values-hu/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"hozzáférés az egészségügyi adataihoz"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Elégetett kalória olvasása"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Engedélyezi az alkalmazásnak az elégetett kalória olvasását"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Olvasás a háttérben"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"olvasás a háttérben"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Egészségi adatok olvasása a háttérben"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Aktívan elégetett kalória"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"aktívan elégetett kalória"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Aktívan elégetett kalóriára vonatkozó adatok olvasása"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"edzésútvonal"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Edzésútvonal írása"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Edzésútvonal áttekintése"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Az összes edzésútvonal olvasása"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Távolság"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"távolság"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Távolságra vonatkozó adatok olvasása"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Egyik alkalmazása sem férhet majd hozzá a Health Connect-adatokhoz, és nem is adhat majd hozzá új adatokat a Health Connecthez. A művelettel nem törlődnek a meglévő adatok.\n\nEz nem befolyásolja azokat az egyéb (például a helyadatokhoz, a kamerához vagy a mikrofonhoz tartozó) engedélyeket, amelyekkel ez az alkalmazás esetleg rendelkezik."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Összes eltávolítása"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Alkalmazás kezelése"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Alkalmazásadatok megtekintése"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Alkalmazásadatok törlése"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Inaktív alkalmazások"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Ezeknek az alkalmazásoknak már nincs hozzáférésük, de továbbra is tárolnak adatokat a Health Connect szolgáltatásban."</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Nincsenek alkalmazásforrások"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Miután alkalmazásengedélyeket adott a(z) <xliff:g id="CATEGORY_NAME">%1$s</xliff:g> kategóriájú adatok írására, itt jelennek meg a források."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Hét"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Hónap"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Ezen a héten"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Múlt héten"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Ebben a hónapban"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Előző hónapban"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Bejegyzések"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Hozzáférés"</string>
 </resources>
diff --git a/apk/res/values-hy/strings.xml b/apk/res/values-hy/strings.xml
index 3ee7448..36f5345 100644
--- a/apk/res/values-hy/strings.xml
+++ b/apk/res/values-hy/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"ձեր առողջության մասին տվյալների հասանելիություն"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Կարդալ՝ որքան կալորիա է այրվել"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Թույլ է տալիս հավելվածին կարդալ այրված կալորիաների մասին տվյալները"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Ընթերցում ֆոնային ռեժիմում"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"ընթերցում ֆոնային ռեժիմում"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Կարդալ առողջության տվյալները ֆոնային ռեժիմում"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Ակտիվ գործողությունների ժամանակ այրված կալորիաներ"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"ակտիվ գործողությունների ժամանակ այրված կալորիաներ"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Կարդալ ակտիվ գործողությունների ժամանակ այրված կալորիաների մասին տվյալները"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"մարզումների երթուղի"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Գրանցել մարզումների երթուղին"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Կարդալ մարզման եղանակների մասին տեղեկություններ"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Կարդալ մարզումների բոլոր երթուղիները"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Հեռավորություն"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"հեռավորություն"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Կարդալ անցած հեռավորության մասին տվյալները"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Ձեր ոչ մի հավելված չի կարողանա օգտագործել Health Connect-ը կամ նոր տվյալներ ավելացնել այնտեղ։ Առկա տվյալները չեն ջնջվի։\n\nՍա չի ազդի այս հավելվածի մյուս թույլտվությունների վրա, օրինակ՝ տեղորոշման, տեսախցիկի կամ խոսափողի։"</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Հեռացնել բոլորը"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Հավելվածի կառավարում"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Տեսնել հավելվածի տվյալները"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Ջնջել հավելվածի տվյալները"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Անգործուն հավելվածներ"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Այս հավելվածներն այլևս չունեն հասանելիություն, սակայն դրանց տվյալները նախկինի պես պահվում են Health Connect-ում"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Հավելվածների աղբյուրներ չկան"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Հենց որ հավելվածներին թույլատրեք տվյալներ գրանցել «<xliff:g id="CATEGORY_NAME">%1$s</xliff:g>» կատեգորիայում, աղբյուրները կհայտնվեն այստեղ։"</string>
     <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="app_data_title" msgid="6499967982291000837">"Հավելվածի տվյալներ"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"Health Connect-ին հասանելիություն ունեցող հավելվածների տվյալները կցուցադրվեն այստեղ"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"Օր"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"Շաբաթ"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Ամիս"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Այս շաբաթ"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Անցած շաբաթ"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Այս ամիս"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Անցած ամիս"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Մուտքագրված տվյալներ"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Հասանելիություն"</string>
 </resources>
diff --git a/apk/res/values-in/strings.xml b/apk/res/values-in/strings.xml
index 90401e4..542362b 100644
--- a/apk/res/values-in/strings.xml
+++ b/apk/res/values-in/strings.xml
@@ -72,13 +72,16 @@
     <string name="inactive_apps_message" msgid="4666501359079362486">"Aplikasi ini tidak dapat menulis data <xliff:g id="DATA_TYPE">%s</xliff:g> lagi, tetapi masih memiliki data yang disimpan di Health Connect"</string>
     <string name="data_access_empty_message" msgid="9084350402254264452">"Aplikasi tidak dapat lagi membaca dan menulis <xliff:g id="DATA_TYPE_0">%1$s</xliff:g>, dan tidak ada data <xliff:g id="DATA_TYPE_2">%2$s</xliff:g> yang disimpan di Health Connect"</string>
     <string name="data_access_exercise_description" msgid="6868583522699443570">"Data ini termasuk informasi seperti waktu aktif, jenis olahraga, putaran, repetisi, sesi, atau gaya renang"</string>
-    <string name="data_access_sleep_description" msgid="74293126050011153">"Data ini termasuk informasi seperti tahapan tidur dan sesi tidur"</string>
+    <string name="data_access_sleep_description" msgid="74293126050011153">"Data ini termasuk informasi seperti fase tidur dan sesi tidur"</string>
     <string name="all_entries_button" msgid="5109091107239135235">"Lihat semua entri"</string>
     <string name="delete_permission_type_data_button" msgid="2270819954943391797">"Hapus data ini"</string>
     <string name="permgrouplab_health" msgid="468961137496587966">"Health Connect"</string>
     <string name="permgroupdesc_health" msgid="252080476917407273">"mengakses data kesehatan Anda"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Membaca kalori yang dibakar"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Mengizinkan aplikasi membaca kalori yang dibakar"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Pembacaan latar belakang"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"pembacaan latar belakang"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Baca data kesehatan di latar belakang"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Kalori aktif yang dibakar"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"kalori aktif yang dibakar"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Baca kalori aktif yang dibakar"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"rute olahraga"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Tulis rute olahraga"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Baca rute olahraga"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Baca semua rute olahraga"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Jarak"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"jarak"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Baca jarak"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Tidak ada aplikasi Anda yang akan dapat mengakses atau menambahkan data baru ke Health Connect. Tindakan ini tidak akan menghapus data yang ada.\n\nTindakan ini tidak akan memengaruhi izin lainnya yang mungkin dimiliki aplikasi ini, seperti lokasi, kamera, atau mikrofon."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Hapus semua"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Kelola aplikasi"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Lihat data aplikasi"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Hapus data aplikasi"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Aplikasi yang tidak aktif"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Aplikasi ini tidak lagi memiliki akses, tetapi masih memiliki data yang tersimpan di Health Connect"</string>
@@ -504,7 +509,7 @@
     <string name="sleep_stage_out_of_bed" msgid="522297068981578046">"beranjak dari tempat tidur"</string>
     <string name="sleep_stage_rem" msgid="1694477904067543104">"tidur REM"</string>
     <string name="sleep_stage_light" msgid="1070117964678317880">"tidur ringan"</string>
-    <string name="sleep_stage_deep" msgid="3134557407657258364">"tidur nyenyak"</string>
+    <string name="sleep_stage_deep" msgid="3134557407657258364">"fase tidur dalam"</string>
     <string name="sleep_stage_unknown" msgid="8664190491902295991">"tidak diketahui"</string>
     <string name="minute_duration" msgid="9035288227090160206">"<xliff:g id="MINUTE">%1$s</xliff:g> m"</string>
     <string name="hour_minute_duration_short" msgid="6862483734123680444">"<xliff:g id="HOUR">%1$s</xliff:g> j <xliff:g id="MIN">%2$s</xliff:g> m"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Tidak ada sumber aplikasi"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Setelah Anda memberi aplikasi izin untuk menulis data <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>, sumber akan muncul di sini."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Minggu"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Bulan"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Minggu ini"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Minggu lalu"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Bulan ini"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Bulan lalu"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Entri"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Akses"</string>
 </resources>
diff --git a/apk/res/values-is/strings.xml b/apk/res/values-is/strings.xml
index e4091c5..9a2f7bf 100644
--- a/apk/res/values-is/strings.xml
+++ b/apk/res/values-is/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"opna heilsugögn"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Lesa brennslu hitaeininga"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Leyfir forritinu að lesa brennslu hitaeininga"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Bakgrunnslestur"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"bakgrunnslestur"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Lesa heilsufarsgögn í bakgrunni"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Brennsla hitaeininga við hreyfingu"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"brennsla hitaeininga við hreyfingu"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Lesa brennslu hitaeininga við hreyfingu"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"æfingaleið"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Skrifa æfingaleið"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Lesa æfingaleið"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Lesa allar æfingaleiðir"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Vegalengd"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"vegalengd"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Lesa vegalengd"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Ekkert forritanna þinna mun geta nálgast gögn eða bætt nýjum gögnum við Heilsutengingu. Þetta eyðir engum fyrirliggjandi gögnum.\n\nÞetta hefur ekki áhrif á aðrar heimildir sem þetta forrit kann að hafa, t.d. fyrir staðsetningu, myndavél eða hljóðnema."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Fjarlægja allt"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Stjórna forriti"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Sjá forritsgögn"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Eyða forritsgögnum"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Óvirk forrit"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Þessi forrit hafa ekki lengur aðgang en gögn frá þeim eru enn vistuð í Heilsutengingu"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Enginn uppruni forrits"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Þegar þú hefur veitt forriti heimild til að skrifa <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>-gögn birtist uppruninn hér."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Vika"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Mánuður"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Þessi vika"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Síðasta vika"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Þessi mánuður"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Síðasti mánuður"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Skráningar"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Aðgangur"</string>
 </resources>
diff --git a/apk/res/values-it/strings.xml b/apk/res/values-it/strings.xml
index ee7a36c..278ce7b 100644
--- a/apk/res/values-it/strings.xml
+++ b/apk/res/values-it/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"accesso ai dati relativi alla salute"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Lettura delle calorie bruciate"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Consente all\'app di leggere le calorie bruciate"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Lettura in background"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"lettura in background"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Lettura di dati sanitari in background"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Calorie bruciate durante l\'attività fisica"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"calorie bruciate durante l\'attività fisica"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Lettura delle calorie bruciate durante l\'attività fisica"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"percorso per l\'esercizio fisico"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Scrittura del percorso per l\'esercizio fisico"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Lettura del percorso per l\'esercizio fisico"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Leggi tutti i tuoi percorsi per l\'esercizio fisico"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Distanza"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"distanza"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Lettura della distanza"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Nessuna delle tue app potrà accedere o aggiungere nuovi dati a Connessione Salute. I dati esistenti non vengono eliminati.\n\nNon ci sono ripercussioni su altre autorizzazioni di cui questa app potrebbe disporre, come posizione, fotocamera o microfono."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Rimuovi tutto"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Gestisci l\'app"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Visualizza i dati dell\'app"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Elimina i dati dell\'app"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"App non attive"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Queste app non possono più accedere, ma hanno dati archiviati in Connessione Salute"</string>
@@ -783,4 +788,19 @@
     <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="edit_data_sources" msgid="79641360876849547">"Modifica origini di app"</string>
+    <!-- no translation found for default_app_summary (6183876151011837062) -->
+    <skip />
+    <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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Settimana"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Mese"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Questa settimana"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Ultima settimana"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Questo mese"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Mese scorso"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Voci"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Accesso"</string>
 </resources>
diff --git a/apk/res/values-iw/strings.xml b/apk/res/values-iw/strings.xml
index 27bc2b8..98b12a2 100644
--- a/apk/res/values-iw/strings.xml
+++ b/apk/res/values-iw/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"גישה לנתוני הבריאות שלך"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"קריאה של נתוני הקלוריות שנשרפו"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"מאפשרת לאפליקציה לקרוא את נתוני הקלוריות שנשרפו"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"קריאה ברקע"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"קריאה ברקע"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"קריאה של נתוני הבריאות ברקע"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"קלוריות שנשרפו בזמן פעילות"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"קלוריות שנשרפו בזמן פעילות"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"קריאה של נתוני הקלוריות שנשרפו בזמן פעילות"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"מסלול האימון"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"כתיבה של נתוני מסלול האימון"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"קריאת הנתונים של מסלול האימון"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"קריאת כל הנתונים של מסלולי האימון"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"מרחק"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"מרחק"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"קריאה של נתוני המרחק"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"לאף אחת מהאפליקציות שלך לא תהיה אפשרות לגשת לנתונים או להוסיף נתונים חדשים ל-Health Connect. הפעולה הזאת לא תמחק נתונים קיימים.\n\nהפעולה הזאת לא משפיעה על הרשאות אחרות שעשויות להיות לאפליקציה, כמו המיקום, המצלמה או המיקרופון."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"הסרת הכול"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"ניהול האפליקציה"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"לצפייה בנתוני האפליקציה"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"מחיקת נתוני האפליקציה"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"אפליקציות לא פעילות"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"לאפליקציות האלה כבר אין גישה, אבל עדיין יש להן נתונים מאוחסנים ב-Health Connect"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"אין מקורות של אפליקציות"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"לאחר מתן הרשאות לאפליקציה לכתוב נתונים של <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>, המקורות יופיעו כאן."</string>
     <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="app_data_title" msgid="6499967982291000837">"נתוני האפליקציה"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"כאן יוצגו נתונים מאפליקציות שיש להן גישה ל-Health Connect"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"יום"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"שבוע"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"חודש"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"השבוע"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"בשבוע שעבר"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"החודש הזה"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"בחודש שעבר"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"רשומות"</string>
+    <string name="tab_access" msgid="7818197975407243701">"גישה"</string>
 </resources>
diff --git a/apk/res/values-ja/strings.xml b/apk/res/values-ja/strings.xml
index 7d02d88..9d2aa6a 100644
--- a/apk/res/values-ja/strings.xml
+++ b/apk/res/values-ja/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"健康に関するデータにアクセスします"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"消費カロリーの読み取り"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"消費カロリーの読み取りをアプリに許可します"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"バックグラウンドでの読み取り"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"バックグラウンドでの読み取り"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"健康に関するデータをバックグラウンドで読み取ります"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"活動中の消費カロリー"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"活動中の消費カロリー"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"活動中の消費カロリーの読み取り"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"エクササイズのルート"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"エクササイズのルートの書き込み"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"エクササイズのルートの読み込み"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"エクササイズのルートをすべて読み上げる"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"距離"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"距離"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"距離の読み取り"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"すべてのアプリがヘルスコネクトへのアクセスやデータの追加を行えなくなります。既存のデータは削除されません。\n\nこの設定は、位置情報、カメラ、マイクなど、このアプリのその他の権限には影響しません。"</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"すべて削除"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"アプリの管理"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"アプリデータを表示"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"アプリデータを削除"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"休止中のアプリ"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"これらのアプリはアクセスできなくなりましたが、ヘルスコネクトからデータが削除されることはありません"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"アプリのソースがありません。"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"<xliff:g id="CATEGORY_NAME">%1$s</xliff:g> のデータを書き込む権限をアプリに付与すると、ソースがここに表示されます。"</string>
     <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="app_data_title" msgid="6499967982291000837">"アプリデータ"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"ヘルスコネクトにアクセスできるアプリのデータがここに表示されます"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"日"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"週"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"月"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"今週"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"先週"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"今月"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"先月"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"エントリ"</string>
+    <string name="tab_access" msgid="7818197975407243701">"アクセス"</string>
 </resources>
diff --git a/apk/res/values-ka/strings.xml b/apk/res/values-ka/strings.xml
index fe6e775..81e50c9 100644
--- a/apk/res/values-ka/strings.xml
+++ b/apk/res/values-ka/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"თქვენს ჯანმრთელობის მონაცემებზე წვდომა"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"დამწვარი კალორიების წაკითხვა"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"საშუალებას აძლევს აპს, წაიკითხოს დამწვარი კალორიები"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"ფონურ რეჟიმში კითხვა"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"ფონურ რეჟიმში კითხვა"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"წაიკითხეთ ჯანმრთელობის მონაცემები ფონურ რეჟიმში"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"დამწვარი აქტიური კალორიები"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"დამწვარი აქტიური კალორიები"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"გაეცანით დამწვარი აქტიური კალორიების მონაცემებს"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"სავარჯიშო მარშრუტი"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"დაწერეთ სავარჯიშო მარშრუტი"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"ვარჯიშის მარშრუტის წაკითხვა"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"წაიკითხეთ ყველა სავარჯიშო მარშრუტი"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"მანძილი"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"მანძილი"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"მანძილის შესახებ ინფორმაციის წაკითხვა"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"ვერცერთი თქვენი აპი ვერ შეძლებს Health Connect-ზე წვდომას ან ახალი მონაცემების დამატებას. ეს არ წაშლის არსებულ მონაცემებს.\n\nეს არ იმოქმედებს სხვა ნებართვებზე, რომლებიც შეიძლება ჰქონდეს ამ აპს, როგორიცაა მდებარეობა, კამერა ან მიკროფონი."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"ყველას წაშლა"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"აპის მართვა"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"აპის მონაცემების ნახვა"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"აპის მონაცემების წაშლა"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"უმოქმედო აპები"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"ამ აპებს აღარ აქვთ წვდომა, მაგრამ Health Connect-ში ჯერ კიდევ აქვთ შენახული მონაცემები"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"აპის წყარო არ არის"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"მას შემდეგ, რაც მიანიჭებთ აპს <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>-ის მონაცემების ჩაწერის ნებართვას, წყაროები აქ გამოჩნდება."</string>
     <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="app_data_title" msgid="6499967982291000837">"აპის მონაცემები"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"მონაცემები იმ აპებიდან, რომლებსაც Health Connect-ზე აქვს წვდომა, აქ გამოჩნდება"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"დღე"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"კვირა"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"თვე"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"ეს კვირა"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"გასული კვირა"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"ეს თვე"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"გასული თვე"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"ჩანაწერები"</string>
+    <string name="tab_access" msgid="7818197975407243701">"წვდომა"</string>
 </resources>
diff --git a/apk/res/values-kk/strings.xml b/apk/res/values-kk/strings.xml
index be8e3a8..fadaa57 100644
--- a/apk/res/values-kk/strings.xml
+++ b/apk/res/values-kk/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"денсаулық деректерін пайдалану рұқсаты"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Жұмсалған калория дерегін оқу"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Қолданбаға жұмсалған калория деректерін оқуға рұқсат береді."</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Фондық режимде оқу"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"фондық режимде оқу"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Денсаулық туралы деректерді фондық режимде оқу"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Қимыл кезінде жұмсалған калория"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"қимыл кезінде жұмсалған калория"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Қимыл кезінде жұмсалған калория деректерін оқу"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"жаттығу бағыты"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Жаттығу бағытын жазу"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Жаттығу бағытын оқу"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Барлық жаттығу бағытын оқыңыз."</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Қашықтық"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"қашықтық"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Қашықтық деректерін оқу"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Қолданбалардың ешқайсысы Health Connect-ті пайдалана немесе оған жаңа деректер қоса алмайды. Мұндайда ағымдағы деректер жойылмайды.\n\nБұл әрекет осы қолданбада болуы мүмкін басқа рұқсаттарға, соның ішінде локацияны, камераны не микрофонды пайдалану рұқсаттарына әсер етпейді."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Барлығын өшіру"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Қолданбаны басқару"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Қолданба деректерін көру"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Қолданба деректерін жою"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Әрекетсіз қолданбалар"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Бұл қолданбалардың енді рұқсаты жоқ, бірақ олардың деректері бұрынғыша Health Connect-те сақталады."</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Қолданба дереккөзі жоқ."</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"<xliff:g id="CATEGORY_NAME">%1$s</xliff:g> деректерін жазуға қолданба рұқсаттарын бергеннен кейін, дереккөздер осы жерде көрсетіледі."</string>
     <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="app_data_title" msgid="6499967982291000837">"Қолданба деректері"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"Health Connect-ке кіру рұқсаты бар қолданбалардан алынған дерек осы жерде көрсетіледі."</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"Күн"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"Апта"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Ай"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Осы апта"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Өткен апта"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Осы ай"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Өткен ай"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Жазбалар"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Пайдалану рұқсаты"</string>
 </resources>
diff --git a/apk/res/values-km/strings.xml b/apk/res/values-km/strings.xml
index c275645..4f325da 100644
--- a/apk/res/values-km/strings.xml
+++ b/apk/res/values-km/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"ចូលប្រើទិន្នន័យសុខភាពរបស់អ្នក"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"អានកាឡូរីដែលបានដុត"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"អនុញ្ញាតឱ្យកម្មវិធីអានកាឡូរីដែលបានដុត"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"ការអាននៅផ្ទៃខាងក្រោយ"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"ការអាននៅផ្ទៃខាងក្រោយ"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"អានទិន្នន័យសុខភាពនៅផ្ទៃខាងក្រោយ"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"កាឡូរីសកម្មដែលបានដុត"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"កាឡូរីសកម្មដែលបានដុត"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"អានកាឡូរីសកម្មដែលបានដុត"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"ផ្លូវលំហាត់ប្រាណ"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"សរសេរ​ផ្លូវលំហាត់ប្រាណ"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"អានអំពីផ្លូវនៃលំហាត់ប្រាណ"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"អានផ្លូវហាត់ប្រាណទាំងអស់"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"ចម្ងាយ"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"ចម្ងាយ"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"អានចម្ងាយ"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"កម្មវិធី​របស់អ្នក​នឹងមិនអាច​ចូលប្រើ ឬ​បញ្ចូលទិន្នន័យថ្មី​ទៅក្នុង Health Connect បានទេ។ សកម្មភាពនេះ​មិនលុបទិន្នន័យ​ដែលមានស្រាប់ណាមួយឡើយ។\n\nសកម្មភាពនេះ​មិនប៉ះពាល់ដល់​ការអនុញ្ញាតផ្សេងទៀត​ដែលកម្មវិធីនេះអាចមាន ដូចជាទីតាំង កាមេរ៉ា ឬមីក្រូហ្វូនជាដើម។"</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"ដកចេញទាំងអស់"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"គ្រប់គ្រង​កម្មវិធី"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"មើលទិន្នន័យកម្មវិធី"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"លុប​ទិន្នន័យ​កម្មវិធី"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"កម្មវិធីអសកម្ម"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"កម្មវិធីទាំងនេះ​លែង​មាន​សិទ្ធិ​ចូល​ប្រើប្រាស់​ទៀតហើយ ប៉ុន្តែ​នៅតែ​រក្សាទុក​ទិន្នន័យ​នៅក្នុង Health Connect ដដែល"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"មិនមានប្រភពកម្មវិធី"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"នៅពេល​អ្នកផ្ដល់​ការអនុញ្ញាតកម្មវិធី​ឱ្យសរសេរទិន្នន័យ <xliff:g id="CATEGORY_NAME">%1$s</xliff:g> ប្រភពនឹង​បង្ហាញនៅទីនេះ។"</string>
     <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="app_data_title" msgid="6499967982291000837">"ទិន្នន័យកម្មវិធី"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"ទិន្នន័យពីកម្មវិធីដែលមានសិទ្ធិចូលប្រើ Health Connect នឹងបង្ហាញនៅទីនេះ"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"ថ្ងៃ"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"សប្ដាហ៍"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"ខែ"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"សប្ដាហ៍​នេះ"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"សប្ដាហ៍មុន"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"ខែ​នេះ"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"ខែមុន"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"ការបញ្ចូល"</string>
+    <string name="tab_access" msgid="7818197975407243701">"សិទ្ធិចូលប្រើប្រាស់"</string>
 </resources>
diff --git a/apk/res/values-kn/strings.xml b/apk/res/values-kn/strings.xml
index 0518b2b..faf6425 100644
--- a/apk/res/values-kn/strings.xml
+++ b/apk/res/values-kn/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"ನಿಮ್ಮ ಹೆಲ್ತ್‌ ಡೇಟಾಗೆ ಆ್ಯಕ್ಸೆಸ್ ಪಡೆಯಿರಿ"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"ಕಳೆಕೊಂಡ ಕ್ಯಾಲೋರಿ ಓದುತ್ತದೆ"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"ಕಳೆದುಕೊಂಡ ಕ್ಯಾಲೋರಿಗಳ ಮೊತ್ತವನ್ನು ಆ್ಯಪ್‌ಗೆ ಓದಲು ಅನುವು ಮಾಡಿಕೊಡುತ್ತದೆ."</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"ಹಿನ್ನೆಲೆಯಲ್ಲಿ ಓದುವಿಕೆ"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"ಹಿನ್ನೆಲೆಯಲ್ಲಿ ಓದುವಿಕೆ"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"ಹಿನ್ನೆಲೆಯಲ್ಲಿ ಆರೋಗ್ಯ ಡೇಟಾವನ್ನು ಓದಿರಿ"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"ಕರಗಿಸಿದ ಸಕ್ರಿಯ ಕ್ಯಾಲೋರಿಗಳು"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"ಸಕ್ರಿಯ ಕ್ಯಾಲೋರಿಗಳನ್ನು ಕಳೆದುಕೊಳ್ಳಲಾಗಿದೆ"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"ಸಕ್ರಿಯ ಕ್ಯಾಲೋರಿಗಳನ್ನು ಕಳೆದುಕೊಂಡಿರುವುದರ ಕುರಿತು ಓದಿರಿ"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"ವ್ಯಾಯಾಮದ ಯೋಜನೆ"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"ವ್ಯಾಯಾಮದ ಯೋಜನೆಯನ್ನು ಬರೆಯಿರಿ"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"ವ್ಯಾಯಾಮದ ಯೋಜನೆಯನ್ನು ಓದಿರಿ"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"ವ್ಯಾಯಾಮ ಮಾಡುವ ಎಲ್ಲಾ ವಿಧಾನಗಳನ್ನು ಓದಿ"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"ದೂರ"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"ದೂರ"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"ದೂರವನ್ನು ಓದುತ್ತದೆ"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"ನಿಮ್ಮ ಯಾವುದೇ ಆ್ಯಪ್‌ಗಳಿಗೆ Health Connect ಅನ್ನು ಆ್ಯಕ್ಸೆಸ್ ಮಾಡಲು ಅಥವಾ ಅದರಲ್ಲಿ ಹೊಸ ಡೇಟಾವನ್ನು ಸೇರಿಸಲು ಸಾಧ್ಯವಾಗುವುದಿಲ್ಲ. ಇದು ಅಸ್ತಿತ್ವದಲ್ಲಿರುವ ಯಾವುದೇ ಡೇಟಾವನ್ನು ಅಳಿಸುವುದಿಲ್ಲ.\n\nಈ ಆ್ಯಪ್ ಹೊಂದಿರಬಹುದಾದ ಸ್ಥಳ, ಕ್ಯಾಮರಾ ಅಥವಾ ಮೈಕ್ರೊಫೋನ್‌ನಂತಹ ಇತರ ಅನುಮತಿಗಳ ಮೇಲೆ ಇದು ಪರಿಣಾಮ ಬೀರುವುದಿಲ್ಲ."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"ಎಲ್ಲವನ್ನೂ ತೆಗೆದುಹಾಕಿ"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"ಆ್ಯಪ್ ಅನ್ನು ನಿರ್ವಹಿಸಿ"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"ಆ್ಯಪ್ ಡೇಟಾ ನೋಡಿ"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"ಆ್ಯಪ್ ಡೇಟಾ ಅಳಿಸಿ"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"ನಿಷ್ಕ್ರಿಯ ಆ್ಯಪ್‌ಗಳು"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"ಈ ಆ್ಯಪ್‌ಗಳು ಇನ್ನು ಮುಂದೆ ಆ್ಯಕ್ಸೆಸ್ ಹೊಂದಿರುವುದಿಲ್ಲ ಆದರೆ ಈಗಲೂ Health Connect ನಲ್ಲಿ ಸಂಗ್ರಹಿಸಲಾದ ಡೇಟಾ ಇರುತ್ತದೆ"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"ಯಾವುದೇ ಆ್ಯಪ್ ಮೂಲಗಳಿಲ್ಲ"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"ನೀವು <xliff:g id="CATEGORY_NAME">%1$s</xliff:g> ಡೇಟಾವನ್ನು ರೈಟ್ ಮಾಡಲು ಆ್ಯಪ್‌ಗೆ ಅನುಮತಿಗಳನ್ನು ನೀಡಿದ ನಂತರ, ಮೂಲಗಳು ಇಲ್ಲಿ ಕಾಣಿಸಿಕೊಳ್ಳುತ್ತವೆ."</string>
     <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="app_data_title" msgid="6499967982291000837">"ಆ್ಯಪ್ ಡೇಟಾ"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"Health Connect ಗೆ ಆ್ಯಕ್ಸೆಸ್ ಹೊಂದಿರುವ ಆ್ಯಪ್‌ಗಳ ಡೇಟಾವನ್ನು ಇಲ್ಲಿ ತೋರಿಸಲಾಗುತ್ತದೆ"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"ದಿನ"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"ವಾರ"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"ತಿಂಗಳು"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"ಈ ವಾರ"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"ಕಳೆದ ವಾರ"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"ಈ ತಿಂಗಳು"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"ಕಳೆದ ತಿಂಗಳು"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"ನಮೂದುಗಳು"</string>
+    <string name="tab_access" msgid="7818197975407243701">"ಆ್ಯಕ್ಸೆಸ್"</string>
 </resources>
diff --git a/apk/res/values-ko/strings.xml b/apk/res/values-ko/strings.xml
index 8c5ef5e..3d53352 100644
--- a/apk/res/values-ko/strings.xml
+++ b/apk/res/values-ko/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"건강 데이터 액세스"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"칼로리 소모량 읽기"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"앱에서 칼로리 소모량을 읽도록 허용"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"백그라운드 읽기"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"백그라운드 읽기"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"백그라운드에서 건강 데이터 읽기"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"활동 칼로리 소모량"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"활동 칼로리 소모량"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"활동 칼로리 소모량 읽기"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"운동 경로"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"운동 경로 기록"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"운동 루트 보기"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"모든 운동 경로 읽기"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"거리"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"거리"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"거리 읽기"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"헬스 커넥트에 액세스하거나 새 데이터를 추가할 수 있는 앱이 없습니다. 기존 데이터는 삭제되지 않습니다.\n\n위치, 카메라, 마이크 등 이 앱에 부여된 다른 권한은 영향을 받지 않습니다."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"전체 삭제"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"앱 관리"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"앱 데이터 보기"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"앱 데이터 삭제"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"비활성 앱"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"앱의 액세스 권한은 취소되었지만 데이터는 계속해서 헬스 커넥트에 저장됩니다."</string>
@@ -326,7 +331,7 @@
     <string name="confirming_question_app_remove_all_permissions" msgid="4170343072352701421">"헬스 커넥트에서 모든 <xliff:g id="APP_WITH_PERMISSIONS">%s</xliff:g> 권한도 삭제"</string>
     <string name="confirming_question_data_type_from_app_all" msgid="8361163993548510509">"<xliff:g id="APP_DATA">%2$s</xliff:g>에서 추가한 모든 <xliff:g id="DATA_TYPE">%1$s</xliff:g> 데이터를 완전히 삭제하시겠습니까?"</string>
     <string name="confirming_question_single_entry" msgid="330919962071369305">"이 항목을 완전히 삭제하시겠습니까?"</string>
-    <string name="confirming_question_message" msgid="2934249835529079545">"연결된 앱이 더 이상 이 헬스 커넥트 데이터에 액세스할 수 없게 됩니다."</string>
+    <string name="confirming_question_message" msgid="2934249835529079545">"연결된 앱에서 더 이상 이 헬스 커넥트 데이터에 액세스할 수 없게 됩니다."</string>
     <string name="confirming_question_message_menstruation" msgid="5286956266565962430">"<xliff:g id="START_DATE">%1$s</xliff:g>~<xliff:g id="END_DATE">%2$s</xliff:g> 기간의 모든 월경 관련 항목이 삭제됩니다."</string>
     <string name="confirming_question_delete_button" msgid="1999996759507959985">"삭제"</string>
     <string name="confirming_question_go_back_button" msgid="9037523726124648221">"뒤로"</string>
@@ -679,7 +684,7 @@
     <string name="see_all_compatible_apps" msgid="6791146164535475726">"호환 앱 모두 보기"</string>
     <string name="see_all_compatible_apps_description" msgid="2092325337403254491">"Google Play에서 앱을 찾아보세요."</string>
     <string name="send_feedback" msgid="7756927746070096780">"의견 보내기"</string>
-    <string name="send_feedback_description" msgid="2887207112856240778">"헬스 커넥트와 함께 어떤 건강/피트니스 앱을 사용할지 알려주세요."</string>
+    <string name="send_feedback_description" msgid="2887207112856240778">"헬스 커넥트와 함께 어떤 건강/피트니스 앱을 사용하고 싶으신지 알려주세요."</string>
     <string name="playstore_app_title" msgid="4138464328693481809">"Play 스토어"</string>
     <string name="auto_delete_button" msgid="8536451792268513619">"자동 삭제"</string>
     <string name="auto_delete_title" msgid="8761742828224207826">"자동 삭제"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"앱 소스 없음"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"<xliff:g id="CATEGORY_NAME">%1$s</xliff:g> 데이터를 쓸 수 있는 권한을 앱에 부여하면 소스가 여기에 표시됩니다."</string>
     <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="app_data_title" msgid="6499967982291000837">"앱 데이터"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"헬스 커넥트에 액세스할 수 있는 앱의 데이터가 여기에 표시됩니다."</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"일"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"주"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"월"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"이번 주"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"지난주"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"이번 달"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"지난달"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"항목"</string>
+    <string name="tab_access" msgid="7818197975407243701">"액세스"</string>
 </resources>
diff --git a/apk/res/values-ky/strings.xml b/apk/res/values-ky/strings.xml
index 869d4e9..77e6b59 100644
--- a/apk/res/values-ky/strings.xml
+++ b/apk/res/values-ky/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"ден соолукка байланыштуу нерселерди көрүү мүмкүнчүлүгү"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Күйгүзүлгөн калорияларды окуу"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Колдонмо күйгүзүлгөн калорияларды окуй алат"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Фондо окуу"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"фондо окуу"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Ден соолук тууралуу маалыматты фондо окуу"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Кыймылдаганда күйгөн калориялар"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"кыймылдаганда күйгөн калориялар"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Кыймылдаганда күйгөн калорияларды окуу"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"машыгуу маршруту"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Машыгуу маршрутун жазуу"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Машыгуу маршрутун окуу"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Бардык машыгуу маршруттарын окуу"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Аралык"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"аралык"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Аралыкты окуу"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Бир да колдонмоңуз Health Connect\'ке кирип же жаңы маалымат кошо албайт. Бул учурдагы маалыматты өчүрбөйт.\n\nБул нерсе колдонмодогу башка уруксаттарга, мисалы, жайгашкан жер, камера же микрофонго кедергисин тийгизбейт."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Баарын өчүрүү"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Колдонмону тескөө"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Колдонмонун дайындарын көрүү"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Колдонмодогу нерселерди өчүрүү"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Иштебеген колдонмолор"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Бул колдонмолорго бул нерселер мындан ары жеткиликсиз, бирок алар Health Connect\'те сактала берет"</string>
@@ -275,7 +280,7 @@
     <string name="other_android_permissions" msgid="8051485761573324702">"Бул колдонмодогу башка Android уруксаттарын башкаруу үчүн Параметрлер &gt; Колдонмолорго өтүңүз"</string>
     <string name="manage_permissions_rationale" msgid="9183689798847740274">"<xliff:g id="APP_NAME">%1$s</xliff:g> менен бөлүшкөн нерселериңиз анын купуялык саясаты менен корголот."</string>
     <string name="other_android_permissions_content_description" msgid="2261431010048933820">"Бул колдонмодогу башка Android уруксаттарын тескөө үчүн Тууралоого өтүп, Колдонмолорду таптап коюңуз"</string>
-    <string name="manage_permissions_learn_more" msgid="2503189875093300767">"Купуялык саясатын окуп чыгуу"</string>
+    <string name="manage_permissions_learn_more" msgid="2503189875093300767">"Купуялык эрежелерин окуп чыгуу"</string>
     <string name="app_perms_content_provider_24h" msgid="5977152673988158889">"Акыркы 24 сааттын ичинде колдонулган"</string>
     <string name="app_access_title" msgid="7137018424885371763">"Колдонмонун мүмкүнчүлүгү"</string>
     <string name="connected_apps_empty_list_section_title" msgid="6821215432694207342">"Учурда орнотулган шайкеш колдонмолоруңуз жок"</string>
@@ -611,7 +616,7 @@
     <string name="respiratory_rate_value_long" msgid="3822748008700697049">"{count,plural, =1{Мүнөтүнө 1 дем алуу}other{Мүнөтүнө # дем алуу}}"</string>
     <string name="kilograms_short_label" msgid="9098342853218050689">"{count,plural, =1{1 кг}other{# кг}}"</string>
     <string name="pounds_short_label" msgid="6256277330455003180">"{count,plural, =1{1 фунт}other{# фунт}}"</string>
-    <string name="stone_short_label" msgid="8377585176530348612">"{count,plural, =1{1 ст}other{# ст}}"</string>
+    <string name="stone_short_label" msgid="8377585176530348612">"{count,plural, =1{1 с}other{# с}}"</string>
     <string name="stone_pound_short_label" msgid="7157344201618366834">"{stone_part} {pound_part}"</string>
     <string name="kilograms_long_label" msgid="7883695071156297670">"{count,plural, =1{1 килограмм}other{# килограмм}}"</string>
     <string name="pounds_long_label" msgid="2916697485006416419">"{count,plural, =1{1 фунт}other{# фунт}}"</string>
@@ -677,7 +682,7 @@
     <string name="check_for_updates" msgid="3841090978657783101">"Жаңыртууларды карап көрүү"</string>
     <string name="check_for_updates_description" msgid="1347667778199095160">"Орнотулган колдонмолордун версиясы жаңы болушу керек"</string>
     <string name="see_all_compatible_apps" msgid="6791146164535475726">"Бардык шайкеш колдонмолорду көрүү"</string>
-    <string name="see_all_compatible_apps_description" msgid="2092325337403254491">"Google Play\'де колдонмо издөө"</string>
+    <string name="see_all_compatible_apps_description" msgid="2092325337403254491">"Google Play\'де колдонмо издеп көрүңүз"</string>
     <string name="send_feedback" msgid="7756927746070096780">"Пикир билдирүү"</string>
     <string name="send_feedback_description" msgid="2887207112856240778">"Health Connect кызматында кайсы ден соолук жана дене-бойду чыңдаган колдонмолорду пайдалангыңыз келет?"</string>
     <string name="playstore_app_title" msgid="4138464328693481809">"Play Store"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Колдонмолор жок"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"<xliff:g id="CATEGORY_NAME">%1$s</xliff:g> маалыматын жазууга уруксат берген колдонмолоруңуз бул жерде көрсөтүлөт."</string>
     <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="app_data_title" msgid="6499967982291000837">"Колдонмонун дайындары"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"Колдонмолордогу Health Connect кызматына байланыштуу нерселер ушул жерде көрүнөт"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"Күн"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"Апта"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Ай"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Ушул апта"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Өткөн апта"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Ушул ай"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Өткөн ай"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Киргизилген нерселер"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Мүмкүнчүлүгү бар колдонмолор"</string>
 </resources>
diff --git a/apk/res/values-lo/strings.xml b/apk/res/values-lo/strings.xml
index 7e70eb3..9a859d3 100644
--- a/apk/res/values-lo/strings.xml
+++ b/apk/res/values-lo/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"ເຂົ້າເຖິງຂໍ້ມູນສຸຂະພາບຂອງທ່ານ"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"ອ່ານຂໍ້ມູນແຄລໍຣີທີ່ໃຊ້ໄປ"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"ອະນຸຍາດໃຫ້ແອັບອ່ານຂໍ້ມູນແຄລໍຣີທີ່ໃຊ້ໄປ"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"ການອ່ານໃນພື້ນຫຼັງ"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"ການອ່ານໃນພື້ນຫຼັງ"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"ອ່ານຂໍ້ມູນສຸຂະພາບໃນພື້ນຫຼັງ"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"ແຄລໍຣີທີ່ໃຊ້ໄປໃນການເຄື່ອນໄຫວຮ່າງກາຍ"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"ແຄລໍຣີທີ່ໃຊ້ໄປໃນການເຄື່ອນໄຫວຮ່າງກາຍ"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"ອ່ານຂໍ້ມູນແຄລໍຣີທີ່ໃຊ້ໄປໃນການເຄື່ອນໄຫວຮ່າງກາຍ"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"ເສັ້ນທາງອອກກຳລັງກາຍ"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"ຂຽນເສັ້ນທາງອອກກຳລັງກາຍ"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"ອ່ານເສັ້ນທາງອອກກຳລັງກາຍ"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"ອ່ານເສັ້ນທາງການອອກກຳລັງກາຍທັງໝົດ"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"ໄລຍະທາງ"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"ໄລຍະທາງ"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"ອ່ານຂໍ້ມູນໄລຍະທາງ"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"ຈະບໍ່ມີແອັບໃດຂອງທ່ານທີ່ຈະສາມາດເຂົ້າເຖິງ ຫຼື ເພີ່ມຂໍ້ມູນໃໝ່ໄປໃສ່ Health Connect ໄດ້. ການດຳເນີນການນີ້ຈະບໍ່ລຶບຂໍ້ມູນທີ່ມີຢູ່ກ່ອນແລ້ວອອກ.\n\nການດຳເນີນການນີ້ຈະບໍ່ມີຜົນກັບການອະນຸຍາດອື່ນທີ່ແອັບນີ້ອາດມີ ເຊັ່ນ: ສະຖານທີ່, ກ້ອງຖ່າຍຮູບ ຫຼື ໄມໂຄຣໂຟນ."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"ລຶບທັງໝົດອອກ"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"ຈັດການແອັບ"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"ເບິ່ງຂໍ້ມູນແອັບ"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"ລຶບຂໍ້ມູນແອັບ"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"ແອັບທີ່ບໍ່ເຄື່ອນໄຫວ"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"ແອັບເຫຼົ່ານີ້ບໍ່ມີສິດເຂົ້າເຖິງອີກຕໍ່ໄປແລ້ວ, ແຕ່ຍັງຄົງມີຂໍ້ມູນທີ່ຈັດເກັບໄວ້ໃນ Health Connect ຢູ່"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"ບໍ່ມີແຫຼ່ງທີ່ມາແອັບ"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"ເມື່ອທ່ານໃຫ້ສິດການອະນຸຍາດແອັບເພື່ອຂຽນຂໍ້ມູນ <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>, ແຫຼ່ງທີ່ມາຈະສະແດງຢູ່ບ່ອນນີ້."</string>
     <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="app_data_title" msgid="6499967982291000837">"ຂໍ້ມູນແອັບ"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"ຂໍ້ມູນຈາກແອັບທີ່ມີສິດເຂົ້າເຖິງ Health Connect ຈະສະແດງຢູ່ບ່ອນນີ້"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"ມື້"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"ອາທິດ"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"ເດືອນ"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"ອາທິດນີ້"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"ອາທິດທີ່ຜ່ານມາ"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"ເດືອນນີ້"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"ເດືອນທີ່ຜ່ານມາ"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"ລາຍການ"</string>
+    <string name="tab_access" msgid="7818197975407243701">"ສິດເຂົ້າເຖິງ"</string>
 </resources>
diff --git a/apk/res/values-lt/strings.xml b/apk/res/values-lt/strings.xml
index 8da578b..2c9776e 100644
--- a/apk/res/values-lt/strings.xml
+++ b/apk/res/values-lt/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"prieiga prie sveikatos duomenų"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Skaityti sudegintų kalorijų duomenis"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Leidžiama programa skaityti sudegintų kalorijų duomenis"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Skaitymas fone"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"skaitymas fone"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Skaityti sveikatos duomenis fone"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Sudegintų kalorijų aktyviu metu skaičius"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"sudegintų kalorijų aktyviu metu skaičius"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Skaityti sudegintų kalorijų aktyviu metu skaičių"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"mankštos maršrutas"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Rašyti mankštos maršrutą"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Skaityti mankštos maršrutą"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Skaityti visus mankštos maršrutus"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Atstumas"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"atstumas"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Skaityti atstumo duomenis"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Nė viena iš jūsų programų negalės pasiekti ar pridėti naujų duomenų prie „Health Connect“. Esami duomenys neištrinami.\n\nTai nepaveiks kitų leidimų, kuriuos ši programa gali turėti, pvz., vietovės, fotoaparato ar mikrofono."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Pašalinti viską"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Programos tvarkymas"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Žr. programos duomenis"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Ištrinti programos duomenis"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Neaktyvios programos"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Šios programos nebeturi prieigos, bet jų duomenys vis tiek saugomi sistemoje „Health Connect“"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Nėra programų šaltinių"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Kai suteiksite programos leidimus įrašyti kategorijos „<xliff:g id="CATEGORY_NAME">%1$s</xliff:g>“ duomenis, šaltiniai bus rodomi čia."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Savaitė"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Mėnuo"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Ši savaitė"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Praėjusi savaitė"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Šis mėnuo"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Praėjęs mėnuo"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Įrašai"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Prieiga"</string>
 </resources>
diff --git a/apk/res/values-lv/strings.xml b/apk/res/values-lv/strings.xml
index d9a2486..887808e 100644
--- a/apk/res/values-lv/strings.xml
+++ b/apk/res/values-lv/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"piekļuve jūsu veselības rādītāju datiem"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Lasīt patērēto kaloriju datus"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Atļauj lietotnei lasīt patērēto kaloriju datus"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Lasīšana fonā"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"lasīšana fonā"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Fonā lasīt veselības datus"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Aktivitātēs patērētās kalorijas"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"aktivitātēs patērētās kalorijas"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Lasīt aktivitātēs patērēto kaloriju datus"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"fizisko aktivitāšu maršruts"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Ierakstīt fizisko aktivitāšu maršrutu"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Lasīt fizisko aktivitāšu maršruta datus"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Lasīt visus treniņu maršrutus"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Attālums"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"attālums"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Lasīt attāluma datus"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Neviena no jūsu lietotnēm nevarēs piekļūt datiem vai pievienot jaunus datus platformā Health Connect. Esošie dati netiek dzēsti.\n\nTādējādi netiek ietekmētas citas atļaujas, kas lietotnei var būt piešķirtas, piemēram, atrašanās vietas, kameras vai mikrofona atļaujas."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Noņemt visas"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Lietotnes pārvaldība"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Skatīt lietotnes datus"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Dzēst lietotnes datus"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Neaktīvās lietotnes"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Šīm lietotnēm vairs nav piekļuves, taču to dati joprojām tiek glabāti platformā Health Connect."</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Nav neviena avota (lietotnes)"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Kad piešķirsiet lietotņu atļaujas rakstīt datus kategorijā “<xliff:g id="CATEGORY_NAME">%1$s</xliff:g>”, avoti tiks rādīti šeit."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Nedēļa"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Mēnesis"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Šī nedēļa"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Iepriekšējā nedēļa"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Šis mēnesis"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Iepriekšējais mēnesis"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Ieraksti"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Piekļuve"</string>
 </resources>
diff --git a/apk/res/values-mk/strings.xml b/apk/res/values-mk/strings.xml
index 249b5f2..30e6a2b 100644
--- a/apk/res/values-mk/strings.xml
+++ b/apk/res/values-mk/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"пристапување до податоците за здравјето"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Читање потрошени калории"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Дозволува апликацијата да чита потрошени калории"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Читање во заднина"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"читање во заднина"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Читање на здравствените податоци во заднина"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Активно потрошени калории"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"активно потрошени калории"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Читање активно потрошени калории"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"маршрута на вежбање"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Запиши маршрута на вежбање"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Читај ја маршрутата на вежбање"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Читање на сите маршрути за вежбање"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Растојание"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"растојание"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Читање растојание"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Ниту една од апликациите нема да има пристап или да додава нови податоци во Health Connect. Ова нема да ги избрише постојните податоци.\n\nОва не влијае на другите дозволи што може да ги има апликацијава, како локација, камера или микрофон."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Отстрани ги сите"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Управувајте со апликацијата"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Прикажи податоци од апликацијата"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Избриши ги податоците од апликација"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Неактивни апликации"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Апликацииве веќе немаат пристап, но уште имаат зачувани податоци во Health Connect"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Нема извори на апликации"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Откако ќе дадете дозволи за апликацијата да пишува податоци за <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>, изворите ќе се прикажат тука."</string>
     <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="app_data_title" msgid="6499967982291000837">"Податоци од апликација"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"Податоците од апликациите со пристап до Health Connect ќе се прикажат тука"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"Ден"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"Седмица"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Месец"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Оваа седмица"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Минатата седмица"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Овој месец"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Минатиот месец"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Записи"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Пристап"</string>
 </resources>
diff --git a/apk/res/values-ml/strings.xml b/apk/res/values-ml/strings.xml
index b6e008d..f6886d7 100644
--- a/apk/res/values-ml/strings.xml
+++ b/apk/res/values-ml/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"നിങ്ങളുടെ ആരോഗ്യ ഡാറ്റ ആക്സസ് ചെയ്യുക"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"കത്തിച്ച കലോറി റീഡ് ചെയ്യുക"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"കത്തിച്ചുകളഞ്ഞ കലോറി സംബന്ധിച്ച ഡാറ്റ റീഡ് ചെയ്യാൻ ആപ്പിനെ അനുവദിക്കുന്നു"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"പശ്ചാത്തലത്തിലെ വായന"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"പശ്ചാത്തലത്തിലെ വായന"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"പശ്ചാത്തലത്തിൽ ആരോഗ്യ ഡാറ്റ വായിക്കുക"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"സജീവമായിരിക്കുമ്പോൾ കത്തിച്ചുകളഞ്ഞ കലോറി"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"സജീവമായിരിക്കുമ്പോൾ കത്തിച്ചുകളഞ്ഞ കലോറി"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"സജീവമായിരിക്കുമ്പോൾ കത്തിച്ചുകളഞ്ഞ കലോറി സംബന്ധിച്ച ഡാറ്റ റീഡ് ചെയ്യുക"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"വ്യായാമ റൂട്ട്"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"വ്യായാമ റൂട്ട് റൈറ്റ് ചെയ്യുക"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"വ്യായാമ റൂട്ട് റീഡ് ചെയ്യുക"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"വ്യായാമ സെഷന്റെ വഴികൾ"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"ദൂരം"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"ദൂരം"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"ദൂരം റീഡ് ചെയ്യുക"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"നിങ്ങളുടെ ആപ്പുകൾക്കൊന്നും Health Connect ആക്‌സസ് ചെയ്യാനോ അതിലേക്ക് പുതിയ ഡാറ്റ ചേർക്കാനോ കഴിയില്ല. ഇത് നിലവിലുള്ള ഡാറ്റയൊന്നും ഇല്ലാതാക്കുന്നില്ല.\n\nലൊക്കേഷൻ, ക്യാമറ, മൈക്രോഫോൺ എന്നിവ പോലെ ഈ ആപ്പിന് ഉണ്ടായിരിക്കാവുന്ന മറ്റ് അനുമതികളെയൊന്നും ഇത് ബാധിക്കുന്നില്ല."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"എല്ലാം നീക്കം ചെയ്യൂ"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"ആപ്പ് മാനേജ് ചെയ്യുക"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"ആപ്പ് ഡാറ്റ കാണുക"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"ആപ്പ് ഡാറ്റ ഇല്ലാതാക്കുക"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"നിഷ്‌ക്രിയമായ ആപ്പുകൾ"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"ഈ ആപ്പുകൾക്ക് ഇനി ആക്‌സസ് ഇല്ല, എന്നാലും Health Connect-ൽ ഇപ്പോഴും ഡാറ്റ സംഭരിച്ചിട്ടുണ്ട്"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"ആപ്പ് ഉറവിടങ്ങൾ ഒന്നുമില്ല"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"<xliff:g id="CATEGORY_NAME">%1$s</xliff:g> ഡാറ്റ റൈറ്റ് ചെയ്യുന്നതിന് നിങ്ങൾ ആപ്പ് അനുമതികൾ നൽകിയാൽ, ഉറവിടങ്ങൾ ഇവിടെ കാണിക്കുന്നതാണ്."</string>
     <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="app_data_title" msgid="6499967982291000837">"ആപ്പ് ഡാറ്റ"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"Health Connect-ലേക്ക് ആക്‌സസ് ഉള്ള ആപ്പുകളിൽ നിന്നുള്ള ഡാറ്റ ഇവിടെ കാണിക്കും"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"ദിവസം"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"ആഴ്‌ച"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"മാസം"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"ഈ ആഴ്‌ച"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"കഴിഞ്ഞ ആഴ്‌ച"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"ഈ മാസം"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"കഴിഞ്ഞ മാസം"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"എൻട്രികൾ"</string>
+    <string name="tab_access" msgid="7818197975407243701">"ആക്‌സസ്"</string>
 </resources>
diff --git a/apk/res/values-mn/strings.xml b/apk/res/values-mn/strings.xml
index fe216cd..6f77bbb 100644
--- a/apk/res/values-mn/strings.xml
+++ b/apk/res/values-mn/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"таны эрүүл мэндийн өгөгдөлд хандах"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Шатаасан калорийг унших"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Аппад шатаасан калори уншихыг зөвшөөрөх"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Ард унших"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"ард унших"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Эрүүл мэндийн өгөгдлийг ард унших"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Идэвхтэй үед шатаасан калори"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"идэвхтэй үед шатаасан калори"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Идэвхтэй үед шатаасан калорийг унших"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"дасгалын маршрут"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Дасгалын маршрут бичих"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Дасгалын маршрутыг уншина уу"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Дасгалын бүх маршрутыг унших"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Зай"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"зай"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Зайг унших"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Таны аппуудын аль нь ч Health Connect-д хандах эсвэл түүнд шинэ өгөгдөл нэмэх боломжгүй болно. Энэ нь одоо байгаа аливаа өгөгдлийг устгахгүй.\n\nЭнэ нь байршил, камер эсвэл микрофон зэрэг энэ аппад байж магадгүй бусад зөвшөөрөлд нөлөөлөхгүй."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Бүгдийг хасах"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Аппыг удирдах"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Аппын өгөгдлийг харах"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Аппын өгөгдлийг устгах"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Идэвхгүй аппууд"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Эдгээр апп цаашид хандах эрхгүй ч Health Connect-д өгөгдөл нь хадгалагдсан хэвээр байна"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Аппын ямар ч эх сурвалж байхгүй"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Таныг аппад <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>-н өгөгдлийг бичих зөвшөөрөл өгсний дараа эх сурвалжийг энд харуулна."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Долоо хоног"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Сар"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Энэ долоо хоног"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Өнгөрсөн долоо хоног"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Энэ сар"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Өнгөрсөн сар"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Оролт"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Хандалт"</string>
 </resources>
diff --git a/apk/res/values-mr/strings.xml b/apk/res/values-mr/strings.xml
index 4627eb7..689f5cd 100644
--- a/apk/res/values-mr/strings.xml
+++ b/apk/res/values-mr/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"तुमच्या आरोग्याशी संबंधित डेटा अ‍ॅक्सेस करा"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"घटवलेल्या कॅलरी रीड करा"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"अ‍ॅपला घटवलेल्या कॅलरी रीड करण्याची अनुमती देते"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"बॅकग्राउंडमध्ये वाचन"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"बॅकग्राउंडमध्ये वाचन"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"बॅकग्राउंडमध्ये आरोग्याशी संबंधित डेटा वाचा"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"घटवलेल्या अ‍ॅक्टिव्ह कॅलरी"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"घटवलेल्या अ‍ॅक्टिव्ह कॅलरी"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"घटवलेल्या अ‍ॅक्टिव्ह कॅलरी रीड करा"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"व्यायामाचा मार्ग"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"व्यायामाचा मार्ग लिहा"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"व्यायामाशी संबंधित मार्ग रीड करा"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"व्यायामाचे सर्व मार्ग वाचा"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"अंतराशी संबंधित डेटा"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"अंतराशी संबंधित डेटा"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"अंतराशी संबंधित डेटा रीड करा"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"तुमच्या कोणत्याही अ‍ॅप्सना Health Connect वरील नवीन डेटा अ‍ॅक्सेस करता येणार नाही किंवा जोडता येणार नाही. यामुळे सध्याचा कोणताही डेटा हटवला जात नाही.\n\nयामुळे स्थान, कॅमेरा किंवा मायक्रोफोन यांसारख्या या अ‍ॅपला असलेल्या इतर परवानग्यांवर परिणाम होत नाही."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"सर्व काढून टाका"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"अ‍ॅप व्यवस्थापित करा"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"अ‍ॅप डेटा पहा"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"अ‍ॅप डेटा हटवा"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"इनॅक्टिव्ह अ‍ॅप्स"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"या अ‍ॅप्सना यापुढे अ‍ॅक्सेस नाही, पण Health Connect मध्ये अजूनही डेटा स्टोअर केलेला राहील"</string>
@@ -679,7 +684,7 @@
     <string name="see_all_compatible_apps" msgid="6791146164535475726">"सर्व कंपॅटिबल अ‍ॅप्स पहा"</string>
     <string name="see_all_compatible_apps_description" msgid="2092325337403254491">"Google Play वर अ‍ॅप्स शोधा"</string>
     <string name="send_feedback" msgid="7756927746070096780">"फीडबॅक पाठवा"</string>
-    <string name="send_feedback_description" msgid="2887207112856240778">"कोणत्या फिटनेस ॲप्सनी Health Connect सह काम करावे असे तुम्हाला वाटते ते आम्हाला सांगा"</string>
+    <string name="send_feedback_description" msgid="2887207112856240778">"कोणत्या आरोग्य आणि फिटनेस ॲप्सनी Health Connect सह काम करावे असे तुम्हाला वाटते ते आम्हाला सांगा"</string>
     <string name="playstore_app_title" msgid="4138464328693481809">"Play Store"</string>
     <string name="auto_delete_button" msgid="8536451792268513619">"ऑटो-डिलीट"</string>
     <string name="auto_delete_title" msgid="8761742828224207826">"ऑटो-डिलीट"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"कोणतीही ॲप स्रोत नाहीत"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"तुम्ही ॲपला <xliff:g id="CATEGORY_NAME">%1$s</xliff:g> डेटा राइट करण्याच्या परवानग्या दिल्यावर, स्रोत येथे दाखवली जातील."</string>
     <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="app_data_title" msgid="6499967982291000837">"अ‍ॅप डेटा"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"Health Connect चा अ‍ॅक्सेस असलेल्या ॲप्सचा डेटा येथे दर्शवला जाईल"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"दिवस"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"आठवडा"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"महिना"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"हा आठवडा"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"मागील आठवडा"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"हा महिना"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"मागील महिना"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"एंट्री"</string>
+    <string name="tab_access" msgid="7818197975407243701">"अ‍ॅक्सेस करा"</string>
 </resources>
diff --git a/apk/res/values-ms/strings.xml b/apk/res/values-ms/strings.xml
index 2fc92bb..1a25b38 100644
--- a/apk/res/values-ms/strings.xml
+++ b/apk/res/values-ms/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"akses data kesihatan anda"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Baca kalori yang dibakar"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Benarkan apl membaca kalori yang dibakar"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Bacaan latar"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"bacaan latar"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Baca data kesihatan pada latar"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Kalori aktif yang dibakar"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"kalori aktif yang dibakar"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Baca kalori aktif yang dibakar"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"laluan senaman"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Tulis laluan senaman"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Baca laluan senaman"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Baca semua laluan senaman"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Jarak"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"jarak"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Baca jarak"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Tiada apl anda yang boleh mengakses atau menambahkan data baharu pada Health Connect. Tindakan ini tidak memadamkan mana-mana data sedia ada.\n\nTindakan ini tidak menjejaskan kebenaran lain yang apl ini mungkin ada seperti lokasi, kamera atau mikrofon."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Alih keluar semua"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Urus apl"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Lihat data apl"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Padamkan data apl"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Apl yang tidak aktif"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Apl ini tidak lagi mempunyai akses, tetapi masih mempunyai data yang disimpan dalam Health Connect"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Tiada sumber apl"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Sebaik sahaja anda memberikan kebenaran apl untuk menulis data <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>, sumber akan dipaparkan di sini."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Minggu"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Bulan"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Minggu ini"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Minggu lalu"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Bulan ini"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Bulan lepas"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Entri"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Akses"</string>
 </resources>
diff --git a/apk/res/values-my/strings.xml b/apk/res/values-my/strings.xml
index 6667445..be02b49 100644
--- a/apk/res/values-my/strings.xml
+++ b/apk/res/values-my/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"ကျန်းမာရေးဒေတာကို သုံးနိုင်သည်"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"လောင်ကျွမ်းသောကယ်လိုရီဖတ်ခြင်း"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"အက်ပ်ကို လောင်ကျွမ်းသွားသော ကယ်လိုရီများ ဖတ်ခွင့်ပေးသည်"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"နောက်ခံတွင် ဖတ်ခြင်း"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"နောက်ခံတွင် ဖတ်ခြင်း"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"နောက်ခံတွင် ကျန်းမာရေးဒေတာကို ဖတ်နိုင်သည်"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"လောင်ကျွမ်းသည့် လှုပ်ရှားကယ်လိုရီ"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"လောင်ကျွမ်းသည့် လှုပ်ရှားကယ်လိုရီ"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"လောင်ကျွမ်းသည့် လှုပ်ရှားကယ်လိုရီကို ဖတ်ရန်"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"လေ့ကျင့်ခန်း လမ်းကြောင်း"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"လေ့ကျင့်ခန်းလမ်းကြောင်း ရေးရန်"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"လေ့ကျင့်ခန်း ပုံမှန်အစီအစဉ် ဖတ်ရန်"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"လေ့ကျင့်ခန်းလမ်းကြောင်းများအားလုံး ဖတ်နိုင်သည်"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"အကွာအဝေး"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"အကွာအဝေး"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"အကွာအဝေးကို ဖတ်ရန်"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Health Connect တွင် သင်၏ မည်သည့်အက်ပ်မျှ ဒေတာသုံးနိုင် (သို့) ထည့်နိုင်တော့မည် မဟုတ်ပါ။ ၎င်းက ရှိပြီးသားဒေတာများကို မဖျက်ပါ။\n\n၎င်းသည် တည်နေရာ၊ ကင်မရာ (သို့) မိုက်ခရိုဖုန်းကဲ့သို့ ဤအက်ပ်တွင် ရှိနိုင်သော အခြားခွင့်ပြုချက်များအပေါ် မသက်ရောက်ပါ။"</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"အားလုံး ဖယ်ရှားရန်"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"အက်ပ်စီမံခြင်း"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"အက်ပ်ဒေတာ ကြည့်ရန်"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"အက်ပ်ဒေတာ ဖျက်ရန်"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"အသုံးမပြုသော အက်ပ်များ"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"ဤအက်ပ်များက သုံးခွင့်မရှိတော့သော်လည်း Health Connect တွင် ဒေတာများ သိမ်းထားသေးသည်"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"အက်ပ်ရင်းမြစ်များ မရှိပါ"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"<xliff:g id="CATEGORY_NAME">%1$s</xliff:g> ဒေတာရေးရန် အက်ပ်ခွင့်ပြုချက်များ ပေးလိုက်ပါက ရင်းမြစ်များကို ဤနေရာတွင် ပြမည်။"</string>
     <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="app_data_title" msgid="6499967982291000837">"အက်ပ်ဒေတာ"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"Health Connect သုံးခွင့်ရှိသော အက်ပ်များမှ ဒေတာကို ဤနေရာတွင် ပြပါမည်"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"ရက်"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"သီတင်းပတ်"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"လ"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"ယခု သီတင်းပတ်"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"ပြီးခဲ့သော သီတင်းပတ်"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"ယခုလ"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"ပြီးခဲ့သောလ"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"ထည့်သွင်းမှုများ"</string>
+    <string name="tab_access" msgid="7818197975407243701">"သုံးခွင့်"</string>
 </resources>
diff --git a/apk/res/values-nb/strings.xml b/apk/res/values-nb/strings.xml
index e8edfc1..6da6cca 100644
--- a/apk/res/values-nb/strings.xml
+++ b/apk/res/values-nb/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"bruke helsedataene dine"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Les forbrente kalorier"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Gir appen tillatelse til å lese forbrente kalorier"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Lesing i bakgrunnen"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"lesing i bakgrunnen"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Les helsedata i bakgrunnen"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Aktive forbrente kalorier"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"aktive forbrente kalorier"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Les aktive forbrente kalorier"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"treningsrute"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Skriving av treningsruten"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Les treningsruten"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Les alle treningsruter"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Distanse"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"distanse"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Les distanse"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Ingen av appene dine kan bruke eller legge til nye data i Health Connect. Dette sletter ikke eksisterende data.\n\nDette påvirker ikke andre tillatelser denne appen eventuelt har, for eksempel posisjon, kamera eller mikrofon."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Fjern alle"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Administrer appen"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Se appdata"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Slett appdataene"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Inaktive apper"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Disse appene har ikke tilgang lenger, men har fremdeles data lagret i Health Connect"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Ingen appkilder"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Når du gir apptillatelser til å skrive <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>-data, vises det kilder her."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Uke"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Måned"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Denne uken"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Forrige uke"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Denne måneden"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Forrige måned"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Oppføringer"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Tilgang"</string>
 </resources>
diff --git a/apk/res/values-ne/strings.xml b/apk/res/values-ne/strings.xml
index 90bcb8d..dd0e98a 100644
--- a/apk/res/values-ne/strings.xml
+++ b/apk/res/values-ne/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"आफ्नो स्वास्थ्यसम्बन्धी डेटा हेर्ने तथा प्रयोग गर्ने अनुमति दिनुहोस्"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"खर्च गरिएको क्यालोरीसम्बन्धी डेटा रिड गर्ने अनुमति दिनुहोस्"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"एपलाई खर्च गरिएको क्यालोरीसम्बन्धी डेटा रिड गर्ने अनुमति दिन्छ"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"ब्याकग्राउन्डमा रिड गर्ने"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"ब्याकग्राउन्डमा रिड गर्ने"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"ब्याकग्राउन्डमा स्वास्थ्यसम्बन्धी डेटा रिड गर्ने"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"सक्रिय हुँदा खर्च गरिएको क्यालोरी"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"सक्रिय हुँदा खर्च गरिएको क्यालोरी"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"सक्रिय हुँदा खर्च गरिएको क्यालोरी रिड गर्ने अनुमति दिनुहोस्"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"कसरत गर्न जाने मार्ग"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"कसरत गर्न जाने मार्ग राइट गर्नुहोस्"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"कसरत गर्ने मार्गका बारेमा जानकारी रिड गरियोस्"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"तपाईंले कसरत गर्ने सबै मार्गहरू रिड गर्ने अनुमति दिनुहोस्"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"दूरी"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"दूरी"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"दूरीसम्बन्धी डेटा रिड गर्ने अनुमति दिनुहोस्"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"तपाईंका कुनै पनि एपले Health Connect मा भएको जानकारी प्रयोग गर्न वा नयाँ जानकारी सेभ गर्न सक्ने छैन। यसले हालका कुनै पनि जानकारी मेटाउँदैन।\n\nयसले यो एपलाई दिइएका हुन सक्ने अन्य अनुमतिहरू (जस्तै, लोकेसन, क्यामेरा वा माइक्रोफोन) मा कुनै असर गर्दैन।"</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"सबै हटाउनुहोस्"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"एप व्यवस्थापन गर्नुहोस्"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"एपसम्बन्धी डेटा हेर्नुहोस्"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"एपसम्बन्धी डेटा मेटाउनुहोस्"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"निष्क्रिय एपहरू"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"यी एपहरूले अबदेखि Health Connect प्रयोग गर्न पाउँदैनन् तर Health Connect मा भने अझै पनि यी एपहरूसम्बन्धी डेटा भण्डारण गरिएको छ"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"डेटाको स्रोतका रूपमा कुनै पनि एप उपलब्ध छैन"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"तपाईंले एपलाई <xliff:g id="CATEGORY_NAME">%1$s</xliff:g> सम्बन्धी डेटा राइट गर्ने अनुमति दिएपछि स्रोतहरू यहाँ देखिने छन्।"</string>
     <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="app_data_title" msgid="6499967982291000837">"एपसम्बन्धी डेटा"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"Health Connect एक्सेस गर्न सक्ने एपहरूको डेटा यहाँ देखिने छ"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"दिन"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"हप्ता"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"महिना"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"यो हप्ता"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"गत हप्ता"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"यो महिना"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"गत महिना"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"इन्ट्रीहरू"</string>
+    <string name="tab_access" msgid="7818197975407243701">"एक्सेस"</string>
 </resources>
diff --git a/apk/res/values-nl/strings.xml b/apk/res/values-nl/strings.xml
index 8fa92dc..8b539b2 100644
--- a/apk/res/values-nl/strings.xml
+++ b/apk/res/values-nl/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"je gezondheidsgegevens openen"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Verbrande calorieën lezen"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Hiermee kan een app de verbrande calorieën lezen"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Lezen op de achtergrond"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"lezen op de achtergrond"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Gezondheidsgegevens lezen op de achtergrond"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Verbrande actieve calorieën"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"verbrande actieve calorieën"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Gegevens over verbrande actieve calorieën lezen"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"trainingsroute"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Trainingsroute schrijven"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Trainingsroute lezen"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Alle trainingsroutes lezen"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Afstand"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"afstand"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Gegevens over afstand lezen"</string>
@@ -242,7 +246,7 @@
     <string name="request_permissions_allow_all" msgid="3419414351406638770">"Alles toestaan"</string>
     <string name="request_permissions_dont_allow" msgid="6375307410951549030">"Niet toestaan"</string>
     <string name="request_permissions_header_desc" msgid="5561173070722750153">"Bepaal welke gegevens deze app mag lezen of schrijven in Health Connect"</string>
-    <string name="request_permissions_header_time_frame_desc" msgid="4617392728203291453">"Als je leesrechten geeft, kan deze app nieuwe gegevens en gegevens van de afgelopen 30 dagen lezen."</string>
+    <string name="request_permissions_header_time_frame_desc" msgid="4617392728203291453">"Als je leesrechten geeft, kan deze app nieuwe gegevens en gegevens van de afgelopen 30 dagen lezen"</string>
     <string name="request_permissions_header_title" msgid="4264236128614363479">"<xliff:g id="APP_NAME">%1$s</xliff:g> toegang geven tot Health Connect?"</string>
     <string name="request_permissions_rationale" msgid="6154280355215802538">"In het <xliff:g id="PRIVACY_POLICY_LINK">%2$s</xliff:g> van de ontwikkelaar lees je hoe <xliff:g id="APP_NAME">%1$s</xliff:g> omgaat met je gegevens."</string>
     <string name="request_permissions_privacy_policy" msgid="228503452643555737">"privacybeleid"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Geen van je apps heeft toegang tot gegevens van Health Connect of kan nieuwe gegevens toevoegen. Bestaande gegevens worden niet verwijderd.\n\nDit heeft geen effect op andere rechten die deze app misschien heeft, zoals voor locatie, de camera of de microfoon."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Alles verwijderen"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"App beheren"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"App-gegevens bekijken"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"App-gegevens verwijderen"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Inactieve apps"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Deze apps hebben geen toegang meer, maar hebben nog wel opgeslagen gegevens in Health Connect"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Geen app-bronnen"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Zodra je app-rechten verleent om <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>-gegevens te schrijven, komen de bronnen hier te staan."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Week"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Maand"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Deze week"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Vorige week"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Deze maand"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Vorige maand"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Items"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Toegang"</string>
 </resources>
diff --git a/apk/res/values-or/strings.xml b/apk/res/values-or/strings.xml
index 1795b47..67818cc 100644
--- a/apk/res/values-or/strings.xml
+++ b/apk/res/values-or/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"ଆପଣଙ୍କ ସ୍ୱାସ୍ଥ୍ୟ ଡାଟାକୁ ଆକ୍ସେସ କରନ୍ତୁ"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"ଖର୍ଚ୍ଚ ହୋଇଥିବା କେଲୋରୀର ଡାଟା ପଢ଼"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"ଖର୍ଚ୍ଚ ହୋଇଥିବା କେଲୋରୀର ଡାଟା ପଢ଼ିବାକୁ ଆପକୁ ଅନୁମତି ଦିଏ"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"ପୃଷ୍ଠପଟରେ ପଢ଼ିବା"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"ପୃଷ୍ଠପଟରେ ପଢ଼ିବା"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"ପୃଷ୍ଠପଟରେ ସ୍ୱାସ୍ଥ୍ୟ ଡାଟା ପଢ଼ନ୍ତୁ"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"ଖର୍ଚ୍ଚ ହୋଇଥିବା ସକ୍ରିୟ କେଲୋରୀ"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"ଖର୍ଚ୍ଚ ହୋଇଥିବା ସକ୍ରିୟ କେଲୋରୀ"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"ଖର୍ଚ୍ଚ ହୋଇଥିବା ସକ୍ରିୟ କେଲୋରୀର ଡାଟା ପଢ଼ନ୍ତୁ"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"ବ୍ୟାୟାମର ମାର୍ଗ"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"ବ୍ୟାୟାମର ମାର୍ଗ ତିଆରି କରନ୍ତୁ"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"ବ୍ୟାୟାମର ମାର୍ଗକୁ ପଢ଼ନ୍ତୁ"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"ସମସ୍ତ ବ୍ୟାୟାମ ରୁଟ ପଢ଼ନ୍ତୁ"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"ଦୂରତା"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"ଦୂରତା"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"ଦୂରତାର ଡାଟା ପଢ଼ନ୍ତୁ"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"ଆପଣଙ୍କର କୌଣସି ଆପ୍ସ Health Connectରେ ନୂଆ ଡାଟାକୁ ଆକ୍ସେସ କିମ୍ବା ଯୋଗ କରିବା ପାଇଁ ସକ୍ଷମ ହେବ ନାହିଁ। ଏହା ପୂର୍ବରୁ ଥିବା କୌଣସି ଡାଟାକୁ ଡିଲିଟ କରେ ନାହିଁ।\n\nଏହି ଆପର ଲୋକେସନ, କେମେରା କିମ୍ବା ମାଇକ୍ରୋଫୋନ ପରି ରହିଥିବା ଅନ୍ୟ ଅନୁମତିଗୁଡ଼ିକୁ ଏହା ପ୍ରଭାବିତ କରେ ନାହିଁ।"</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"ସବୁ କାଢ଼ି ଦିଅନ୍ତୁ"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"ଆପକୁ ପରିଚାଳନା କରନ୍ତୁ"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"ଆପ ଡାଟା ଦେଖନ୍ତୁ"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"ଆପ ଡାଟାକୁ ଡିଲିଟ କରିବେ?"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"ନିଷ୍କ୍ରିୟ ଆପ୍ସ"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"ଏହି ଆପ୍ସର ଆଉ ଆକ୍ସେସ ନାହିଁ, କିନ୍ତୁ ଏବେ ବି ଷ୍ଟୋର କରାଯାଇଥିବା ଡାଟା Health Connectରେ ଅଛି"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"କୌଣସି ଆପ ସୋର୍ସ ନାହିଁ"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"<xliff:g id="CATEGORY_NAME">%1$s</xliff:g> ଡାଟା ଲେଖିବାକୁ ଆପଣ ଆପ ଅନୁମତି ଦେବା ପରେ ଏଠାରେ ସୋର୍ସଗୁଡ଼ିକ ଦେଖାଯିବ।"</string>
     <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="app_data_title" msgid="6499967982291000837">"ଆପ ଡାଟା"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"Health Connectକୁ ଆକ୍ସେସ ଥିବା ଆପ୍ସରୁ ଡାଟା ଏଠାରେ ଦେଖାଯିବ"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"ଦିନ"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"ସପ୍ତାହ"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"ମାସ"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"ଏହି ସପ୍ତାହ"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"ଗତ ସପ୍ତାହ"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"ଏହି ମାସ"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"ଗତ ମାସ"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"ଏଣ୍ଟ୍ରିଗୁଡ଼ିକ"</string>
+    <string name="tab_access" msgid="7818197975407243701">"ଆକ୍ସେସ କରନ୍ତୁ"</string>
 </resources>
diff --git a/apk/res/values-pa/strings.xml b/apk/res/values-pa/strings.xml
index 0964635..a290fd6 100644
--- a/apk/res/values-pa/strings.xml
+++ b/apk/res/values-pa/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"ਆਪਣੀ ਸਿਹਤ ਸੰਬੰਧੀ ਡਾਟੇ ਤੱਕ ਪਹੁੰਚ ਕਰੋ"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"ਖਰਚ ਹੋਈਆਂ ਕੈਲੋਰੀਆਂ ਸੰਬੰਧੀ ਡਾਟਾ ਪੜ੍ਹੋ"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"ਐਪ ਨੂੰ ਖਰਚ ਹੋਈਆਂ ਕੈਲੋਰੀਆਂ ਸੰਬੰਧੀ ਡਾਟਾ ਪੜ੍ਹਨ ਦੀ ਆਗਿਆ ਦਿਓ"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"ਬੈਕਗ੍ਰਾਊਂਡ ਰੀਡਿੰਗ"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"ਬੈਕਗ੍ਰਾਊਂਡ ਰੀਡਿੰਗ"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"ਬੈਕਗ੍ਰਾਊਂਡ ਵਿੱਚ ਸਿਹਤ ਸੰਬੰਧੀ ਡਾਟਾ ਪੜ੍ਹੋ"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"ਕਸਰਤ ਦੌਰਾਨ ਖਰਚ ਹੋਈਆਂ ਕੈਲੋਰੀਆਂ ਸੰਬੰਧੀ ਡਾਟਾ"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"ਕਸਰਤ ਦੌਰਾਨ ਖਰਚ ਹੋਈਆਂ ਕੈਲੋਰੀਆਂ ਸੰਬੰਧੀ ਡਾਟਾ"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"ਕਸਰਤ ਦੌਰਾਨ ਖਰਚ ਹੋਈਆਂ ਕੈਲੋਰੀਆਂ ਸੰਬੰਧੀ ਡਾਟਾ ਪੜ੍ਹੋ"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"ਕਸਰਤ ਕਰਨ ਦਾ ਤਰੀਕਾ"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"ਕਸਰਤ ਕਰਨ ਦਾ ਤਰੀਕਾ ਲਿਖੋ"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"ਕਸਰਤ ਸੰਬੰਧੀ ਰਸਤੇ ਨੂੰ ਪੜ੍ਹੋ"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"ਕਸਰਤ ਦੇ ਸਾਰੇ ਰੂਟ ਪੜ੍ਹੋ"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"ਦੂਰੀ"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"ਦੂਰੀ"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"ਦੂਰੀ ਸੰਬੰਧੀ ਡਾਟਾ ਪੜ੍ਹੋ"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"ਤੁਹਾਡੀ ਕੋਈ ਵੀ ਐਪ Health Connect ਵਿੱਚ ਨਵੇਂ ਡਾਟੇ ਤੱਕ ਪਹੁੰਚ ਨਹੀਂ ਕਰ ਸਕੇਗੀ ਅਤੇ ਨਾ ਹੀ ਨਵਾਂ ਡਾਟਾ ਸ਼ਾਮਲ ਕਰ ਸਕੇਗੀ। ਇਸ ਨਾਲ ਕੋਈ ਮੌਜੂਦਾ ਡਾਟਾ ਨਹੀਂ ਮਿਟਦਾ।\n\nਇਸ ਨਾਲ ਐਪ ਦੀਆਂ ਟਿਕਾਣੇ, ਕੈਮਰੇ ਜਾਂ ਮਾਈਕ੍ਰੋਫ਼ੋਨ ਵਰਗੀਆਂ ਹੋਰ ਇਜਾਜ਼ਤਾਂ ਪ੍ਰਭਾਵਿਤ ਨਹੀਂ ਹੁੰਦੀਆਂ ਹਨ।"</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"ਸਭ ਹਟਾਓ"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"ਐਪ ਦਾ ਪ੍ਰਬੰਧਨ ਕਰੋ"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"ਐਪ ਡਾਟਾ ਦੇਖੋ"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"ਐਪ ਡਾਟਾ ਮਿਟਾਓ"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"ਅਕਿਰਿਆਸ਼ੀਲ ਐਪਾਂ"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"ਇਨ੍ਹਾਂ ਐਪਾਂ ਕੋਲ ਹੁਣ ਪਹੁੰਚ ਨਹੀਂ ਹੈ, ਪਰ ਹਾਲੇ ਵੀ ਇਨ੍ਹਾਂ ਨੇ Health Connect ਵਿੱਚ ਡਾਟਾ ਸਟੋਰ ਕੀਤਾ ਹੋਇਆ ਹੈ"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"ਕੋਈ ਐਪ ਸਰੋਤ ਨਹੀਂ"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"<xliff:g id="CATEGORY_NAME">%1$s</xliff:g> ਡਾਟਾ ਲਿਖਣ ਲਈ ਐਪ ਇਜਾਜ਼ਤਾਂ ਦੇਣ ਤੋਂ ਬਾਅਦ, ਸਰੋਤ ਇੱਥੇ ਦਿਸਣਗੇ।"</string>
     <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="app_data_title" msgid="6499967982291000837">"ਐਪ ਡਾਟਾ"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"Health Connect ਤੱਕ ਪਹੁੰਚ ਵਾਲੀਆਂ ਐਪਾਂ ਦਾ ਡਾਟਾ ਇੱਥੇ ਦਿਖੇਗਾ"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"ਦਿਨ"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"ਹਫ਼ਤਾ"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"ਮਹੀਨਾ"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"ਇਸ ਹਫ਼ਤੇ"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"ਪਿਛਲੇ ਹਫ਼ਤੇ"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"ਇਸ ਮਹੀਨੇ"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"ਪਿਛਲੇ ਮਹੀਨੇ"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"ਇੰਦਰਾਜ"</string>
+    <string name="tab_access" msgid="7818197975407243701">"ਪਹੁੰਚ"</string>
 </resources>
diff --git a/apk/res/values-pl/strings.xml b/apk/res/values-pl/strings.xml
index f6cd3aa..ba5c0a0 100644
--- a/apk/res/values-pl/strings.xml
+++ b/apk/res/values-pl/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"dostęp do Twoich danych dotyczących zdrowia"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Odczytuj spalone kalorie"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Zezwala aplikacji na odczytywanie spalonych kalorii"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Odczytywanie w tle"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"odczytywanie w tle"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Odczytuj dane dotyczące zdrowia w tle"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Kalorie spalone podczas aktywności"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"kalorie spalone podczas aktywności"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Odczytuj informacje o kaloriach spalonych podczas aktywności"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"trasa ćwiczeń"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Zapisz trasę ćwiczeń"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Odczyt trasy ćwiczeń"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Odczytuj wszystkie trasy ćwiczeń"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Odległość"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"odległość"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Odczytuj dane o odległości"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Żadna aplikacja nie będzie już mogła korzystać z danych z Health Connect ani dodawać nowych danych. Istniejące dane nie zostaną usunięte.\n\nNie wpływa na to na inne uprawnienia, które może mieć ta aplikacja, np. dotyczące lokalizacji, aparatu czy mikrofonu."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Usuń wszystko"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Zarządzaj aplikacją"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Zobacz dane aplikacji"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Usuń dane aplikacji"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Nieaktywne aplikacje"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Te aplikacje nie mają już dostępu, ale ich dane wciąż są przechowywane w Health Connect"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Brak źródeł aplikacji"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Kiedy nadasz aplikacji uprawnienia do zapisywania danych <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>, źródła pojawią się tutaj."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Tydzień"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Miesiąc"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Ten tydzień"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Ostatni tydzień"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Ten miesiąc"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Ostatni miesiąc"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Wpisy"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Dostęp"</string>
 </resources>
diff --git a/apk/res/values-pt-rPT/strings.xml b/apk/res/values-pt-rPT/strings.xml
index 5a5a601..172192c 100644
--- a/apk/res/values-pt-rPT/strings.xml
+++ b/apk/res/values-pt-rPT/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"aceder aos seus dados de saúde"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Ler calorias queimadas"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Permite que a app leia as calorias queimadas"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Leitura em segundo plano"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"leitura em segundo plano"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Ler dados de saúde em segundo plano"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Calorias queimadas em atividade"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"calorias queimadas em atividade"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Ler calorias queimadas em atividade"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"trajeto de exercício"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Escrever trajeto de exercício"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Ler o trajeto do exercício"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Leia todos os trajetos de exercício"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Distância"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"distância"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Ler distância"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Nenhuma das suas apps vai poder aceder ou adicionar novos dados à Saúde Connect. Isto não elimina os dados existentes.\n\nIsto não afeta outras autorizações que esta app possa ter, como Localização, Câmara ou Microfone."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Remover tudo"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Faça a gestão da app"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Ver dados de apps"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Apagar dados da app"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Apps inativas"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Estas apps já não têm acesso, mas ainda têm dados armazenados na Saúde Connect"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Sem origens de apps"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Assim que conceder autorizações da app para escrever dados de <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>, as origens vão ser apresentadas aqui."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Semana"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Mês"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Esta semana"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Semana passada"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Este mês"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Mês passado"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Entradas"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Acesso"</string>
 </resources>
diff --git a/apk/res/values-pt/strings.xml b/apk/res/values-pt/strings.xml
index 2b676d0..d3f058e 100644
--- a/apk/res/values-pt/strings.xml
+++ b/apk/res/values-pt/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"acesse seus dados de saúde"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Ler informações de calorias queimadas"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Permite que o app leia as calorias queimadas"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Leitura em segundo plano"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"leitura em segundo plano"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Ler dados de saúde em segundo plano"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Calorias ativas queimadas"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"calorias ativas queimadas"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Ler informações de calorias ativas queimadas"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"trajeto do exercício"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Gravar trajeto do exercício"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Ler trajeto do exercício"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Ler todos os trajetos de exercícios"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Distância"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"distância"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Ler informações de distância"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Nenhum dos seus apps vai poder acessar ou adicionar novos dados ao app Conexão Saúde. Os dados já existentes não serão excluídos.\n\nIsso não afeta outras permissões que o app possa ter, como de localização, câmera ou microfone."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Remover tudo"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Gerenciar o app"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Mostrar dados do app"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Excluir dados do app"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Apps inativos"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Estes apps não têm mais acesso, mas ainda possuem dados armazenados na plataforma Conexão Saúde"</string>
@@ -646,7 +651,7 @@
     <string name="feet_inches_format" msgid="768610500549967860">"<xliff:g id="FT">%1$s</xliff:g><xliff:g id="IN">%2$s</xliff:g>⁠"</string>
     <string name="feet_inches_format_long" msgid="5187265716573430363">"<xliff:g id="FT">%1$s</xliff:g> <xliff:g id="IN">%2$s</xliff:g>⁠"</string>
     <string name="calories_long" msgid="7225535148232419419">"{count,plural, =1{1 caloria}one{# caloria}other{# calorias}}"</string>
-    <string name="calories" msgid="320906359079319632">"{count,plural, =1{1 cal}one{# cal}other{# cal}}"</string>
+    <string name="calories" msgid="320906359079319632">"{count,plural, =1{1 kcal}one{# kcal}other{# kcal}}"</string>
     <string name="kj" msgid="2742876437259085714">"{count,plural, =1{1 kJ}one{# kJ}other{# kJ}}"</string>
     <string name="kj_long" msgid="1837278261960345400">"{count,plural, =1{1 quilojoule}one{# quilojoule}other{# quilojoules}}"</string>
     <string name="percent" formatted="false" msgid="9199428244800776575">"{value,plural, =1{1%}one{#%}other{#%}}"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Nenhuma origem de apps"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Depois que você permitir que o app grave dados de <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>, as origens serão mostradas aqui."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Semana"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Mês"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Esta semana"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Semana passada"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Este mês"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Mês passado"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Entradas"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Acesso"</string>
 </resources>
diff --git a/apk/res/values-ro/strings.xml b/apk/res/values-ro/strings.xml
index 4464772..c0fa680 100644
--- a/apk/res/values-ro/strings.xml
+++ b/apk/res/values-ro/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"să-ți acceseze datele despre sănătate"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Să citească numărul de calorii arse"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Permite aplicației să citească numărul de calorii arse"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Citire în fundal"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"citire în fundal"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Citește informațiile de sănătate din fundal"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Calorii active arse"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"calorii active arse"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Să citească date despre caloriile active arse"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"traseul pentru exerciții"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Scrie traseul pentru exerciții"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Citește despre moduri de a exersa"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Citește toate traseele pentru exerciții"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Distanță"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"distanță"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Să citească distanța"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Niciuna dintre aplicații nu va putea să acceseze sau să adauge date noi în Health Connect. Nu se vor șterge datele existente.\n\nAcest lucru nu afectează celelalte permisiuni pe care le poate avea aplicația, cum ar fi cele pentru locație, camera foto sau microfon."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Elimină-le pe toate"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Gestionează aplicația"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Vezi datele aplicației"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Șterge datele aplicației"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Aplicații inactive"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Aceste aplicații nu mai au acces, dar totuși au date stocate în Health Connect"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Nicio sursă de aplicații"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"După ce acorzi aplicațiilor permisiuni de scriere datelor despre <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>, sursele vor apărea aici."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Săptămână"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Lună"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Săptămâna aceasta"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Săptămâna trecută"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Luna aceasta"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Luna trecută"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Intrări"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Acces"</string>
 </resources>
diff --git a/apk/res/values-ru/strings.xml b/apk/res/values-ru/strings.xml
index d3225b1..78214d6 100644
--- a/apk/res/values-ru/strings.xml
+++ b/apk/res/values-ru/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"доступ к вашим данным о здоровье"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Чтение данных о калориях"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Приложение сможет считывать данные о количестве сожженных калорий."</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Чтение в фоновом режиме"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"чтение в фоновом режиме"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Читать данные о здоровье в фоновом режиме."</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Сожжено калорий во время активности"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"сожжено калорий во время активности"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Чтение данных о количестве сожженных калорий во время активности"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"маршрут тренировки"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Запись маршрута тренировки"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Чтение маршрута тренировки"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Читать все маршруты тренировок"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Расстояние"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"расстояние"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Чтение данных о расстоянии"</string>
@@ -242,7 +246,7 @@
     <string name="request_permissions_allow_all" msgid="3419414351406638770">"Разрешить все"</string>
     <string name="request_permissions_dont_allow" msgid="6375307410951549030">"Запретить"</string>
     <string name="request_permissions_header_desc" msgid="5561173070722750153">"Выберите, какие данные это приложение сможет читать и записывать в сервисе \"Здоровье и спорт\"."</string>
-    <string name="request_permissions_header_time_frame_desc" msgid="4617392728203291453">"Если вы предоставите приложению доступ для чтения, оно сможет считывать как новые данные, так и информацию за последние 30 дней."</string>
+    <string name="request_permissions_header_time_frame_desc" msgid="4617392728203291453">"Если вы предоставите приложению доступ для чтения, оно сможет получать как новые данные, так и информацию за последние 30 дней."</string>
     <string name="request_permissions_header_title" msgid="4264236128614363479">"Открыть приложению \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" доступ к сервису \"Здоровье и спорт\"?"</string>
     <string name="request_permissions_rationale" msgid="6154280355215802538">"Подробнее о том, как приложение \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" обрабатывает ваши данные, можно прочитать в <xliff:g id="PRIVACY_POLICY_LINK">%2$s</xliff:g> разработчика."</string>
     <string name="request_permissions_privacy_policy" msgid="228503452643555737">"политике конфиденциальности"</string>
@@ -268,12 +272,13 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Ни одно из ваших приложений больше не сможет получать доступ к информации в сервисе \"Здоровье и спорт\" или добавлять ее. Записанные ранее данные не будут удалены.\n\nЭто действие не повлияет на другие разрешения, такие как доступ к сведениям о местоположении, камере или микрофону."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Запретить"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Управление приложением"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Смотреть данные приложения"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Удалить данные приложения"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Неактивные приложения"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"У этих приложений больше нет доступа, но их данные по-прежнему хранятся в сервисе \"Здоровье и спорт\"."</string>
     <string name="manage_permissions_time_frame" msgid="1299483940842401923">"Приложение \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" получит доступ к данным, добавленным после <xliff:g id="DATA_ACCESS_DATE">%2$s</xliff:g>"</string>
     <string name="other_android_permissions" msgid="8051485761573324702">"Чтобы задать разрешения для этого приложения в Android, выберите \"Настройки &gt; Приложения\""</string>
-    <string name="manage_permissions_rationale" msgid="9183689798847740274">"Данные, которые вы предоставляете приложению \"<xliff:g id="APP_NAME">%1$s</xliff:g>\", защищены его политикой конфиденциальности."</string>
+    <string name="manage_permissions_rationale" msgid="9183689798847740274">"На данные, которые вы предоставляете приложению \"<xliff:g id="APP_NAME">%1$s</xliff:g>\", распространяется его политика конфиденциальности."</string>
     <string name="other_android_permissions_content_description" msgid="2261431010048933820">"Чтобы задать разрешения для этого приложения в Android, выберите \"Настройки &gt; Приложения\""</string>
     <string name="manage_permissions_learn_more" msgid="2503189875093300767">"Ознакомиться с политикой конфиденциальности"</string>
     <string name="app_perms_content_provider_24h" msgid="5977152673988158889">"Разрешение использовалось в последние 24 часа"</string>
@@ -297,16 +302,16 @@
     <string name="onboarding_go_back_button_text" msgid="5020083846511184625">"Назад"</string>
     <string name="onboarding_get_started_button_text" msgid="2348061971090731336">"Начать"</string>
     <string name="delete_button_content_description" msgid="9125115327455379618">"Удалить данные"</string>
-    <string name="time_range_title" msgid="6831605283322600165">"Выберите данные для удаления"</string>
+    <string name="time_range_title" msgid="6831605283322600165">"Какие данные удалить?"</string>
     <string name="time_range_next_button" msgid="5849096934896557888">"Далее"</string>
     <string name="time_range_message_all" msgid="7280888587242744729">"Все данные, добавленные в приложение \"Здоровье и спорт\" за выбранный период времени, будут навсегда удалены."</string>
     <string name="time_range_message_data_type" msgid="1896125004829258195">"Данные \"<xliff:g id="DATA_TYPE">%s</xliff:g>\", добавленные в приложение \"Здоровье и спорт\" за выбранный период времени, будут навсегда удалены."</string>
     <string name="time_range_message_category" msgid="1136451418397326356">"Данные категории \"<xliff:g id="CATEGORY">%s</xliff:g>\", добавленные в приложение \"Здоровье и спорт\" за выбранный период времени, будут навсегда удалены."</string>
     <string name="time_range_message_app_data" msgid="2590800457710603556">"Данные приложения \"<xliff:g id="APP_DATA">%s</xliff:g>\", добавленные в сервис \"Здоровье и спорт\" за выбранный период времени, будут навсегда удалены."</string>
-    <string name="time_range_one_day" msgid="7162709826595446727">"Удалить данные за последние 24 часа"</string>
-    <string name="time_range_one_week" msgid="8754523384275645434">"Удалить данные за последние 7 дней"</string>
-    <string name="time_range_one_month" msgid="3034747870231999766">"Удалить данные за последние 30 дней"</string>
-    <string name="time_range_all" msgid="8167350212705839943">"Удалить все данные"</string>
+    <string name="time_range_one_day" msgid="7162709826595446727">"За последние 24 часа"</string>
+    <string name="time_range_one_week" msgid="8754523384275645434">"За последние 7 дней"</string>
+    <string name="time_range_one_month" msgid="3034747870231999766">"За последние 30 дней"</string>
+    <string name="time_range_all" msgid="8167350212705839943">"Все"</string>
     <string name="confirming_question_all" msgid="1585414659784742952">"Навсегда удалить все данные?"</string>
     <string name="confirming_question_one_day" msgid="8001434729335611950">"Навсегда удалить все данные за последние 24 часа?"</string>
     <string name="confirming_question_one_week" msgid="5441506951423969587">"Навсегда удалить все данные за последние 7 дней?"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Нет приложений"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Здесь появятся приложения, которым вы разрешите записывать данные в категории \"<xliff:g id="CATEGORY_NAME">%1$s</xliff:g>\"."</string>
     <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="app_data_title" msgid="6499967982291000837">"Данные приложения"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"Здесь будут появляться данные из приложений, у которых есть доступ к приложению \"Здоровье и спорт\"."</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"День"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"Неделя"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Месяц"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Эта неделя"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Прошлая неделя"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Этот месяц"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Прошлый месяц"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Записи"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Доступ"</string>
 </resources>
diff --git a/apk/res/values-si/strings.xml b/apk/res/values-si/strings.xml
index fe0d4f5..bf0429b 100644
--- a/apk/res/values-si/strings.xml
+++ b/apk/res/values-si/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"ඔබේ සෞඛ්‍ය දත්ත වෙත ප්‍රවේශය"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"දහනය කරන ලද කැලරි කියවන්න"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"යෙදුමට දහන කරන ලද කැලරි කියවීමට ඉඩ දෙයි"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"පසුබිම් කියවීම"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"පසුබිම් කියවීම"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"පසුබිමේ සෞඛ්‍ය දත්ත කියවන්න"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"දහනය කළ ක්‍රීයාකාරී කැලරි"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"දහනය කළ ක්‍රීයාකාරී කැලරි"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"දහනය කළ ක්‍රියාකාරී කැලරි කියවන්න"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"ව්‍යායාම මාර්ගය"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"ව්‍යායාම මාර්ගය ලියන්න"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"ව්‍යායාම මාර්ගය කියවන්න"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"සියලු ව්‍යායාම මාර්ග කියවන්න"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"දුර"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"දුර"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"දුර කියවන්න"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"ඔබේ කිසිදු යෙදුමකට Health Connect වෙත ප්‍රවේශ වීමට හෝ නව දත්ත එක් කිරීමට නොහැකි වේ. මෙය පවතින දත්ත කිසිවක් මකන්නේ නැත.\n\nමෙය ස්ථානය, කැමරාව හෝ මයික්‍රොෆෝනය වැනි මෙම යෙදුමට තිබිය හැකි වෙනත් අවසරවලට බලපාන්නේ නැත."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"සියල්ල ඉවත් කරන්න"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"යෙදුම කළමනා කරන්න"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"යෙදුම් දත්ත බලන්න"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"යෙදුම් දත්ත මකන්න"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"අකර්මණ්‍ය යෙදුම්"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"මෙම යෙදුම්වලට තවදුරටත් ප්‍රවේශය නැති නමුත්, තවමත් Health Connect තුළ දත්ත ගබඩා කර ඇත"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"යෙදුම් මූලාශ්‍ර නැත"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"<xliff:g id="CATEGORY_NAME">%1$s</xliff:g> දත්ත ලිවීමට ඔබ යෙදුම් අවසර දුන් පසු මූලාශ්‍ර මෙහි පෙන්වනු ලැබේ."</string>
     <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="app_data_title" msgid="6499967982291000837">"යෙදුම් දත්ත"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"Health Connect වෙත ප්‍රවේශය ඇති යෙදුම් වෙතින් දත්ත මෙහි පෙන්වනු ඇත"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"දිනය"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"සතිය"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"මාසය"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"මෙම සතිය"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"පසුගිය සතිය"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"මෙම මාසය"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"පසුගිය මාසය"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"ඇතුළත් කිරීම්"</string>
+    <string name="tab_access" msgid="7818197975407243701">"ප්‍රවේශය"</string>
 </resources>
diff --git a/apk/res/values-sk/strings.xml b/apk/res/values-sk/strings.xml
index 1bf5f31..0d853fc 100644
--- a/apk/res/values-sk/strings.xml
+++ b/apk/res/values-sk/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"prístup k vašim údajom o zdraví"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Čít. údajov o spál. kalóriách"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Umožňuje aplikáciám čítať údaje o spálených kalóriách"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Čítanie na pozadí"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"čítanie na pozadí"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Čítať zdravotné údaje na pozadí"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Kalórie spálené počas aktivity"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"kalórie spálené počas aktivity"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Čítanie údajov o kalóriách spálených počas aktivity"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"trasa cvičenia"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Zapisovať trasu cvičenia"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Čítať trasu cvičenia"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Čítať všetky trasy cvičenia"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Vzdialenosť"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"vzdialenosť"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Čítanie údajov o vzdialenosti"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Žiadne aplikácie už nebudú mať prístup k údajom Dát o zdraví ani do nich nebudú môcť pridávať nové údaje. Žiadne existujúce údaje sa tým neodstránia.\n\nNeovplyvní to ďalšie povolenia, ktoré táto aplikácia môže mať, ako je poloha, kamera alebo mikrofón."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Odstrániť všetky"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Správa aplikácie"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Zobraziť dáta aplikácie"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Odstrániť údaje aplikácie"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Neaktívne aplikácie"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Tieto aplikácie už nemajú prístup, ale stále majú údaje uložené v Dátach o zdraví"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Žiadne zdroje týkajúce sa aplikácií"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Tu sa budú zobrazovať zdroje, keď udelíte povolenia aplikácie zapisovať údaje v kategórii <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Týždeň"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Mesiac"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Tento týždeň"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Minulý týždeň"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Tento mesiac"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Minulý mesiac"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Vstupy"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Prístup"</string>
 </resources>
diff --git a/apk/res/values-sl/strings.xml b/apk/res/values-sl/strings.xml
index 69467e2..b5541bc 100644
--- a/apk/res/values-sl/strings.xml
+++ b/apk/res/values-sl/strings.xml
@@ -25,7 +25,7 @@
     <string name="all_categories_title" msgid="1446410643217937926">"Vse kategorije"</string>
     <string name="see_all_categories" msgid="5599882403901010434">"Pokaži vse kategorije"</string>
     <string name="no_data" msgid="1906986019249068659">"Ni podatkov"</string>
-    <string name="connected_apps_title" msgid="279942692804743223">"Dovoljenja za aplikacijo"</string>
+    <string name="connected_apps_title" msgid="279942692804743223">"Dovoljenja za aplikacije"</string>
     <string name="connected_apps_subtitle" msgid="8464462995533399175">"Upravljanje aplikacij in dovoljenj"</string>
     <string name="connected_apps_button_subtitle" msgid="8855528937028500370">"Dostop ima toliko aplikacij: <xliff:g id="NUM_APPS_CONNECTED">%1$s</xliff:g> od <xliff:g id="NUM_POSSIBLE_APPS">%2$s</xliff:g>."</string>
     <string name="connected_apps_all_apps_connected_subtitle" msgid="3432698291862059492">"Dostop ima toliko aplikacij: <xliff:g id="NUM_APPS_CONNECTED">%1$s</xliff:g>."</string>
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"dostop do podatkov o zdravju"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Branje porabljenih kalorij"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Aplikaciji omogoča branje porabljenih kalorij"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Branje v ozadju"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"branje v ozadju"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Branje podatkov o zdravju v ozadju"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Porabljene kalorije med dejavnostjo"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"porabljene kalorije med dejavnostjo"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Branje porabljenih kalorij med dejavnostjo"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"vadbena pot"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Zapisovanje vadbene poti"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Branje vadbene poti"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Branje vseh vadbenih poti"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Razdalja"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"razdalja"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Branje razdalje"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Nobena od vaših aplikacij ne bo mogla dostopati do podatkov v aplikaciji Health Connect ali vanjo dodajati novih podatkov. S tem ne izbrišete obstoječih podatkov.\n\nTo ne vpliva na druga dovoljenja, ki jih morda ima ta aplikacija, na primer za lokacijo, fotoaparat ali mikrofon."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Odstrani vse"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Upravljanje aplikacije"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Prikaži podatke aplikacije"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Izbriši podatke aplikacije"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Neaktivne aplikacije"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Te aplikacije nimajo več dostopa, njihovi podatki pa so še vedno shranjeni v storitvi Health Connect."</string>
@@ -278,7 +283,7 @@
     <string name="manage_permissions_learn_more" msgid="2503189875093300767">"Preberite pravilnik o zasebnosti"</string>
     <string name="app_perms_content_provider_24h" msgid="5977152673988158889">"Dostop v zadnjih 24 urah"</string>
     <string name="app_access_title" msgid="7137018424885371763">"Dostop aplikacije"</string>
-    <string name="connected_apps_empty_list_section_title" msgid="6821215432694207342">"Trenutno nimate nameščene nobene združljive aplikacije"</string>
+    <string name="connected_apps_empty_list_section_title" msgid="6821215432694207342">"Trenutno nimate nameščene nobene združljive aplikacije."</string>
     <string name="denied_apps_banner_title" msgid="1997745063608657965">"Odstranjena dovoljenja za aplikacije"</string>
     <string name="denied_apps_banner_message_one_app" msgid="17659513485678315">"Storitev Health Connect je odstranila dovoljenja za aplikacijo <xliff:g id="APP_DATA">%s</xliff:g>."</string>
     <string name="denied_apps_banner_message_two_apps" msgid="1147216810892373640">"Storitev Health Connect je odstranila dovoljenja za aplikaciji <xliff:g id="APP_DATA_0">%1$s</xliff:g> in <xliff:g id="APP_DATA_TWO">%2$s</xliff:g>."</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Ni virov aplikacij."</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Ko boste aplikacijam dodelili dovoljenja za zapisovanje podatkov vrste »<xliff:g id="CATEGORY_NAME">%1$s</xliff:g>«, bodo viri prikazani tukaj."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Teden"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Mesec"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Ta teden"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Prejšnji teden"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Ta mesec"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Prejšnji mesec"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Vnosi"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Dostop"</string>
 </resources>
diff --git a/apk/res/values-sq/strings.xml b/apk/res/values-sq/strings.xml
index b77efcf..1ff1aa2 100644
--- a/apk/res/values-sq/strings.xml
+++ b/apk/res/values-sq/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"të ketë qasje te të dhënat e tua të shëndetit"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Të lexojë kaloritë e djegura"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Lejon aplikacionin që të lexojë kaloritë e djegura"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Leximi në sfond"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"leximi në sfond"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Të lexojë të dhënat e shëndetit në sfond"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Kalori aktive të djegura"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"kalori aktive të djegura"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Të lexojë të dhënat për kaloritë aktive të djegura"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"itinerari i stërvitjes"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Të shkruajë itinerarin e stërvitjes"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Lexo itinerarin e stërvitjes"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Të lexojë të gjitha itineraret e stërvitjeve"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Distanca"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"distanca"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Të lexojë të dhënat për distancën"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Asnjë nga aplikacionet e tua nuk do të mund të ketë qasje ose të shtojë të dhëna të reja te Health Connect. Kjo nuk fshin asnjë të dhënë ekzistuese.\n\nKjo nuk ndikon te lejet e tjera që ky aplikacion mund të ketë, si p.sh. te vendndodhja, kamera apo mikrofoni."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Hiqi të gjitha"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Menaxho aplikacionin"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Shiko të dhënat e aplikacionit"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Fshi të dhënat e aplikacionit"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Aplikacionet joaktive"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Këto aplikacione nuk kanë më qasje, por kanë ende të dhëna të ruajtura në Health Connect"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Nuk ka burime aplikacionesh"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Pasi t\'i japësh aplikacionit leje për të shkruar të dhëna të <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>, burimet do të shfaqen këtu."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Java"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Muaji"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Këtë javë"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Java e fundit"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Këtë muaj"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Muaji i fundit"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Hyrjet"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Qasja"</string>
 </resources>
diff --git a/apk/res/values-sr/strings.xml b/apk/res/values-sr/strings.xml
index b8c2f3e..0fa793f 100644
--- a/apk/res/values-sr/strings.xml
+++ b/apk/res/values-sr/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"приступ подацима о здрављу"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Читање потрошених калорија"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Дозвољава апликацији да чита потрошене калорије"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Читање у позадини"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"читање у позадини"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Читај податке о здрављу у позадини"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Утрошене активне калорије"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"утрошене активне калорије"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Читање утрошених активних калорија"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"рута вежбања"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Уписивање руте вежбања"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Читање руте вежбања"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Читај све руте вежбања"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Раздаљина"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"раздаљина"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Читање података о раздаљини"</string>
@@ -227,10 +231,10 @@
     <string name="oxygen_saturation_lowercase_label" msgid="7264179897533866327">"засићеност кисеоником"</string>
     <string name="oxygen_saturation_read_content_description" msgid="4756434113425028212">"Читање података о засићености кисеоником"</string>
     <string name="oxygen_saturation_write_content_description" msgid="7189901097196830875">"Уписивање података о засићености кисеоником"</string>
-    <string name="respiratory_rate_uppercase_label" msgid="4609498171205294389">"Респираторна брзина"</string>
-    <string name="respiratory_rate_lowercase_label" msgid="8138249029197360098">"респираторна брзина"</string>
-    <string name="respiratory_rate_read_content_description" msgid="8545898979648419722">"Читање података о респираторној брзини"</string>
-    <string name="respiratory_rate_write_content_description" msgid="7689533746809591931">"Уписивање података о респираторној брзини"</string>
+    <string name="respiratory_rate_uppercase_label" msgid="4609498171205294389">"Брзина дисања"</string>
+    <string name="respiratory_rate_lowercase_label" msgid="8138249029197360098">"брзина дисања"</string>
+    <string name="respiratory_rate_read_content_description" msgid="8545898979648419722">"Читање података о брзини дисања"</string>
+    <string name="respiratory_rate_write_content_description" msgid="7689533746809591931">"Уписивање података о брзини дисања"</string>
     <string name="resting_heart_rate_uppercase_label" msgid="5700827752396195453">"Пулс у мировању"</string>
     <string name="resting_heart_rate_lowercase_label" msgid="4533866739695973169">"пулс у мировању"</string>
     <string name="resting_heart_rate_read_content_description" msgid="1068160055773401020">"Читање података о пулсу у мировању"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Ниједна апликација неће моћи да приступа подацима Повезивања здравља ни да додаје нове податке у њега. Тиме се не бришу постојећи подаци.\n\nТо не утиче на друге дозволе које ова апликација може да има, попут локације, камере или микрофона."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Уклони све"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Управљајте апликацијом"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Прикажи податке апликација"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Избриши податке апликација"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Неактивне апликације"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Ове апликације више немају приступ, али и даље имају сачуване податке у Повезивању здравља"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Нема извора апликација"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Када дате дозволе за апликације за уписивање података из категорије <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>, извори ће се приказивати овде."</string>
     <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="app_data_title" msgid="6499967982291000837">"Подаци апликација"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"Подаци из апликација са приступом Повезивању здравља приказаће се овде"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"Дан"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"Недеља"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Месец"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Ове недеље"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Прошла недеља"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Овог месеца"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Прошли месец"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Уноси"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Приступ"</string>
 </resources>
diff --git a/apk/res/values-sv/strings.xml b/apk/res/values-sv/strings.xml
index 68e2f7a..93e77b0 100644
--- a/apk/res/values-sv/strings.xml
+++ b/apk/res/values-sv/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"åtkomst till hälsodata"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Läsbehörighet för brända kal."</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Tillåter att appen läser antalet brända kalorier"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Bakgrundsläsning"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"bakgrundsläsning"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Läs hälsodata i bakgrunden"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Förbrända kalorier vid aktivitet"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"förbrända kalorier vid aktivitet"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Läsbehörighet för antalet förbrända kalorier vid aktivitet"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"träningsrutt"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Skapa träningsrutt"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Läs träningsrutt"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Läsa alla träningsrutter"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Sträcka"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"sträcka"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Läsbehörighet för sträcka"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Inga av dina appar kan komma åt eller lägga till ny data i Health Connect. Ingen befintlig data raderas.\n\nDetta påverkar inte andra behörigheter som appen kanske har, t.ex. plats-, kamera- eller mikrofonbehörighet."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Ta bort alla"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Hantera app"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Se appdata"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Radera appdata"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Inaktiva appar"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"De här apparna har inte längre åtkomst till Health Connect, men har fortfarande data lagrad där"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Inga appkällor"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"När du ger appar behörighet att skriva <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>-data visas källorna här."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Vecka"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Månad"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Den här veckan"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Förra veckan"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Den här månaden"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Förra månaden"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Poster"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Åtkomst"</string>
 </resources>
diff --git a/apk/res/values-sw/strings.xml b/apk/res/values-sw/strings.xml
index fef0000..4eace2a 100644
--- a/apk/res/values-sw/strings.xml
+++ b/apk/res/values-sw/strings.xml
@@ -63,7 +63,7 @@
     <string name="delete_all_data_button" msgid="7238755635416521487">"Futa data yote"</string>
     <string name="no_categories" msgid="2636778482437506241">"Huna data yoyote kwenye Health Connect"</string>
     <string name="permission_types_title" msgid="7698058200557389436">"Data yako"</string>
-    <string name="app_priority_button" msgid="3126133977893705098">"Kipaumbele cha programu"</string>
+    <string name="app_priority_button" msgid="3126133977893705098">"Programu yenye kipaumbele"</string>
     <string name="delete_category_data_button" msgid="2324773398768267043">"Futa data ya <xliff:g id="CATEGORY">%s</xliff:g>"</string>
     <string name="select_all_apps_title" msgid="884487568464305913">"Programu zote"</string>
     <string name="can_read" msgid="4568261079308309564">"Zinazoweza kusoma <xliff:g id="PERMISSION_TYPE">%s</xliff:g>"</string>
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"Ifikie data yako ya afya"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Isome data ya kalori ulizotumia"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Huruhusu programu kusoma data ya kalori ulizotumia"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Kusoma chinichini"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"kusoma chinichini"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Kusoma data ya afya chinichini"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Kalori ulizotumia"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"kalori ulizotumia"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Isome data ya kalori ulizotumia"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"njia ya mazoezi"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Andika njia ya mazoezi"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Soma njia ya mazoezi"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Kusoma njia zote za mazoezi"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Umbali"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"umbali"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Isome data ya umbali"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Hakuna programu yako yoyote itakayoweza kufikia au kuweka data mpya kwenye Health Connect. Hatua hii haifuti data yoyote iliyopo.\n\nPia, haitaathiri ruhusa zingine ambazo programu hii inaweza kuwa nazo, kama vile ruhusa ya mahali, kamera au maikrofoni."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Ondoa zote"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Dhibiti programu"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Angalia data ya programu"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Futa data ya programu"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Programu zilizozimwa"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Programu hizi hazina tena ufikiaji, lakini bado zina data iliyohifadhiwa kwenye Health Connect"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Hamna vyanzo vya programu"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Ukishaipa programu ruhusa za kuandika data ya<xliff:g id="CATEGORY_NAME">%1$s</xliff:g>, vyanzo vitaonekana hapa."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Wiki"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Mwezi"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Wiki hii"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Wiki iliyopita"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Mwezi huu"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Mwezi uliopita"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Vipengee"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Idhini"</string>
 </resources>
diff --git a/apk/res/values-ta/strings.xml b/apk/res/values-ta/strings.xml
index 2c72983..2178f2c 100644
--- a/apk/res/values-ta/strings.xml
+++ b/apk/res/values-ta/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"உங்கள் உடல் ஆரோக்கியம் தொடர்பான தரவை அணுகும்"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"எரித்த கலோரிகளை வாசிக்கும்"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"எரிக்கப்பட்ட கலோரிகள் குறித்த தரவை வாசிக்க ஆப்ஸை அனுமதிக்கும்"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"பின்னணி வாசிப்பு"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"பின்னணி வாசிப்பு"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"பின்னணியில் உடல்நலம் தொடர்பான தரவை வாசிக்கும்"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"செயல்பாட்டின்போது எரிக்கப்பட்ட கலோரிகள்"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"செயல்பாட்டின் மூலம் எரிக்கப்பட்ட கலோரிகள்"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"செயல்பாட்டின்போது எரிக்கப்பட்ட கலோரிகள் தொடர்பான தரவை வாசிக்கும்"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"உடற்பயிற்சிப் பாதை"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"உடற்பயிற்சிப் பாதை தொடர்பான தரவை எழுதும்"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"உடற்பயிற்சிப் பாதையைப் படியுங்கள்"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"உடற்பயிற்சிக்கான வழிகள் அனைத்தையும் வாசி"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"தொலைவு"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"தொலைவு"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"தொலைவு தொடர்பான தரவை வாசிக்கும்"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"உங்களிடமுள்ள எந்தவொரு ஆப்ஸாலும் Health Connectடில் புதிய தரவைச் சேர்க்க/அணுக முடியாது. ஏற்கெனவே உள்ள தரவு எதையும் இது நீக்காது.\n\nஇது இந்த ஆப்ஸுக்கு இருக்கக்கூடிய பிற அனுமதிகளை (இருப்பிடம், கேமரா, மைக்ரோஃபோன் போன்றவை) பாதிக்காது."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"அனைத்தையும் அகற்று"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"ஆப்ஸை நிர்வகித்தல்"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"ஆப்ஸ் தரவைக் காட்டு"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"ஆப்ஸ் தரவை நீக்கு"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"செயலில் இல்லாத ஆப்ஸ்"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"இந்த ஆப்ஸுக்கு இனி அணுகல் இருக்காது என்றாலும் அவை சேமித்த தரவு Health Connectடில் அப்படியே இருக்கும்"</string>
@@ -673,7 +678,7 @@
     <string name="temperature_unit_kelvin_label" msgid="3786210768294615821">"கெல்வின்"</string>
     <string name="help_and_feedback" msgid="4772169905005369871">"உதவி &amp; கருத்து"</string>
     <string name="cant_see_all_your_apps_description" msgid="7344859063463536472">"நிறுவப்பட்ட ஓர் ஆப்ஸ் காட்டப்படவில்லை எனில், அது Health Connect ஆப்ஸுடன் இன்னும் இணங்காமல் இருக்கக்கூடும்"</string>
-    <string name="things_to_try" msgid="8200374691546152703">"பயன்படுத்திப் பார்க்க வேண்டியவை"</string>
+    <string name="things_to_try" msgid="8200374691546152703">"இவற்றைப் பாருங்கள்"</string>
     <string name="check_for_updates" msgid="3841090978657783101">"புதுப்பிப்புகளைக் காட்டு"</string>
     <string name="check_for_updates_description" msgid="1347667778199095160">"நிறுவப்பட்டுள்ள ஆப்ஸ் சமீபத்தியவையாக உள்ளதை உறுதிசெய்யும்"</string>
     <string name="see_all_compatible_apps" msgid="6791146164535475726">"இணக்கமான ஆப்ஸ் அனைத்தையும் காட்டு"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"ஆப்ஸ் தகவல்கள் எதுவுமில்லை"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"<xliff:g id="CATEGORY_NAME">%1$s</xliff:g> தரவை எழுதுவதற்கு நீங்கள் ஆப்ஸ் அனுமதிகள் வழங்கியதும் அவை இங்கே காட்டப்படும்."</string>
     <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="app_data_title" msgid="6499967982291000837">"ஆப்ஸ் தரவு"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"Health Connectடுக்கான அணுகல் உள்ள ஆப்ஸின் தரவு இங்கே காட்டப்படும்"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"நாள்"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"வாரம்"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"மாதம்"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"இந்த வாரம்"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"கடந்த வாரம்"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"இந்த மாதம்"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"கடந்த மாதம்"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"உள்ளீடுகள்"</string>
+    <string name="tab_access" msgid="7818197975407243701">"அணுகல்"</string>
 </resources>
diff --git a/apk/res/values-te/strings.xml b/apk/res/values-te/strings.xml
index 91fb0ad..d458d77 100644
--- a/apk/res/values-te/strings.xml
+++ b/apk/res/values-te/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"మీ ఆరోగ్య డేటాను యాక్సెస్ చేయండి"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"ఖర్చు అయిన కేలరీలను చదువుతుంది"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"ఖర్చు అయిన కేలరీలను చదవడానికి యాప్‌ను అనుమతిస్తుంది"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"బ్యాక్ గ్రౌండ్ రీడింగ్"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"బ్యాక్ గ్రౌండ్ రీడింగ్"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"బ్యాక్‌గ్రౌండ్‌లో ఆరోగ్య డేటాను చదవండి"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"ఖర్చు అయిన యాక్టివ్ కేలరీల డేటా"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"ఖర్చు అయిన యాక్టివ్ కేలరీల డేటా"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"ఖర్చు అయిన యాక్టివ్ కేలరీల డేటాను చదువుతుంది"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"వ్యాయామ విధానం"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"వ్యాయామ మార్గం గురించి రాయండి"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"వ్యాయామ మార్గాన్ని చదవండి"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"వ్యాయామ సెషన్‌కు సంబంధించిన రూట్‌లన్నింటిని చదవండి"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"దూరం"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"దూరం"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"దూరం డేటాను చదువుతుంది"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"మీ యాప్‌లు ఏవీ Health Connectకు సంబంధించిన కొత్త డేటాను యాక్సెస్ చేయలేవు లేదా జోడించలేవు. ఇప్పటికే ఉన్న ఏ డేటానూ ఇది తొలగించదు.\n\n ఈ యాప్ కలిగి ఉండే అవకాశమున్న లొకేషన్, కెమెరా లేదా మైక్రోఫోన్ వంటి ఇతర అనుమతులను ఈ చర్య ప్రభావితం చేయదు."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"అన్నీ తీసివేయండి"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"యాప్‌ను మేనేజ్ చేయండి"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"యాప్ డేటాను చూడండి"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"యాప్ డేటాను తొలగించండి"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"ఇన్‌యాక్టివ్ యాప్‌లు"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"ఈ యాప్‌లకు ఇకపై యాక్సెస్ లేదు, కానీ Health Connect‌లో ఇప్పటికీ డేటా స్టోర్ అయ్యి ఉంటుంది"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"యాప్ సోర్స్‌లు లేవు"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"మీరు <xliff:g id="CATEGORY_NAME">%1$s</xliff:g> డేటాను రాయడానికి యాప్ అనుమతులు ఇచ్చిన తర్వాత, సోర్స్‌లు ఇక్కడ చూపబడతాయి."</string>
     <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="app_data_title" msgid="6499967982291000837">"యాప్ డేటా"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"Health Connectకు యాక్సెస్ ఉన్న యాప్‌ల డేటా ఇక్కడ చూపబడుతుంది"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"రోజు"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"వారం"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"నెల"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"ఈ వారం"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"గత వారం"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"ఈ నెల"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"గత నెల"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"ఎంట్రీలు"</string>
+    <string name="tab_access" msgid="7818197975407243701">"యాక్సెస్"</string>
 </resources>
diff --git a/apk/res/values-th/strings.xml b/apk/res/values-th/strings.xml
index c17c456..8546ad2 100644
--- a/apk/res/values-th/strings.xml
+++ b/apk/res/values-th/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"เข้าถึงข้อมูลสุขภาพของคุณ"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"อ่านข้อมูลแคลอรี่ที่ใช้ไป"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"อนุญาตให้แอปอ่านข้อมูลแคลอรี่ที่ใช้ไป"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"การอ่านในเบื้องหลัง"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"การอ่านในเบื้องหลัง"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"อ่านข้อมูลสุขภาพในเบื้องหลัง"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"แคลอรี่ที่ใช้ไปในการเคลื่อนไหวร่างกาย"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"แคลอรี่ที่ใช้ไปในการเคลื่อนไหวร่างกาย"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"อ่านข้อมูลแคลอรี่ที่ใช้ไปในการเคลื่อนไหวร่างกาย"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"เส้นทางออกกำลังกาย"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"เขียนเส้นทางออกกำลังกาย"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"อ่านเส้นทางออกกำลังกาย"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"อ่านเส้นทางออกกำลังกายทั้งหมด"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"ระยะทาง"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"ระยะทาง"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"อ่านข้อมูลระยะทาง"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"ไม่มีแอปที่เข้าถึงหรือเพิ่มข้อมูลใหม่ใน Health Connect ได้ การดำเนินการนี้จะไม่ลบข้อมูลที่มีอยู่\n\nอีกทั้งไม่มีผลกับสิทธิ์อื่นๆ ที่แอปนี้อาจมี เช่น สิทธิ์เข้าถึงตำแหน่ง กล้อง หรือไมโครโฟน"</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"นำออกทั้งหมด"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"จัดการแอป"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"ดูข้อมูลแอป"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"ลบข้อมูลแอป"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"แอปที่ไม่ได้ใช้งาน"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"แอปเหล่านี้ไม่มีสิทธิ์เข้าถึงอีกต่อไป แต่จะยังคงมีข้อมูลจัดเก็บอยู่ใน Health Connect"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"ไม่มีแหล่งที่มาของแอป"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"เมื่อคุณให้สิทธิ์แก่แอปในการเขียนข้อมูล <xliff:g id="CATEGORY_NAME">%1$s</xliff:g> แล้ว แหล่งที่มาจะแสดงขึ้นที่นี่"</string>
     <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="app_data_title" msgid="6499967982291000837">"ข้อมูลแอป"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"ข้อมูลจากแอปที่มีสิทธิ์เข้าถึง Health Connect จะแสดงที่นี่"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"วัน"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"สัปดาห์"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"เดือน"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"สัปดาห์นี้"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"สัปดาห์ที่แล้ว"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"เดือนนี้"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"เดือนที่แล้ว"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"รายการ"</string>
+    <string name="tab_access" msgid="7818197975407243701">"การเข้าถึง"</string>
 </resources>
diff --git a/apk/res/values-tl/strings.xml b/apk/res/values-tl/strings.xml
index efa1793..c079047 100644
--- a/apk/res/values-tl/strings.xml
+++ b/apk/res/values-tl/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"i-access ang iyong data ng kalusugan"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"I-read ang na-burn na calorie"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Pinapayagan ang app na i-read ang mga na-burn na calorie"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Pag-read sa background"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"pag-read sa background"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"I-read ang data ng kalusugan sa background"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Mga calorie na na-burn habang aktibo"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"mga calorie na na-burn habang aktibo"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"I-read ang mga calorie na na-burn habang aktibo"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"ruta ng pag-eehersisyo"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Sumulat ng ruta ng pag-eehersisyo"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"I-read ang ruta ng pag-eehersisyo"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Basahin ang lahat ng ruta ng pag-e-ehersisyo"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Layo"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"layo"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"I-read ang layo"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Hindi na makaka-access o makakapagdagdag ng bagong data sa Health Connect ang anumang app mo. Hindi nito dine-delete ang anumang kasalukuyang data.\n\nHindi nito maaapektuhan ang iba pang pahintulot na posibleng mayroon ang app na ito, tulad ng lokasyon, camera, o mikropono."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Alisin lahat"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Pamahalaan ang app"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Tingnan ang data ng app"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"I-delete ang data ng app"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Mga hindi aktibong app"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Wala nang access ang mga app na ito, pero mayroon pa ring data ang mga ito na naka-store sa Health Connect"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Walang source ng app"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Kapag nagbigay ka na ng mga pahintulot sa app na mag-write ng data na <xliff:g id="CATEGORY_NAME">%1$s</xliff:g>, lalabas dito ang mga source."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Linggo"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Buwan"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Ngayong linggo"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Nakaraang linggo"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Ngayong buwan"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Nakaraang buwan"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Mga Entry"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Access"</string>
 </resources>
diff --git a/apk/res/values-tr/strings.xml b/apk/res/values-tr/strings.xml
index 79283a4..81a8a6c 100644
--- a/apk/res/values-tr/strings.xml
+++ b/apk/res/values-tr/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"sağlık verinize erişme"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Yakılan kaloriyi okuma"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Uygulamanın yakılan kaloriyi okumasına izin verir"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Arka planda okuma"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"arka planda okuma"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Arka planda sağlık verileri okunur"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Yakılan aktif kalori"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"yakılan aktif kalori"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Yakılan aktif kaloriyi oku"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"egzersiz rotası"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Egzersiz rotasını yaz"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Egzersiz rotasını okuma"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Tüm egzersiz rotalarını okuma"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Mesafe"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"mesafe"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Mesafeyi oku"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Uygulamalarınızın hiçbiri Health Connect\'teki verilere erişemez ve Health Connect\'e yeni veri ekleyemez. Bu işlem mevcut verileri silmez.\n\nBu uygulamanın sahip olduğu diğer izinler (ör. konum, kamera, mikrofon) bu durumdan etkilenmez."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Tümünü kaldır"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Uygulamayı yönetin"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Uygulama verilerini göster"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Uygulama verilerini sil"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Etkin olmayan uygulamalar"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Artık erişimi olmayan ancak verileri Health Connect\'te saklanmaya devam eden uygulamalar"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Uygulama kaynağı yok"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Uygulamaya <xliff:g id="CATEGORY_NAME">%1$s</xliff:g> verisi yazma izni verdiğinizde kaynaklar burada gösterilir."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Hafta"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Ay"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Bu hafta"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Geçen hafta"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Bu ay"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Geçen ay"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Girişler"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Erişim"</string>
 </resources>
diff --git a/apk/res/values-uk/strings.xml b/apk/res/values-uk/strings.xml
index 1b8b6e3..ea9f422 100644
--- a/apk/res/values-uk/strings.xml
+++ b/apk/res/values-uk/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"доступ до даних про здоров’я"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Зчитувати спалені калорії"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Дозволяє додатку зчитувати дані про спалені калорії"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Перегляд у фоновому режимі"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"перегляд у фоновому режимі"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Переглядати дані про здоров’я у фоновому режимі"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Спалені калорії під час активності"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"кількість спалених активних калорій"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Зчитувати кількість спалених активних калорій"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"маршрут тренування"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Записувати маршрут тренування"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Зчитувати маршрут тренування"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Переглядати всі маршрути тренувань"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Відстань"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"відстань"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Зчитувати відстань"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Жоден із ваших додатків не матиме доступу до сервісу Health Connect і не зможе зберігати в ньому нові дані. Наявні дані видалено не буде.\n\nЦе не вплине на інші дозволи, надані цьому додатку, наприклад на доступ до геоданих, камери чи мікрофона."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Вилучити всі"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Керування додатком"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Переглянути дані додатка"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Видалити дані додатка"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Неактивні додатки"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Ці додатки більше не мають доступу, однак їх дані досі зберігаються в Health Connect"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Немає додатків-джерел"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Вони з’являться тут, коли ви надасте додатку дозвіл на запис даних у категорії \"<xliff:g id="CATEGORY_NAME">%1$s</xliff:g>\"."</string>
     <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="app_data_title" msgid="6499967982291000837">"Дані додатка"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"Тут відображатимуться дані з додатків, які мають доступ до Health Connect"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"День"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"Тиждень"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Місяць"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Цей тиждень"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Минулий тиждень"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Цей місяць"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Минулий місяць"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Записи"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Доступ"</string>
 </resources>
diff --git a/apk/res/values-ur/strings.xml b/apk/res/values-ur/strings.xml
index 35749db..ef5596f 100644
--- a/apk/res/values-ur/strings.xml
+++ b/apk/res/values-ur/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"اپنی صحت سے متعلق ڈیٹا تک رسائی حاصل کریں"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"برن کی گئی کیلوریز پڑھیں"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"برن کی گئی کیلوریز کا ڈیٹا پڑھنے کے لیے ایپس کو اجازت دیتی ہے"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"پس منظر میں پڑھنا"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"پس منظر میں پڑھنا"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"پس منظر میں صحت کا ڈیٹا پڑھیں"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"برن کی گئی فعال کیلوریز کا ڈیٹا"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"برن کی گئی فعال کیلوریز کا ڈیٹا"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"برن کی گئی فعال کیلوریز کا ڈیٹا پڑھیں"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"ورزش کا راستہ"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"ورزش کا راستہ لکھیں"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"ورزش کا طریقہ پڑھیں"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"ورزش کے تمام روٹس پڑھیں"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"فاصلہ"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"فاصلہ"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"فاصلے سے متعلق ڈیٹا پڑھیں"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"آپ کی کوئی بھی ایپ Health Connect پر نئے ڈیٹا تک رسائی حاصل کرنے یا نیا ڈیٹا شامل کرنے کے قابل نہیں ہوگی۔ یہ کسی بھی موجودہ ڈیٹا کو حذف نہیں کرتا ہے۔\n\nاس سے ایپ کے پاس ممکنہ طور پر موجود دیگر اجازتوں پر اثر نہیں پڑتا، جیسے کہ مقام، کیمرا یا مائیکروفون۔"</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"سبھی ہٹائیں"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"ایپ کا نظم کریں"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"ایپ کے ڈیٹا کو دیکھیں"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"ایپ کا ڈیٹا حذف کریں"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"غیر فعال ایپس"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"ان ایپس کو اب مزید رسائی حاصل نہیں ہے لیکن ان کے پاس اب بھی Health Connect میں ڈیٹا اسٹور ہے"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"ایپ کا کوئی ذریعہ نہیں ہے"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"ایک بار جب آپ ایپ کو <xliff:g id="CATEGORY_NAME">%1$s</xliff:g> ڈیٹا لکھنے کی اجازتیں دیتے ہیں تو ذرائع یہاں دکھائی دیں گے۔"</string>
     <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="app_data_title" msgid="6499967982291000837">"ایپ کا ڈیٹا"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"Health Connect تک رسائی والی ایپس کا ڈیٹا یہاں ظاہر ہوگا"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"دن"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"ہفتہ"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"مہینہ"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"اس ہفتہ"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"پچھلا ہفتہ"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"اس ماہ"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"پچھلا مہینہ"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"اندراج"</string>
+    <string name="tab_access" msgid="7818197975407243701">"رسائی"</string>
 </resources>
diff --git a/apk/res/values-uz/strings.xml b/apk/res/values-uz/strings.xml
index 3a02dc3..d4c310e 100644
--- a/apk/res/values-uz/strings.xml
+++ b/apk/res/values-uz/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"salomatlik axborotiga kirish"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Sarflangan kaloriyani oʻqish"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Ilovaga sarflangan kaloriyalar miqdorini oʻqish uchun ruxsat beradi"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Orqa fonda oʻqish"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"orqa fonda oʻqish"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Orqa fonda salomatlik maʼlumotlarini oʻqish"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Faol sarflangan kaloriyalar"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"faol sarflangan kaloriyalar"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Faol sarflangan kaloriyalar haqidagi axborotni oʻqish"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"mashq yoʻnalishi"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Mashq yoʻnalishini yozing"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Mashq yoʻnalishini oʻqish"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Barcha mashq marshrutlarini oʻqish"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Masofa"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"masofa"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Masofani oʻqish"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Ilovalardan hech biri Health Connectga kira olmaydi va yangi maʼlumotlarni kirita olmaydi. Hech qanday maʼlumot oʻchirilmaydi.\n\nBu ilovada joylashuv, kamera yoki mikrofon kabi boshqa ruxsatlarga taʼsir qilmaydi."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Hammasini tozalash"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Ilovani boshqarish"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Ilova maʼlumotlarini ochish"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Ilova maʼlumotlarini oʻchirsih"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Nofaol ilovalar"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Bu ilovalarda endi ruxsat yoʻq, lekin ularning maʼlumotlari Health Connect ichida saqlanib turadi"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Hech qanday ilova manbasi yoʻq"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Ilovaga <xliff:g id="CATEGORY_NAME">%1$s</xliff:g> axborotini yozish ruxsatini berishingiz bilan manbalar shu yerda chiqadi."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Hafta"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Oy"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Shu hafta"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Avvalgi hafta"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Shu oyda"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Avvalgi oy"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Maʼlumotlar kiritish"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Ruxsat"</string>
 </resources>
diff --git a/apk/res/values-vi/strings.xml b/apk/res/values-vi/strings.xml
index d15a9bf..52ae93c 100644
--- a/apk/res/values-vi/strings.xml
+++ b/apk/res/values-vi/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"truy cập vào dữ liệu sức khoẻ của bạn"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Đọc dữ liệu về lượng calo đã đốt cháy"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Cho phép ứng dụng đọc dữ liệu về lượng calo đã đốt cháy"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Đọc ở chế độ nền"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"đọc ở chế độ nền"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Đọc dữ liệu sức khoẻ ở chế độ nền"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Lượng calo hoạt động đã đốt cháy"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"lượng calo hoạt động đã đốt cháy"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Đọc dữ liệu về lượng calo hoạt động đã đốt cháy"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"tuyến đường tập thể dục"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Ghi dữ liệu về tuyến đường tập thể dục"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Đọc dữ liệu về lộ trình tập luyện"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Đọc tất cả tuyến đường tập thể dục"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Quãng đường"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"quãng đường"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Đọc dữ liệu về quãng đường"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Bạn chưa có ứng dụng nào truy cập hoặc thêm được dữ liệu mới vào Health Connect. Điều này không xoá bất kỳ dữ liệu nào hiện có.\n\nĐiều này không ảnh hưởng đến các quyền khác mà ứng dụng có thể có, chẳng hạn như quyền đối với thông tin vị trí, máy ảnh hoặc micrô."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Xoá tất cả"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Quản lý ứng dụng"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Xem dữ liệu ứng dụng"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Xoá dữ liệu ứng dụng"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Ứng dụng không hoạt động"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Các ứng dụng này không còn quyền truy cập nhưng vẫn có dữ liệu được lưu trữ trong Health Connect"</string>
@@ -757,7 +762,7 @@
     <string name="app_update_needed_banner_title" msgid="4724335956851853802">"Cần cập nhật ứng dụng"</string>
     <string name="app_update_needed_banner_description_single" msgid="2229935331303234217">"<xliff:g id="APP_NAME">%1$s</xliff:g> cần được cập nhật để có thể tiếp tục tích hợp với Health Connect"</string>
     <string name="app_update_needed_banner_description_multiple" msgid="1523113182062764912">"Một số ứng dụng cần được cập nhật để có thể tiếp tục tích hợp với Health Connect"</string>
-    <string name="app_update_needed_banner_button" msgid="8223115764065649627">"Kiểm tra bản cập nhật"</string>
+    <string name="app_update_needed_banner_button" msgid="8223115764065649627">"Kiểm tra để tìm bản cập nhật"</string>
     <string name="migration_pending_permissions_dialog_title" msgid="6019552841791757048">"Tích hợp Health Connect"</string>
     <string name="migration_pending_permissions_dialog_content" msgid="6350115816948005466">"Health Connect đã sẵn sàng tích hợp với hệ thống Android. Nếu bạn cấp ngay quyền truy cập cho <xliff:g id="APP_NAME">%1$s</xliff:g>, một số tính năng sẽ chưa hoạt động được cho đến khi quá trình tích hợp hoàn tất."</string>
     <string name="migration_pending_permissions_dialog_content_apps" msgid="6417173899016940664">"Health Connect đã sẵn sàng tích hợp với hệ thống Android của bạn. Nếu bạn cấp quyền truy cập ứng dụng ngay lúc này, một số tính năng sẽ chưa thể hoạt động cho đến khi quá trình tích hợp hoàn tất."</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Không có nguồn ứng dụng nào"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Sau khi bạn cấp quyền ghi dữ liệu <xliff:g id="CATEGORY_NAME">%1$s</xliff:g> cho ứng dụng, nguồn dữ liệu sẽ xuất hiện ở đây."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Tuần"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Tháng"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Tuần này"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Tuần trước"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Tháng này"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Tháng trước"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Mục nhập"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Quyền truy cập"</string>
 </resources>
diff --git a/apk/res/values-zh-rCN/strings.xml b/apk/res/values-zh-rCN/strings.xml
index 6388318..37366a6 100644
--- a/apk/res/values-zh-rCN/strings.xml
+++ b/apk/res/values-zh-rCN/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"访问您的健康数据"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"读取消耗的卡路里数"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"允许该应用读取消耗的卡路里数"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"后台读取"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"后台读取"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"在后台读取健康数据"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"运动消耗的卡路里"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"运动消耗的卡路里"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"读取运动消耗的卡路里数据"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"锻炼路线"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"写入锻炼路线"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"读取锻炼路线"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"读取所有锻炼路线"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"距离"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"距离"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"读取距离数据"</string>
@@ -241,10 +245,10 @@
     <string name="request_permissions_allow" msgid="4201324235711040631">"允许"</string>
     <string name="request_permissions_allow_all" msgid="3419414351406638770">"全部允许"</string>
     <string name="request_permissions_dont_allow" msgid="6375307410951549030">"不允许"</string>
-    <string name="request_permissions_header_desc" msgid="5561173070722750153">"选择允许此应用读取或写入 Health Connect 的数据"</string>
-    <string name="request_permissions_header_time_frame_desc" msgid="4617392728203291453">"如果您授予读取权限,此应用可以读取新数据和过去 30 天的数据"</string>
+    <string name="request_permissions_header_desc" msgid="5561173070722750153">"选择允许此应用对 Health Connect 读取或写入数据"</string>
+    <string name="request_permissions_header_time_frame_desc" msgid="4617392728203291453">"如果你授予读取权限,此应用可以读取新数据和过去 30 天的数据"</string>
     <string name="request_permissions_header_title" msgid="4264236128614363479">"要允许<xliff:g id="APP_NAME">%1$s</xliff:g>访问 Health Connect 吗?"</string>
-    <string name="request_permissions_rationale" msgid="6154280355215802538">"您可以参阅开发者的<xliff:g id="PRIVACY_POLICY_LINK">%2$s</xliff:g>,了解<xliff:g id="APP_NAME">%1$s</xliff:g>如何处理您的数据"</string>
+    <string name="request_permissions_rationale" msgid="6154280355215802538">"你可以参阅开发者的<xliff:g id="PRIVACY_POLICY_LINK">%2$s</xliff:g>,了解<xliff:g id="APP_NAME">%1$s</xliff:g>如何处理你的数据"</string>
     <string name="request_permissions_privacy_policy" msgid="228503452643555737">"隐私权政策"</string>
     <string name="permissions_disconnect_dialog_title" msgid="7355211540619034695">"要撤消所有权限吗?"</string>
     <string name="permissions_disconnect_dialog_disconnect" msgid="8854787587948224752">"全部移除"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"您的应用都将无法访问 Health Connect 中的数据,也不能向其中添加新数据。这不会删除任何现有数据。\n\n也不会影响此应用可能拥有的其他权限,例如位置信息、相机或麦克风权限。"</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"全部移除"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"管理应用"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"查看应用数据"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"删除应用数据"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"非活跃应用"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"这些应用已经没有访问权限,但仍有数据存储在 Health Connect 中"</string>
@@ -673,7 +678,7 @@
     <string name="temperature_unit_kelvin_label" msgid="3786210768294615821">"开尔文"</string>
     <string name="help_and_feedback" msgid="4772169905005369871">"帮助和反馈"</string>
     <string name="cant_see_all_your_apps_description" msgid="7344859063463536472">"如果您看不到某个已安装的应用,可能是因为该应用与 Health Connect 尚不兼容"</string>
-    <string name="things_to_try" msgid="8200374691546152703">"可以尝试的方法"</string>
+    <string name="things_to_try" msgid="8200374691546152703">"可以尝试的操作"</string>
     <string name="check_for_updates" msgid="3841090978657783101">"检查是否有更新"</string>
     <string name="check_for_updates_description" msgid="1347667778199095160">"确保安装的应用是最新版本"</string>
     <string name="see_all_compatible_apps" msgid="6791146164535475726">"查看所有兼容应用"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"没有任何应用来源"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"您为应用授予写入<xliff:g id="CATEGORY_NAME">%1$s</xliff:g>数据的权限后,系统就会在这里显示来源。"</string>
     <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="app_data_title" msgid="6499967982291000837">"应用数据"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"具有 Health Connect 访问权的应用中的数据将显示在此处"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"日"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"周"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"月"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"本周"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"上周"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"本月"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"上个月"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"条目"</string>
+    <string name="tab_access" msgid="7818197975407243701">"有访问权限的应用"</string>
 </resources>
diff --git a/apk/res/values-zh-rHK/strings.xml b/apk/res/values-zh-rHK/strings.xml
index 64ef4fa..6f98a40 100644
--- a/apk/res/values-zh-rHK/strings.xml
+++ b/apk/res/values-zh-rHK/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"存取你的健康資料"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"讀取卡路里消耗量資料"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"允許應用程式讀取卡路里消耗量資料"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"背景讀取"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"背景讀取"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"在背景讀取健康資料"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"活動卡路里消耗量"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"活動卡路里消耗量"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"讀取活動卡路里消耗量資料"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"運動路線"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"寫入運動路線"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"讀取運動路線"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"朗讀所有運動路線"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"距離"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"距離"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"讀取距離資料"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"所有應用程式將無法存取 Health Connect,亦無法在當中新增資料。系統並不會刪除任何現有資料。\n\n此操作不會影響此應用程式可能擁有的位置、相機或麥克風等其他權限。"</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"全部移除"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"管理應用程式"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"查看應用程式資料"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"刪除應用程式資料"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"已停用的應用程式"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"這些應用程式不再擁有存取權,但仍有資料儲存在 Health Connect"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"沒有任何應用程式來源"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"當你授予應用程式寫入<xliff:g id="CATEGORY_NAME">%1$s</xliff:g>資料的權限後,系統就會在這裡顯示來源。"</string>
     <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="app_data_title" msgid="6499967982291000837">"應用程式資料"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"有 Health Connect 存取權的應用程式所提供的資料會在這裡顯示"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"日"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"週"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"月"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"本週"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"上週"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"本月"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"上個月"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"項目"</string>
+    <string name="tab_access" msgid="7818197975407243701">"存取權"</string>
 </resources>
diff --git a/apk/res/values-zh-rTW/strings.xml b/apk/res/values-zh-rTW/strings.xml
index 84dde17..635dc08 100644
--- a/apk/res/values-zh-rTW/strings.xml
+++ b/apk/res/values-zh-rTW/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"存取你的健康資料"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"讀取卡路里燃燒量資料"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"允許應用程式讀取卡路里燃燒量資料"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"背景讀取"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"背景讀取"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"在背景讀取健康資料"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"活動卡路里燃燒量"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"活動卡路里燃燒量"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"讀取活動卡路里燃燒量資料"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"運動路線"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"寫入運動路線"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"讀取運動路線"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"讀取所有運動路線"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"距離"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"距離"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"讀取距離資料"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"所有應用程式都將無法存取 Health Connect,也無法在當中新增資料。請注意,這項操作不會刪除任何現有資料。\n\n此操作也不會影響這個應用程式可能具備的其他權限,例如位置、相機或麥克風。"</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"全部移除"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"管理應用程式"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"查看應用程式資料"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"刪除應用程式資料"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"已停用的應用程式"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"這些應用程式已無存取權,但仍有資料儲存在 Health Connect 中"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"沒有應用程式來源"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"當你將寫入<xliff:g id="CATEGORY_NAME">%1$s</xliff:g>資料的權限授予應用程式後,這裡就會顯示來源。"</string>
     <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="app_data_title" msgid="6499967982291000837">"應用程式資料"</string>
+    <string name="no_data_footer" msgid="4777297654713673100">"如果資料是來自有權存取 Health Connect 的應用程式,就會顯示在這裡"</string>
+    <string name="date_picker_day" msgid="3076687507968958991">"一天"</string>
+    <string name="date_picker_week" msgid="1038805538316142229">"一週"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"一個月"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"本週"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"上週"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"本月"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"上個月"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"資料"</string>
+    <string name="tab_access" msgid="7818197975407243701">"存取權"</string>
 </resources>
diff --git a/apk/res/values-zu/strings.xml b/apk/res/values-zu/strings.xml
index 94444d1..bd9831f 100644
--- a/apk/res/values-zu/strings.xml
+++ b/apk/res/values-zu/strings.xml
@@ -79,6 +79,9 @@
     <string name="permgroupdesc_health" msgid="252080476917407273">"finyelela idatha yakho yezempilo"</string>
     <string name="permlab_readCaloriesBurned" msgid="8998140381590624692">"Funda amakholari ashisiwe"</string>
     <string name="permdesc_readCaloriesBurned" msgid="9012595355389868570">"Ivumela i-app ukufunda amakholari ashisiwe"</string>
+    <string name="background_read_uppercase_label" msgid="5727643918930403320">"Ukufunda kungemuva"</string>
+    <string name="background_read_lowercase_label" msgid="2490218479761177680">"ukufunda kungemuva"</string>
+    <string name="background_read_description" msgid="5263745158443722174">"Funda idatha yempilo kungemuva"</string>
     <string name="active_calories_burned_uppercase_label" msgid="6231684842932528272">"Amakhalari asebenzayo ashisiwe"</string>
     <string name="active_calories_burned_lowercase_label" msgid="6743090878253096737">"amakhalari asebenzayo ashisiwe"</string>
     <string name="active_calories_burned_read_content_description" msgid="6449442660408754186">"Funda amakhalari asebenzayo ashisiwe"</string>
@@ -91,6 +94,7 @@
     <string name="exercise_route_lowercase_label" msgid="1691912731748211252">"umzila wokuzivocavoca"</string>
     <string name="exercise_route_write_content_description" msgid="257809942953352611">"Bhala umzila wokuzivocavoca"</string>
     <string name="exercise_route_read_content_description" msgid="8394028537674463440">"Funda indlela yokujima"</string>
+    <string name="exercise_routes_all_read_content_description" msgid="5799035808926978381">"Funda yonke imizila yokujima"</string>
     <string name="distance_uppercase_label" msgid="1420705424462077174">"Ibanga"</string>
     <string name="distance_lowercase_label" msgid="2287154001209381379">"ibanga"</string>
     <string name="distance_read_content_description" msgid="8787235642020285789">"Funda ibanga"</string>
@@ -268,6 +272,7 @@
     <string name="permissions_disconnect_all_dialog_message" msgid="3151109627457270499">"Awekho ama-app akho azokwazi ukufinyelela noma ukwengeza idatha entsha ku-Health Connect. Lokhu akususi noma iyiphi idatha ekhona.\n\nLokhu akuthinti ezinye izimvume le-app okungenzeka inazo, njengendawo, ikhamera, noma imakrofoni."</string>
     <string name="permissions_disconnect_all_dialog_disconnect" msgid="2134136493310257746">"Susa konke"</string>
     <string name="manage_permissions_manage_app_header" msgid="6356348062088358761">"Phatha i-app"</string>
+    <string name="see_app_data" msgid="3951030076195119476">"Bona idatha ye-app"</string>
     <string name="delete_app_data" msgid="6890357774873859952">"Sula idatha ye-app"</string>
     <string name="inactive_apps_section_title" msgid="7492812973696378690">"Izinhlelo zokusebenza ezingasebenzi"</string>
     <string name="inactive_apps_section_message" msgid="2610789262055974739">"Lawa ma-app awasenakho ukufinyelela, kodwa asenayo idatha egcinwe ku-Health Connect"</string>
@@ -783,4 +788,19 @@
     <string name="data_sources_empty_state" msgid="1899652759274805556">"Ayikho imithombo ye-app"</string>
     <string name="data_sources_empty_state_footer" msgid="8933950342291569638">"Ngemva kokunika i-app izimvume zokubhala idatha ye-<xliff:g id="CATEGORY_NAME">%1$s</xliff:g>, imithombo izovela lapha."</string>
     <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="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>
+    <string name="date_picker_week" msgid="1038805538316142229">"Iviki"</string>
+    <string name="date_picker_month" msgid="3560692391260778560">"Inyanga"</string>
+    <string name="this_week_header" msgid="1280121922548216973">"Leli viki"</string>
+    <string name="last_week_header" msgid="5194448963146719382">"Iviki eledlule"</string>
+    <string name="this_month_header" msgid="2452395268894677189">"Le nyanga"</string>
+    <string name="last_month_header" msgid="1359164797239191253">"Inyanga edlule"</string>
+    <string name="tab_entries" msgid="3402700951602029493">"Ukungena"</string>
+    <string name="tab_access" msgid="7818197975407243701">"Ukufinyelela"</string>
 </resources>
diff --git a/apk/res/values/attrs.xml b/apk/res/values/attrs.xml
index 79c4d49..6bc7cbd 100644
--- a/apk/res/values/attrs.xml
+++ b/apk/res/values/attrs.xml
@@ -75,6 +75,11 @@
     <attr name="moreSpaceNeededIcon" format="reference"/>
     <attr name="updateNeededIcon" format="reference"/>
 
+    <!-- Data sources & priority -->
+    <attr name="addIcon" format="reference"/>
+    <attr name="editIcon" format="reference"/>
+    <attr name="closeIcon" format="reference"/>
+
     <!-- END REGION -->
 
     <!-- REGION DIALOG GRAVITY -->
@@ -100,7 +105,9 @@
     <attr name="textAppearanceLabelSmall" format="reference"/>
     <attr name="textAppearanceSubheader" format="reference"/>
     <attr name="textAppearanceSubheader2" format="reference"/>
+    <attr name="textAppearanceSubheader3" format="reference"/>
     <attr name="textAppearanceRouteRequest" format="reference"/>
+    <attr name="textAppearancePreferenceCategory" format="reference"/>
 
     <!-- Buttons -->
     <attr name="dialogButtonFull" format="reference"/>
diff --git a/apk/res/values/dimens.xml b/apk/res/values/dimens.xml
index 3b0c24c..9337d10 100644
--- a/apk/res/values/dimens.xml
+++ b/apk/res/values/dimens.xml
@@ -48,5 +48,6 @@
     <dimen name="recent_access_date_min_width">60dp</dimen>
     <dimen name="recent_access_fab_bottom_padding">90dp</dimen>
 
+    <dimen name="tab_corner_radius">12dp</dimen>
 
 </resources>
diff --git a/apk/res/values/overlayable.xml b/apk/res/values/overlayable.xml
index b1637f0..9370bc3 100644
--- a/apk/res/values/overlayable.xml
+++ b/apk/res/values/overlayable.xml
@@ -67,6 +67,11 @@
             <item type="drawable" name="ic_storage"/>
             <item type="drawable" name="ic_system_security_update"/>
 
+            <!-- Data sources & priority -->
+            <item type="drawable" name="ic_add"/>
+            <item type="drawable" name="ic_edit"/>
+            <item type="drawable" name="ic_close"/>
+
             <!-- END REGION -->
 
             <!-- REGION DIALOG GRAVITY -->
@@ -89,12 +94,16 @@
             <item type="style" name="TextAppearance.HealthConnect.Headline2"/>
             <item type="style" name="TextAppearance.HealthConnect.Label.Small"/>
             <item type="style" name="TextAppearance.HealthConnect.Subheader"/>
+            <item type="style" name="TextAppearance.HealthConnect.Subheader2"/>
+            <item type="style" name="TextAppearance.HealthConnect.Subheader3"/>
             <item type="style" name="TextAppearance.HealthConnect.RouteRequest"/>
+            <item type="style" name="PreferenceCategoryTitleTextStyle.HealthConnect"/>
 
             <item type="style" name="Widget.HealthConnect.Button"/>
             <item type="style" name="Widget.HealthConnect.CheckBox"/>
             <item type="style" name="Widget.HealthConnect.RadioButton.Chip"/>
             <item type="style" name="Widget.HealthConnect.CalendarSpinner"/>
+            <item type="style" name="Widget.HealthConnect.TabLayout"/>
             <!-- END REGION -->
 
             <!-- REGION DIMENSIONS -->
diff --git a/apk/res/values/strings.xml b/apk/res/values/strings.xml
index ce89462..38c4035 100644
--- a/apk/res/values/strings.xml
+++ b/apk/res/values/strings.xml
@@ -111,6 +111,13 @@
     <!--endregion-->
 
     <!-- region permission ui labels -->
+
+    <!-- Other health permissions -->
+    <string name="background_read_uppercase_label" description="Uppercase label used to show background reading permission. [CHAR_LIMIT=50]">Background reading</string>
+    <string name="background_read_lowercase_label" description="Lowercase label used to show background reading permission. [CHAR_LIMIT=50]">background reading</string>
+    <string name="background_read_description" description="Content description for switch that enables permissions for reading health data in the background  [CHAR_LIMIT=50]">Read health data in the background</string>
+    <!--endregion-->
+
     <!-- region Activity data types -->
     <string name="active_calories_burned_uppercase_label" description="Uppercase label used to show the user's active calories burned data [CHAR_LIMIT=50]">Active calories burned</string>
     <string name="active_calories_burned_lowercase_label" description="Lowercase label used to show the user's active calories burned data [CHAR_LIMIT=50]">active calories burned</string>
@@ -126,6 +133,7 @@
     <string name="exercise_route_lowercase_label" description="Lowercase label used to show the user's exercise route [CHAR_LIMIT=50]">exercise route</string>
     <string name="exercise_route_write_content_description" description="Content description for switch that enables permissions for writing the user's exercise route [CHAR_LIMIT=NONE]">Write exercise route</string>
     <string name="exercise_route_read_content_description" description="Content description for switch that enables permissions for reading the user's exercise route [CHAR_LIMIT=NONE]">Read exercise route</string>
+    <string name="exercise_routes_all_read_content_description" description="Content description for switch that enables permissions for reading all user's exercise routes [CHAR_LIMIT=NONE]">Read all exercise routes</string>
 
     <string name="distance_uppercase_label" description="Uppercase label used to show the user's distance data [CHAR_LIMIT=50]">Distance</string>
     <string name="distance_lowercase_label" description="Lowercase label used to show the user's distance data [CHAR_LIMIT=50]">distance</string>
@@ -360,6 +368,7 @@
     <string name="permissions_disconnect_all_dialog_message" description="Text informing the user that all apps will no longer be able to read or write data after the permissions have been removed. Note: Health Connect is the brand. [CHAR_LIMIT=NONE]">None of your apps will be able to access or add new data to Health&#160;Connect. This doesn\'t delete any existing data.\n\nThis doesn\'t affect other permissions this app may have, like location, camera, or microphone.</string>
     <string name="permissions_disconnect_all_dialog_disconnect" description="Label for dialog button to confirm removal of all permissions for all apps [CHAR_LIMIT=20]">Remove all</string>
     <string name="manage_permissions_manage_app_header" description="Title shown for section to manage app [CHAR_LIMIT=40]">Manage app</string>
+    <string name="see_app_data" description="Label of a button that opens the app data screen. [CHAR_LIMIT=40]">See app data</string>
     <string name="delete_app_data" description="Label of a button that deletes data written by given app. [CHAR_LIMIT=40]">Delete app data</string>
     <string name="inactive_apps_section_title" description="Title shown for section of inactive apps [CHAR_LIMIT=40]">Inactive apps</string>
     <string name="inactive_apps_section_message" description="Description text shown in the inactive apps section that explains what inactive apps are [CHAR_LIMIT=NONE]">These apps no longer have access, but still have data stored in Health&#160;Connect</string>
@@ -467,7 +476,8 @@
     <!--endregion-->
 
     <!-- region Data entries -->
-    <string name="data_entry_header" translatable="false"><xliff:g example="14:41" id="time">%1$s</xliff:g> • <xliff:g example="Run tracker" id="app_data">%2$s</xliff:g></string>
+    <string name="data_entry_header_with_source_app" translatable="false"><xliff:g example="14:41" id="time">%1$s</xliff:g> • <xliff:g example="Run tracker" id="app_data">%2$s</xliff:g></string>
+    <string name="data_entry_header_without_source_app" translatable="false"><xliff:g example="14:41" id="time">%1$s</xliff:g></string>
     <string name="data_point_action_content_description" description="Content description for the menu button of an individual data entry [CHAR LIMIT=NONE]">Delete data entry</string>
     <string name="delete_data_point" description="Menu option for deleting an individual data entry [CHAR LIMIT=50]">Delete entry</string>
     <string name="aggregation_total" description="Description before the total value. For instance, Total: 1000 steps. [CHAR LIMIT=50]">Total: <xliff:g id="total_value" example="1000 steps">%s</xliff:g></string>
@@ -1085,6 +1095,29 @@
     <string name="data_sources_empty_state" description="Text for the empty state of the data sources screen explaining why there are no apps shown [CHAR_LIMIT=NONE]">No app sources</string>
     <string name="data_sources_empty_state_footer" description="Text for the footer of the empty state of the data sources screen [CHAR_LIMIT=NONE]">Once you give app permissions to write <xliff:g example="activity" id="category_name">%1$s</xliff:g> data, sources will show here.</string>
     <string name="data_sources_help_link" description="Text for helper link in the data sources screen taking the user to an article about how sources and prioritization work [CHAR_LIMIT=NONE]">How sources &amp; prioritization work</string>
+    <string name="data_sources_add_app" description="Title for preference in the data sources screen used to add an app to the list of data sources [CHAR_LIMIT=NONE]">Add an app</string>
+    <string name="edit_data_sources" description="Menu option for adding or removing apps from the app sources list">Edit app sources</string>
+    <string name="default_app_summary" description="Text for an app on the priority list that is the device default app for health connect data [CHAR_LIMIT=50]">Device default</string>
+    <!--  endregion -->
+
+    <!-- region App data -->
+    <string name="app_data_title" description="Title of screen that shows the data of a certain app. [CHAR_LIMIT=50]">App data</string>
+    <string name="no_data_footer" description="Footer displayed on the empty data browse screen if no data has been written to Health Connect. [CHAR_LIMIT=NONE]">Data from apps with access to Health Connect will show here</string>
+    <!--  endregion -->
+
+    <!-- region App entries -->
+    <string name="date_picker_day" description="Dropdown option that indicates that one day's worth of data is displayed. [CHAR_LIMIT=50]">Day</string>
+    <string name="date_picker_week" description="Dropdown option that indicates that one week's worth of data is displayed. [CHAR_LIMIT=50]">Week</string>
+    <string name="date_picker_month" description="Dropdown option that indicates that one month's worth of data is displayed. [CHAR_LIMIT=50]">Month</string>
+    <string name="this_week_header" description="Header in the list of dataentries to differentiate between entries from this week and last week. [CHAR LIMIT=50]">This week</string>
+    <string name="last_week_header" description="Header in the list of data entries to differentiate between entries from this week and last week. [CHAR LIMIT=50]">Last week</string>
+    <string name="this_month_header" description="Header in the list of data entries to differentiate between entries from this month and last month. [CHAR LIMIT=50]">This month</string>
+    <string name="last_month_header" description="Header in the list of data entries to differentiate between entries from this month and last month. [CHAR LIMIT=50]">Last month</string>
+    <!--  endregion -->
+
+    <!-- region Entries + Access -->
+    <string name="tab_entries" description="Tab that navigates to the data entries screen. [CHAR_LIMIT=50]">Entries</string>
+    <string name="tab_access" description="Tab that navigates to the screen that shows which apps have access to the given data type. [CHAR_LIMIT=50]">Access</string>
     <!--  endregion -->
 
 </resources>
diff --git a/apk/res/values/styles.xml b/apk/res/values/styles.xml
index 8b62e22..2dc7db0 100644
--- a/apk/res/values/styles.xml
+++ b/apk/res/values/styles.xml
@@ -36,7 +36,6 @@
         <item name="android:textFontWeight">500</item>
         <item name="android:textSize">14sp</item>
         <item name="android:lineHeight">20sp</item>
-        <item name="android:letterSpacing">0.025</item>
     </style>
 
     <!-- Preference title 400/18/24-->
@@ -89,6 +88,13 @@
         <item name="android:letterSpacing">0.025</item>
     </style>
 
+    <style name="TextAppearance.HealthConnect.Subheader3" parent="@style/TextAppearance.PreferenceTitle.SettingsLib">
+        <item name="android:textFontWeight">400</item>
+        <item name="android:textSize">14sp</item>
+        <item name="android:lineHeight">24sp</item>
+        <item name="android:letterSpacing">0.02</item>
+    </style>
+
     <style name="TextAppearance.HealthConnect.RouteRequest" parent="@style/TextAppearance.PreferenceTitle.SettingsLib">
         <item name="android:textFontWeight">500</item>
         <item name="android:textSize">12sp</item>
@@ -223,4 +229,20 @@
         <item name="android:foreground">@drawable/filter_chip_button_ripple</item>
     </style>
 
+    <style name="Widget.HealthConnect.TabLayout" parent="@style/Widget.Material3.TabLayout">
+        <item name="android:background">@android:color/transparent</item>
+        <item name="tabMaxWidth">0dp</item>
+        <item name="tabGravity">fill</item>
+        <item name="tabMode">fixed</item>
+        <item name="tabIndicatorHeight">0dp</item>
+        <item name="tabSelectedTextColor">?android:attr/textColorPrimary</item>
+        <item name="tabTextColor">?android:attr/textColorSecondary</item>
+        <item name="tabBackground">@drawable/tab_background</item>
+        <item name="tabTextAppearance">?attr/textAppearanceSubheader</item>
+    </style>
+
+    <style name="PreferenceCategoryTitleTextStyle.HealthConnect">
+        <item name="android:textAppearance">?attr/preferenceCategoryTitleTextAppearance</item>
+        <item name="android:textColor">?attr/preferenceCategoryTitleTextColor</item>
+    </style>
 </resources>
diff --git a/apk/res/values/themes.xml b/apk/res/values/themes.xml
index 3e4b83b..98ba2fd 100644
--- a/apk/res/values/themes.xml
+++ b/apk/res/values/themes.xml
@@ -74,6 +74,11 @@
         <item name="moreSpaceNeededIcon">@drawable/ic_storage</item>
         <item name="updateNeededIcon">@drawable/ic_system_security_update</item>
 
+        <!-- Data sources & priority -->
+        <item name="addIcon">@drawable/ic_add</item>
+        <item name="editIcon">@drawable/ic_edit</item>
+        <item name="closeIcon">@drawable/ic_close</item>
+
         <!-- END REGION -->
 
         <!-- REGION DIALOG GRAVITY -->
@@ -98,7 +103,9 @@
         <item name="textAppearanceLabelSmall">@style/TextAppearance.HealthConnect.Label.Small</item>
         <item name="textAppearanceSubheader">@style/TextAppearance.HealthConnect.Subheader</item>
         <item name="textAppearanceSubheader2">@style/TextAppearance.HealthConnect.Subheader2</item>
+        <item name="textAppearanceSubheader3">@style/TextAppearance.HealthConnect.Subheader3</item>
         <item name="textAppearanceRouteRequest">@style/TextAppearance.HealthConnect.RouteRequest</item>
+        <item name="textAppearancePreferenceCategory">@style/PreferenceCategoryTitleTextStyle.HealthConnect</item>
 
         <!-- Buttons -->
         <item name="dialogButtonFull">@style/Widget.HealthConnect.DialogButton.Full</item>
@@ -116,6 +123,7 @@
         <item name="android:checkboxStyle">@style/Widget.HealthConnect.CheckBox</item>
         <item name="chipStyle">@style/Widget.HealthConnect.RadioButton.Chip</item>
         <item name="spinnerStyle">@style/Widget.HealthConnect.CalendarSpinner</item>
+        <item name="tabStyle">@style/Widget.HealthConnect.TabLayout</item>
         <!-- END REGION -->
 
         <!-- REGION DIMENSIONS -->
diff --git a/apk/res/xml/access_screen.xml b/apk/res/xml/access_screen.xml
new file mode 100644
index 0000000..d7af26e
--- /dev/null
+++ b/apk/res/xml/access_screen.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+-->
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+    android:key="access_screen">
+    <PreferenceCategory
+        android:key="can_read">
+    </PreferenceCategory>
+    <PreferenceCategory
+        android:key="can_write">
+    </PreferenceCategory>
+    <PreferenceCategory
+        android:key="inactive"
+        android:title="@string/inactive_apps">
+    </PreferenceCategory>
+</PreferenceScreen>
\ No newline at end of file
diff --git a/apk/res/xml/add_an_app_screen.xml b/apk/res/xml/add_an_app_screen.xml
new file mode 100644
index 0000000..b3e24e4
--- /dev/null
+++ b/apk/res/xml/add_an_app_screen.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2022 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.
+-->
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+
+</PreferenceScreen>
\ No newline at end of file
diff --git a/apk/res/xml/app_data_screen.xml b/apk/res/xml/app_data_screen.xml
new file mode 100644
index 0000000..d7af26e
--- /dev/null
+++ b/apk/res/xml/app_data_screen.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+-->
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+    android:key="access_screen">
+    <PreferenceCategory
+        android:key="can_read">
+    </PreferenceCategory>
+    <PreferenceCategory
+        android:key="can_write">
+    </PreferenceCategory>
+    <PreferenceCategory
+        android:key="inactive"
+        android:title="@string/inactive_apps">
+    </PreferenceCategory>
+</PreferenceScreen>
\ No newline at end of file
diff --git a/apk/res/xml/connected_app_screen.xml b/apk/res/xml/connected_app_screen.xml
index 48c1752..e011738 100644
--- a/apk/res/xml/connected_app_screen.xml
+++ b/apk/res/xml/connected_app_screen.xml
@@ -30,10 +30,6 @@
     <PreferenceCategory
         android:key="manage_app"
         android:title="@string/manage_permissions_manage_app_header">
-        <com.android.healthconnect.controller.shared.preference.HealthPreference
-            android:key="delete_app_data"
-            android:title="@string/delete_app_data"
-            android:icon="?attr/deleteIcon" />
     </PreferenceCategory>
     <com.android.settingslib.widget.FooterPreference
         android:key="connected_app_footer"
diff --git a/apk/res/xml/data_sources_and_priority_screen.xml b/apk/res/xml/data_sources_and_priority_screen.xml
index ca1809c..4767019 100644
--- a/apk/res/xml/data_sources_and_priority_screen.xml
+++ b/apk/res/xml/data_sources_and_priority_screen.xml
@@ -1,11 +1,25 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="utf-8"?><!--
+  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.
+-->
 <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto">
-<!--    TODO (b/292266625) add data totals cards -->
-<!--    <PreferenceCategory-->
-<!--        android:key="data_totals_group"-->
-<!--        android:title="@string/data_totals_header"-->
-<!--        android:order="1"/>-->
+    <PreferenceCategory
+        android:key="data_totals_group"
+        android:title="@string/data_totals_header"
+        android:order="1"
+        app:isPreferenceVisible="false"/>
     <PreferenceCategory
         android:key="app_sources_group"
         android:title="@string/app_sources_header"
diff --git a/apk/src/com/android/healthconnect/controller/MainActivity.kt b/apk/src/com/android/healthconnect/controller/MainActivity.kt
index b5f625b..4b13be6 100644
--- a/apk/src/com/android/healthconnect/controller/MainActivity.kt
+++ b/apk/src/com/android/healthconnect/controller/MainActivity.kt
@@ -28,8 +28,8 @@
 import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
 import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity
 import dagger.hilt.android.AndroidEntryPoint
-import javax.inject.Inject
 import kotlinx.coroutines.runBlocking
+import javax.inject.Inject
 
 /** Entry point activity for Health Connect. */
 @AndroidEntryPoint(CollapsingToolbarBaseActivity::class)
@@ -51,7 +51,7 @@
             openOnboardingActivity.launch(1)
         }
 
-        val currentMigrationState = runBlocking { migrationViewModel.getCurrentMigrationUiState() }
+        val currentMigrationState = migrationViewModel.getCurrentMigrationUiState()
 
         if (maybeRedirectToMigrationActivity(this, currentMigrationState)) {
             return
@@ -66,7 +66,7 @@
 
     override fun onResume() {
         super.onResume()
-        val currentMigrationState = runBlocking { migrationViewModel.getCurrentMigrationUiState() }
+        val currentMigrationState = migrationViewModel.getCurrentMigrationUiState()
 
         if (maybeRedirectToMigrationActivity(this, currentMigrationState)) {
             return
@@ -78,6 +78,7 @@
         if (!navController.popBackStack()) {
             finish()
         }
+
     }
 
     override fun onNavigateUp(): Boolean {
@@ -85,6 +86,7 @@
         if (!navController.popBackStack()) {
             finish()
         }
+
         return true
     }
 
diff --git a/apk/src/com/android/healthconnect/controller/data/DataManagementActivity.kt b/apk/src/com/android/healthconnect/controller/data/DataManagementActivity.kt
index 1388415..6031655 100644
--- a/apk/src/com/android/healthconnect/controller/data/DataManagementActivity.kt
+++ b/apk/src/com/android/healthconnect/controller/data/DataManagementActivity.kt
@@ -21,6 +21,7 @@
 import android.os.Bundle
 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
@@ -29,29 +30,37 @@
 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.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()
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_data_management)
+        if (featureUtils.isNewInformationArchitectureEnabled()) {
+            updateNavGraphToNewIA()
+        }
 
         if (maybeRedirectIntoTwoPaneSettings(this)) {
             return
         }
 
-        if (maybeRedirectToOnboardingActivity(this)) {
-            return
+        if (maybeRedirectToOnboardingActivity(this) && savedInstanceState == null) {
+            openOnboardingActivity.launch(1)
         }
 
-        val currentMigrationState = runBlocking { migrationViewModel.getCurrentMigrationUiState() }
+        val currentMigrationState = migrationViewModel.getCurrentMigrationUiState()
 
         if (maybeRedirectToMigrationActivity(this, currentMigrationState)) {
             return
@@ -71,6 +80,16 @@
         }
     }
 
+    private fun updateNavGraphToNewIA() {
+        val navRes = R.navigation.data_nav_graph_new_ia
+        val finalHost = NavHostFragment.create(navRes)
+        supportFragmentManager
+            .beginTransaction()
+            .replace(R.id.nav_host_fragment, finalHost)
+            .setPrimaryNavigationFragment(finalHost)
+            .commit()
+    }
+
     override fun onStart() {
         super.onStart()
         findNavController(R.id.nav_host_fragment)
@@ -79,9 +98,9 @@
 
     override fun onResume() {
         super.onResume()
-        val currentMigrationState = runBlocking { migrationViewModel.getCurrentMigrationUiState() }
+        val currentMigrationState = migrationViewModel.getCurrentMigrationUiState()
 
-        if (MigrationActivity.maybeRedirectToMigrationActivity(this, currentMigrationState)) {
+        if (maybeRedirectToMigrationActivity(this, currentMigrationState)) {
             return
         }
     }
@@ -100,4 +119,11 @@
         }
         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/AccessFragment.kt b/apk/src/com/android/healthconnect/controller/data/access/AccessFragment.kt
new file mode 100644
index 0000000..f7499f5
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/data/access/AccessFragment.kt
@@ -0,0 +1,207 @@
+/*
+ * 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.data.access
+
+import android.os.Bundle
+import android.view.View
+import androidx.core.os.bundleOf
+import androidx.fragment.app.viewModels
+import androidx.preference.Preference
+import androidx.preference.PreferenceGroup
+import com.android.healthconnect.controller.R
+import com.android.healthconnect.controller.data.appdata.AppDataFragment.Companion.PERMISSION_TYPE_KEY
+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.DeletionType
+import com.android.healthconnect.controller.permissions.connectedapps.HealthAppPreference
+import com.android.healthconnect.controller.permissions.data.HealthPermissionStrings.Companion.fromPermissionType
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType
+import com.android.healthconnect.controller.shared.app.AppMetadata
+import com.android.healthconnect.controller.shared.inactiveapp.InactiveAppPreference
+import com.android.healthconnect.controller.shared.preference.HealthPreferenceFragment
+import com.android.healthconnect.controller.utils.logging.DataAccessElement
+import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
+import com.android.healthconnect.controller.utils.logging.PageName
+import com.android.healthconnect.controller.utils.logging.ToolbarElement
+import com.android.healthconnect.controller.utils.setTitle
+import com.android.healthconnect.controller.utils.setupMenu
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
+
+/** Fragment displaying health data access information. */
+@AndroidEntryPoint(HealthPreferenceFragment::class)
+class AccessFragment : Hilt_AccessFragment() {
+
+    companion object {
+        private const val CAN_READ_SECTION = "can_read"
+        private const val CAN_WRITE_SECTION = "can_write"
+        private const val INACTIVE_SECTION = "inactive"
+    }
+
+    init {
+        this.setPageName(PageName.DATA_ACCESS_PAGE)
+    }
+
+    @Inject lateinit var logger: HealthConnectLogger
+
+    private val viewModel: AccessViewModel by viewModels()
+
+    private lateinit var permissionType: HealthPermissionType
+
+    private val mCanReadSection: PreferenceGroup? by lazy {
+        preferenceScreen.findPreference(CAN_READ_SECTION)
+    }
+
+    private val mCanWriteSection: PreferenceGroup? by lazy {
+        preferenceScreen.findPreference(CAN_WRITE_SECTION)
+    }
+
+    private val mInactiveSection: PreferenceGroup? by lazy {
+        preferenceScreen.findPreference(INACTIVE_SECTION)
+    }
+
+    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+        super.onCreatePreferences(savedInstanceState, rootKey)
+        setPreferencesFromResource(R.xml.access_screen, rootKey)
+        if (requireArguments().containsKey(PERMISSION_TYPE_KEY)) {
+            permissionType =
+                arguments?.getSerializable(PERMISSION_TYPE_KEY, HealthPermissionType::class.java)
+                    ?: throw IllegalArgumentException("PERMISSION_TYPE_KEY can't be null!")
+        }
+
+        mCanReadSection?.isVisible = false
+        mCanWriteSection?.isVisible = false
+        mInactiveSection?.isVisible = false
+        mCanReadSection?.title =
+            getString(
+                R.string.can_read, getString(fromPermissionType(permissionType).lowercaseLabel))
+        mCanWriteSection?.title =
+            getString(
+                R.string.can_write, getString(fromPermissionType(permissionType).lowercaseLabel))
+    }
+
+    override fun onResume() {
+        super.onResume()
+        setTitle(fromPermissionType(permissionType).uppercaseLabel)
+        viewModel.loadAppMetaDataMap(permissionType)
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        viewModel.loadAppMetaDataMap(permissionType)
+        viewModel.appMetadataMap.observe(viewLifecycleOwner) { state ->
+            when (state) {
+                is AccessViewModel.AccessScreenState.Loading -> {
+                    setLoading(isLoading = true)
+                }
+                is AccessViewModel.AccessScreenState.Error -> {
+                    setError(hasError = true)
+                }
+                is AccessViewModel.AccessScreenState.WithData -> {
+                    setLoading(isLoading = false, animate = false)
+                    updateDataAccess(state.appMetadata)
+                }
+            }
+        }
+
+        setupMenu(R.menu.set_data_units_with_send_feedback_and_help, viewLifecycleOwner, logger) {
+            menuItem ->
+            when (menuItem.itemId) {
+                R.id.menu_open_units -> {
+                    logger.logImpression(ToolbarElement.TOOLBAR_UNITS_BUTTON)
+                    // TODO(b/291249677): Enable in an upcoming CL.
+                    //                    findNavController()
+                    //
+                    // .navigate(R.id.action_entriesAndAccessFragment_to_unitFragment)
+                    true
+                }
+                else -> false
+            }
+        }
+    }
+
+    private fun updateDataAccess(appMetadataMap: Map<AppAccessState, List<AppMetadata>>) {
+        mCanReadSection?.removeAll()
+        mCanWriteSection?.removeAll()
+        mInactiveSection?.removeAll()
+
+        if (appMetadataMap.containsKey(AppAccessState.Read)) {
+            if (appMetadataMap[AppAccessState.Read]!!.isEmpty()) {
+                mCanReadSection?.isVisible = false
+            } else {
+                mCanReadSection?.isVisible = true
+                appMetadataMap[AppAccessState.Read]!!.forEach { _appMetadata ->
+                    mCanReadSection?.addPreference(createAppPreference(_appMetadata))
+                }
+            }
+        }
+        if (appMetadataMap.containsKey(AppAccessState.Write)) {
+            if (appMetadataMap[AppAccessState.Write]!!.isEmpty()) {
+                mCanWriteSection?.isVisible = false
+            } else {
+                mCanWriteSection?.isVisible = true
+                appMetadataMap[AppAccessState.Write]!!.forEach { _appMetadata ->
+                    mCanWriteSection?.addPreference(createAppPreference(_appMetadata))
+                }
+            }
+        }
+        if (appMetadataMap.containsKey(AppAccessState.Inactive)) {
+            if (appMetadataMap[AppAccessState.Inactive]!!.isEmpty()) {
+                mInactiveSection?.isVisible = false
+            } else {
+                mInactiveSection?.isVisible = true
+                mInactiveSection?.addPreference(
+                    Preference(requireContext()).also {
+                        it.summary =
+                            getString(
+                                R.string.inactive_apps_message,
+                                getString(fromPermissionType(permissionType).lowercaseLabel))
+                    })
+                appMetadataMap[AppAccessState.Inactive]?.forEach { _appMetadata ->
+                    mInactiveSection?.addPreference(
+                        InactiveAppPreference(requireContext()).also {
+                            it.title = _appMetadata.appName
+                            it.icon = _appMetadata.icon
+                            it.logName = DataAccessElement.DATA_ACCESS_INACTIVE_APP_BUTTON
+                            it.setOnDeleteButtonClickListener {
+                                // TODO(b/291249677): Update when new deletion flows are ready.
+                                val deletionType =
+                                    DeletionType.DeletionTypeAppData(
+                                        _appMetadata.packageName, _appMetadata.appName)
+                                childFragmentManager.setFragmentResult(
+                                    START_DELETION_EVENT, bundleOf(DELETION_TYPE to deletionType))
+                            }
+                        })
+                }
+            }
+        }
+    }
+
+    private fun createAppPreference(appMetadata: AppMetadata): HealthAppPreference {
+        return HealthAppPreference(requireContext(), appMetadata).also {
+            it.logName = DataAccessElement.DATA_ACCESS_APP_BUTTON
+            it.setOnPreferenceClickListener {
+                // TODO(b/291249677): Enable in an upcoming CL.
+                //                findNavController()
+                //                    .navigate(
+                //                        R.id.action_entriesAndAccessFragment_to_appAccess,
+                //                        bundleOf(EXTRA_PACKAGE_NAME to appMetadata.packageName))
+                true
+            }
+        }
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/dataaccess/HealthDataAccessViewModel.kt b/apk/src/com/android/healthconnect/controller/data/access/AccessViewModel.kt
similarity index 61%
rename from apk/src/com/android/healthconnect/controller/dataaccess/HealthDataAccessViewModel.kt
rename to apk/src/com/android/healthconnect/controller/data/access/AccessViewModel.kt
index e11f1c5..0cd86d8 100644
--- a/apk/src/com/android/healthconnect/controller/dataaccess/HealthDataAccessViewModel.kt
+++ b/apk/src/com/android/healthconnect/controller/data/access/AccessViewModel.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -14,14 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.healthconnect.controller.dataaccess
+package com.android.healthconnect.controller.data.access
 
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
-import com.android.healthconnect.controller.dataaccess.HealthDataAccessViewModel.DataAccessScreenState.Error
-import com.android.healthconnect.controller.dataaccess.HealthDataAccessViewModel.DataAccessScreenState.WithData
+import com.android.healthconnect.controller.data.access.AccessViewModel.AccessScreenState.Error
+import com.android.healthconnect.controller.data.access.AccessViewModel.AccessScreenState.WithData
 import com.android.healthconnect.controller.permissions.data.HealthPermissionType
 import com.android.healthconnect.controller.shared.app.AppMetadata
 import com.android.healthconnect.controller.shared.usecase.UseCaseResults
@@ -30,24 +30,26 @@
 import javax.inject.Inject
 import kotlinx.coroutines.launch
 
-/** View model for [HealthDataAccessFragment]. */
+/**
+ * View model for the Access tab in [EntriesAccessFragment] and
+ * [com.android.healthconnect.controller.dataaccess.HealthDataAccessFragment].
+ */
 @HiltViewModel
-class HealthDataAccessViewModel
-@Inject
-constructor(private val loadDataAccessUseCase: LoadDataAccessUseCase) : ViewModel() {
+class AccessViewModel @Inject constructor(private val loadAccessUseCase: LoadAccessUseCase) :
+    ViewModel() {
 
-    private val _appMetadataMap = MutableLiveData<DataAccessScreenState>()
+    private val _appMetadataMap = MutableLiveData<AccessScreenState>()
 
-    val appMetadataMap: LiveData<DataAccessScreenState>
+    val appMetadataMap: LiveData<AccessScreenState>
         get() = _appMetadataMap
 
     fun loadAppMetaDataMap(permissionType: HealthPermissionType) {
         val appsMap = _appMetadataMap.value
         if (appsMap is WithData && appsMap.appMetadata.isEmpty()) {
-            _appMetadataMap.postValue(DataAccessScreenState.Loading)
+            _appMetadataMap.postValue(AccessScreenState.Loading)
         }
         viewModelScope.launch {
-            when (val result = loadDataAccessUseCase.invoke(permissionType)) {
+            when (val result = loadAccessUseCase.invoke(permissionType)) {
                 is UseCaseResults.Success -> {
                     _appMetadataMap.postValueIfUpdated(WithData(result.data))
                 }
@@ -59,12 +61,12 @@
     }
 
     /** Represents DataAccessFragment state. */
-    sealed class DataAccessScreenState {
-        object Loading : DataAccessScreenState()
+    sealed class AccessScreenState {
+        object Loading : AccessScreenState()
 
-        object Error : DataAccessScreenState()
+        object Error : AccessScreenState()
 
-        data class WithData(val appMetadata: Map<DataAccessAppState, List<AppMetadata>>) :
-            DataAccessScreenState()
+        data class WithData(val appMetadata: Map<AppAccessState, List<AppMetadata>>) :
+            AccessScreenState()
     }
 }
diff --git a/apk/src/com/android/healthconnect/controller/dataaccess/DataAccessAppState.kt b/apk/src/com/android/healthconnect/controller/data/access/AppAccessState.kt
similarity index 75%
rename from apk/src/com/android/healthconnect/controller/dataaccess/DataAccessAppState.kt
rename to apk/src/com/android/healthconnect/controller/data/access/AppAccessState.kt
index bde8af1..18b79e5 100644
--- a/apk/src/com/android/healthconnect/controller/dataaccess/DataAccessAppState.kt
+++ b/apk/src/com/android/healthconnect/controller/data/access/AppAccessState.kt
@@ -14,11 +14,13 @@
  * limitations under the License.
  */
 
-package com.android.healthconnect.controller.dataaccess
+package com.android.healthconnect.controller.data.access
 
 /** Represents app data access state. */
-sealed class DataAccessAppState {
-    object Read : DataAccessAppState()
-    object Write : DataAccessAppState()
-    object Inactive : DataAccessAppState()
+sealed class AppAccessState {
+    object Read : AppAccessState()
+
+    object Write : AppAccessState()
+
+    object Inactive : AppAccessState()
 }
diff --git a/apk/src/com/android/healthconnect/controller/dataaccess/LoadDataAccessUseCase.kt b/apk/src/com/android/healthconnect/controller/data/access/LoadAccessUseCase.kt
similarity index 89%
rename from apk/src/com/android/healthconnect/controller/dataaccess/LoadDataAccessUseCase.kt
rename to apk/src/com/android/healthconnect/controller/data/access/LoadAccessUseCase.kt
index 476196c..f83ba9c 100644
--- a/apk/src/com/android/healthconnect/controller/dataaccess/LoadDataAccessUseCase.kt
+++ b/apk/src/com/android/healthconnect/controller/data/access/LoadAccessUseCase.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -13,8 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-package com.android.healthconnect.controller.dataaccess
+package com.android.healthconnect.controller.data.access
 
 import com.android.healthconnect.controller.permissions.api.GetGrantedHealthPermissionsUseCase
 import com.android.healthconnect.controller.permissions.data.HealthPermission
@@ -31,7 +30,7 @@
 import kotlinx.coroutines.withContext
 
 @Singleton
-class LoadDataAccessUseCase
+class LoadAccessUseCase
 @Inject
 constructor(
     private val loadPermissionTypeContributorAppsUseCase: LoadPermissionTypeContributorAppsUseCase,
@@ -40,10 +39,10 @@
     private val appInfoReader: AppInfoReader,
     @IoDispatcher private val dispatcher: CoroutineDispatcher
 ) {
-    /** Returns a map of [DataAccessAppState] to apps. */
+    /** Returns a map of [AppAccessState] to apps. */
     suspend operator fun invoke(
         permissionType: HealthPermissionType
-    ): UseCaseResults<Map<DataAccessAppState, List<AppMetadata>>> =
+    ): UseCaseResults<Map<AppAccessState, List<AppMetadata>>> =
         withContext(dispatcher) {
             try {
                 val appsWithHealthPermissions: List<String> =
@@ -81,11 +80,10 @@
 
                 val appAccess =
                     mapOf(
-                        DataAccessAppState.Read to
-                            alphabeticallySortedMetadataList(readAppMetadataSet),
-                        DataAccessAppState.Write to
+                        AppAccessState.Read to alphabeticallySortedMetadataList(readAppMetadataSet),
+                        AppAccessState.Write to
                             alphabeticallySortedMetadataList(writeAppMetadataSet),
-                        DataAccessAppState.Inactive to
+                        AppAccessState.Inactive to
                             alphabeticallySortedMetadataList(inactiveAppMetadataSet))
                 UseCaseResults.Success(appAccess)
             } catch (ex: Exception) {
diff --git a/apk/src/com/android/healthconnect/controller/dataaccess/LoadPermissionTypeContributorAppsUseCase.kt b/apk/src/com/android/healthconnect/controller/data/access/LoadPermissionTypeContributorAppsUseCase.kt
similarity index 73%
rename from apk/src/com/android/healthconnect/controller/dataaccess/LoadPermissionTypeContributorAppsUseCase.kt
rename to apk/src/com/android/healthconnect/controller/data/access/LoadPermissionTypeContributorAppsUseCase.kt
index a2c1795..3613481 100644
--- a/apk/src/com/android/healthconnect/controller/dataaccess/LoadPermissionTypeContributorAppsUseCase.kt
+++ b/apk/src/com/android/healthconnect/controller/data/access/LoadPermissionTypeContributorAppsUseCase.kt
@@ -1,4 +1,19 @@
-package com.android.healthconnect.controller.dataaccess
+/*
+ * 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.data.access
 
 import android.health.connect.HealthConnectManager
 import android.health.connect.RecordTypeInfoResponse
@@ -24,9 +39,7 @@
     @IoDispatcher private val dispatcher: CoroutineDispatcher
 ) {
 
-    /**
-     * Returns a list of [AppMetadata]s that have data in this [HealthPermissionType].
-     */
+    /** Returns a list of [AppMetadata]s that have data in this [HealthPermissionType]. */
     suspend operator fun invoke(permissionType: HealthPermissionType): List<AppMetadata> =
         withContext(dispatcher) {
             try {
diff --git a/apk/src/com/android/healthconnect/controller/data/alldata/AllDataFragment.kt b/apk/src/com/android/healthconnect/controller/data/alldata/AllDataFragment.kt
new file mode 100644
index 0000000..80ff2e9
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/data/alldata/AllDataFragment.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.data.alldata
+
+import android.os.Bundle
+import android.view.View
+import androidx.core.os.bundleOf
+import androidx.fragment.app.viewModels
+import androidx.navigation.fragment.findNavController
+import androidx.preference.PreferenceCategory
+import com.android.healthconnect.controller.R
+import com.android.healthconnect.controller.data.appdata.AppDataFragment.Companion.PERMISSION_TYPE_KEY
+import com.android.healthconnect.controller.data.appdata.PermissionTypesPerCategory
+import com.android.healthconnect.controller.permissions.data.HealthPermissionStrings
+import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.icon
+import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.uppercaseTitle
+import com.android.healthconnect.controller.shared.preference.HealthPreference
+import com.android.healthconnect.controller.shared.preference.HealthPreferenceFragment
+import com.android.healthconnect.controller.shared.preference.NoDataPreference
+import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
+import com.android.settingslib.widget.FooterPreference
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
+
+/** Fragment for health permission types. */
+@AndroidEntryPoint(HealthPreferenceFragment::class)
+open class AllDataFragment : Hilt_AllDataFragment() {
+
+    companion object {
+        private const val TAG = "AllDataFragmentTag"
+    }
+
+    @Inject lateinit var logger: HealthConnectLogger
+
+    private val viewModel: AllDataViewModel by viewModels()
+
+    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+        super.onCreatePreferences(savedInstanceState, rootKey)
+        setPreferencesFromResource(R.xml.app_data_screen, rootKey)
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        viewModel.loadAllData()
+
+        viewModel.allData.observe(viewLifecycleOwner) { state ->
+            when (state) {
+                is AllDataViewModel.AllDataState.Loading -> {
+                    setLoading(isLoading = true)
+                }
+                is AllDataViewModel.AllDataState.Error -> {
+                    setError(hasError = true)
+                }
+                is AllDataViewModel.AllDataState.WithData -> {
+                    setLoading(isLoading = false)
+                    setError(hasError = false)
+                    updatePreferenceScreen(state.dataMap)
+                }
+            }
+        }
+    }
+
+    private fun updatePreferenceScreen(
+        permissionTypesPerCategoryList: List<PermissionTypesPerCategory>
+    ) {
+        preferenceScreen?.removeAll()
+
+        val populatedCategories =
+            permissionTypesPerCategoryList
+                .filter { it.data.isNotEmpty() }
+                .sortedBy { getString(it.category.uppercaseTitle()) }
+
+        if (populatedCategories.isEmpty()) {
+            preferenceScreen.addPreference(NoDataPreference(requireContext()))
+            preferenceScreen.addPreference(
+                FooterPreference(requireContext()).also { it.setTitle(R.string.no_data_footer) })
+            return
+        }
+
+        populatedCategories.forEach { permissionTypesPerCategory ->
+            val category = permissionTypesPerCategory.category
+            val categoryIcon = category.icon(requireContext())
+
+            val preferenceCategory =
+                PreferenceCategory(requireContext()).also { it.setTitle(category.uppercaseTitle()) }
+            preferenceScreen.addPreference(preferenceCategory)
+
+            permissionTypesPerCategory.data
+                .sortedBy {
+                    getString(HealthPermissionStrings.fromPermissionType(it).uppercaseLabel)
+                }
+                .forEach { permissionType ->
+                    preferenceCategory.addPreference(
+                        HealthPreference(requireContext()).also {
+                            it.icon = categoryIcon
+                            it.setTitle(
+                                HealthPermissionStrings.fromPermissionType(permissionType)
+                                    .uppercaseLabel)
+                            // TODO(b/291249677): Add in upcoming CL.
+                            // it.logName = AllDataElement.PERMISSION_TYPE_BUTTON
+                            it.setOnPreferenceClickListener {
+                                findNavController()
+                                    .navigate(
+                                        R.id.action_allData_to_entriesAndAccess,
+                                        bundleOf(PERMISSION_TYPE_KEY to permissionType))
+                                true
+                            }
+                        })
+                }
+        }
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/data/alldata/AllDataViewModel.kt b/apk/src/com/android/healthconnect/controller/data/alldata/AllDataViewModel.kt
new file mode 100644
index 0000000..6bd5bd0
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/data/alldata/AllDataViewModel.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.data.alldata
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.android.healthconnect.controller.data.appdata.AppDataUseCase
+import com.android.healthconnect.controller.data.appdata.PermissionTypesPerCategory
+import com.android.healthconnect.controller.shared.usecase.UseCaseResults
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.launch
+
+/** View model for the [AllDataFragment] . */
+@HiltViewModel
+class AllDataViewModel @Inject constructor(private val loadAppDataUseCase: AppDataUseCase) :
+    ViewModel() {
+
+    companion object {
+        private const val TAG = "AllDataViewModel"
+    }
+
+    private val _allData = MutableLiveData<AllDataState>()
+
+    /** Provides a list of [PermissionTypesPerCategory]s to be displayed in [AllDataFragment]. */
+    val allData: LiveData<AllDataState>
+        get() = _allData
+
+    fun loadAllData() {
+        _allData.postValue(AllDataState.Loading)
+
+        viewModelScope.launch {
+            when (val result = loadAppDataUseCase.loadAllData()) {
+                is UseCaseResults.Success -> {
+                    _allData.postValue(AllDataState.WithData(result.data))
+                }
+                is UseCaseResults.Failed -> {
+                    _allData.postValue(AllDataState.Error)
+                }
+            }
+        }
+    }
+
+    sealed class AllDataState {
+        object Loading : AllDataState()
+
+        object Error : AllDataState()
+
+        data class WithData(val dataMap: List<PermissionTypesPerCategory>) : AllDataState()
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/data/appdata/AppDataFragment.kt b/apk/src/com/android/healthconnect/controller/data/appdata/AppDataFragment.kt
new file mode 100644
index 0000000..f2fd4ab
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/data/appdata/AppDataFragment.kt
@@ -0,0 +1,156 @@
+/*
+ * 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.data.appdata
+
+import android.content.Intent.EXTRA_PACKAGE_NAME
+import android.os.Bundle
+import android.view.View
+import androidx.core.os.bundleOf
+import androidx.fragment.app.viewModels
+import androidx.navigation.fragment.findNavController
+import androidx.preference.PreferenceCategory
+import com.android.healthconnect.controller.R
+import com.android.healthconnect.controller.permissions.data.HealthPermissionStrings
+import com.android.healthconnect.controller.permissions.shared.Constants
+import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.icon
+import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.uppercaseTitle
+import com.android.healthconnect.controller.shared.preference.HealthPreference
+import com.android.healthconnect.controller.shared.preference.HealthPreferenceFragment
+import com.android.healthconnect.controller.shared.preference.NoDataPreference
+import com.android.settingslib.widget.AppHeaderPreference
+import com.android.settingslib.widget.FooterPreference
+import dagger.hilt.android.AndroidEntryPoint
+
+/** Fragment to display data in Health Connect written by a given app. */
+@AndroidEntryPoint(HealthPreferenceFragment::class)
+open class AppDataFragment : Hilt_AppDataFragment() {
+
+    companion object {
+        private const val TAG = "AppDataFragmentTag"
+        const val PERMISSION_TYPE_KEY = "permission_type_key"
+    }
+
+    init {
+        // TODO(b/281811925):
+        // this.setPageName(PageName.APP_DATA_PAGE)
+    }
+
+    private var packageName: String = ""
+    private var appName: String = ""
+
+    private val viewModel: AppDataViewModel by viewModels()
+
+    private lateinit var header: AppHeaderPreference
+
+    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+        super.onCreatePreferences(savedInstanceState, rootKey)
+        setPreferencesFromResource(R.xml.app_data_screen, rootKey)
+
+        if (requireArguments().containsKey(EXTRA_PACKAGE_NAME) &&
+            requireArguments().getString(EXTRA_PACKAGE_NAME) != null) {
+            packageName = requireArguments().getString(EXTRA_PACKAGE_NAME)!!
+        }
+        if (requireArguments().containsKey(Constants.EXTRA_APP_NAME) &&
+            requireArguments().getString(Constants.EXTRA_APP_NAME) != null) {
+            appName = requireArguments().getString(Constants.EXTRA_APP_NAME)!!
+        }
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        viewModel.loadAppInfo(packageName)
+        viewModel.loadAppData(packageName)
+
+        header = AppHeaderPreference(requireContext())
+        viewModel.appInfo.observe(viewLifecycleOwner) { appMetadata ->
+            header.apply {
+                icon = appMetadata.icon
+                title = appMetadata.appName
+            }
+        }
+
+        viewModel.appData.observe(viewLifecycleOwner) { state ->
+            when (state) {
+                is AppDataViewModel.AppDataState.Loading -> {
+                    setLoading(isLoading = true)
+                }
+                is AppDataViewModel.AppDataState.Error -> {
+                    setError(hasError = true)
+                }
+                is AppDataViewModel.AppDataState.WithData -> {
+                    setLoading(isLoading = false)
+                    setError(hasError = false)
+                    updatePreferenceScreen(state.dataMap)
+                }
+            }
+        }
+    }
+
+    private fun updatePreferenceScreen(
+        permissionTypesPerCategoryList: List<PermissionTypesPerCategory>
+    ) {
+        preferenceScreen?.removeAll()
+        preferenceScreen.addPreference(header)
+
+        val populatedCategories =
+            permissionTypesPerCategoryList
+                .filter { it.data.isNotEmpty() }
+                .sortedBy { getString(it.category.uppercaseTitle()) }
+
+        if (populatedCategories.isEmpty()) {
+            preferenceScreen.addPreference(NoDataPreference(requireContext()))
+            preferenceScreen.addPreference(
+                FooterPreference(requireContext()).also { it.setTitle(R.string.no_data_footer) })
+            return
+        }
+
+        populatedCategories.forEach { permissionTypesPerCategory ->
+            val category = permissionTypesPerCategory.category
+            val categoryIcon = category.icon(requireContext())
+
+            val preferenceCategory =
+                PreferenceCategory(requireContext()).also { it.setTitle(category.uppercaseTitle()) }
+            preferenceScreen.addPreference(preferenceCategory)
+
+            permissionTypesPerCategory.data
+                .sortedBy {
+                    getString(HealthPermissionStrings.fromPermissionType(it).uppercaseLabel)
+                }
+                .forEach { permissionType ->
+                    preferenceCategory.addPreference(
+                        HealthPreference(requireContext()).also {
+                            it.icon = categoryIcon
+                            it.setTitle(
+                                HealthPermissionStrings.fromPermissionType(permissionType)
+                                    .uppercaseLabel)
+                            it.setOnPreferenceClickListener {
+                                // TODO(b/281811925): Add in upcoming cl.
+                                // it.logName = AppDataElement.PERMISSION_TYPE_BUTTON
+                                findNavController()
+                                    .navigate(
+                                        R.id.action_appData_to_appEntries,
+                                        bundleOf(
+                                            EXTRA_PACKAGE_NAME to packageName,
+                                            Constants.EXTRA_APP_NAME to appName,
+                                            PERMISSION_TYPE_KEY to permissionType))
+                                true
+                            }
+                        })
+                }
+        }
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/data/appdata/AppDataUseCase.kt b/apk/src/com/android/healthconnect/controller/data/appdata/AppDataUseCase.kt
new file mode 100644
index 0000000..7068a87
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/data/appdata/AppDataUseCase.kt
@@ -0,0 +1,139 @@
+/*
+ * 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.data.appdata
+
+import android.health.connect.HealthConnectManager
+import android.health.connect.RecordTypeInfoResponse
+import android.health.connect.datatypes.Record
+import android.util.Log
+import androidx.core.os.asOutcomeReceiver
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType
+import com.android.healthconnect.controller.permissions.data.fromHealthPermissionCategory
+import com.android.healthconnect.controller.service.IoDispatcher
+import com.android.healthconnect.controller.shared.HEALTH_DATA_CATEGORIES
+import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.healthPermissionTypes
+import com.android.healthconnect.controller.shared.HealthDataCategoryInt
+import com.android.healthconnect.controller.shared.usecase.UseCaseResults
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+
+@Singleton
+class AppDataUseCase
+@Inject
+constructor(
+    private val healthConnectManager: HealthConnectManager,
+    @IoDispatcher private val dispatcher: CoroutineDispatcher
+) {
+
+    /** Returns list of all health categories and permission types to be shown on the HC UI. */
+    suspend fun loadAllData(): UseCaseResults<List<PermissionTypesPerCategory>> =
+        withContext(dispatcher) {
+            try {
+                val recordTypeInfoMap: Map<Class<out Record>, RecordTypeInfoResponse> =
+                    suspendCancellableCoroutine { continuation ->
+                        healthConnectManager.queryAllRecordTypesInfo(
+                            Runnable::run, continuation.asOutcomeReceiver())
+                    }
+                val categories =
+                    HEALTH_DATA_CATEGORIES.map {
+                        PermissionTypesPerCategory(
+                            it,
+                            getPermissionTypesPerCategory(
+                                it, recordTypeInfoMap, packageName = null))
+                    }
+                UseCaseResults.Success(categories)
+            } catch (e: Exception) {
+                Log.e("TAG_ERROR", "Loading error ", e)
+                UseCaseResults.Failed(e)
+            }
+        }
+
+    /**
+     * Returns list of health categories and permission types written by the given app to be shown
+     * on the HC UI.
+     */
+    suspend fun loadAppData(packageName: String): UseCaseResults<List<PermissionTypesPerCategory>> =
+        withContext(dispatcher) {
+            try {
+                val recordTypeInfoMap: Map<Class<out Record>, RecordTypeInfoResponse> =
+                    suspendCancellableCoroutine { continuation ->
+                        healthConnectManager.queryAllRecordTypesInfo(
+                            Runnable::run, continuation.asOutcomeReceiver())
+                    }
+                val categories =
+                    HEALTH_DATA_CATEGORIES.map {
+                        PermissionTypesPerCategory(
+                            it, getPermissionTypesPerCategory(it, recordTypeInfoMap, packageName))
+                    }
+                UseCaseResults.Success(categories)
+            } catch (e: Exception) {
+                UseCaseResults.Failed(e)
+            }
+        }
+
+    /**
+     * Returns those [HealthPermissionType]s that have some data written by the given [packageName]
+     * app. If the is no app provided then return all data.
+     */
+    private fun getPermissionTypesPerCategory(
+        category: @HealthDataCategoryInt Int,
+        recordTypeInfoMap: Map<Class<out Record>, RecordTypeInfoResponse>,
+        packageName: String?
+    ): List<HealthPermissionType> {
+        if (packageName == null) {
+            return category.healthPermissionTypes().filter { hasData(it, recordTypeInfoMap) }
+        }
+        return category.healthPermissionTypes().filter {
+            hasDataByApp(it, recordTypeInfoMap, packageName)
+        }
+    }
+
+    private fun hasData(
+        permissionType: HealthPermissionType,
+        recordTypeInfoMap: Map<Class<out Record>, RecordTypeInfoResponse>,
+    ): Boolean =
+        recordTypeInfoMap.values.firstOrNull {
+            fromHealthPermissionCategory(it.permissionCategory) == permissionType &&
+                it.contributingPackages.isNotEmpty()
+        } != null
+
+    private fun hasDataByApp(
+        permissionType: HealthPermissionType,
+        recordTypeInfoMap: Map<Class<out Record>, RecordTypeInfoResponse>,
+        packageName: String
+    ): Boolean =
+        recordTypeInfoMap.values.firstOrNull {
+            fromHealthPermissionCategory(it.permissionCategory) == permissionType &&
+                it.contributingPackages.isNotEmpty() &&
+                it.contributingPackages
+                    .map { contributingApp -> contributingApp.packageName }
+                    .contains(packageName)
+        } != null
+}
+
+/**
+ * Represents Health Category group to be shown in health connect screens.
+ *
+ * @param category Category id
+ * @param data [HealthPermissionType]s within the category that have data written by given app.
+ */
+data class PermissionTypesPerCategory(
+    val category: @HealthDataCategoryInt Int,
+    val data: List<HealthPermissionType>
+)
diff --git a/apk/src/com/android/healthconnect/controller/data/appdata/AppDataViewModel.kt b/apk/src/com/android/healthconnect/controller/data/appdata/AppDataViewModel.kt
new file mode 100644
index 0000000..acf0648
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/data/appdata/AppDataViewModel.kt
@@ -0,0 +1,80 @@
+/*
+ * 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.data.appdata
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.android.healthconnect.controller.shared.app.AppInfoReader
+import com.android.healthconnect.controller.shared.app.AppMetadata
+import com.android.healthconnect.controller.shared.usecase.UseCaseResults
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.launch
+
+/** View model for the [AppDataFragment] . */
+@HiltViewModel
+class AppDataViewModel
+@Inject
+constructor(
+    private val appInfoReader: AppInfoReader,
+    private val loadAppDataUseCase: AppDataUseCase
+) : ViewModel() {
+
+    companion object {
+        private const val TAG = "AppDataViewModel"
+    }
+
+    private val _appData = MutableLiveData<AppDataState>()
+    private val _appInfo = MutableLiveData<AppMetadata>()
+
+    /** Provides a list of [PermissionTypesPerCategory]s to be displayed in [AppDataFragment]. */
+    val appData: LiveData<AppDataState>
+        get() = _appData
+
+    val appInfo: LiveData<AppMetadata>
+        get() = _appInfo
+
+    fun loadAppData(packageName: String) {
+        _appData.postValue(AppDataState.Loading)
+
+        viewModelScope.launch {
+            when (val result = loadAppDataUseCase.loadAppData(packageName)) {
+                is UseCaseResults.Success -> {
+                    _appData.postValue(AppDataState.WithData(result.data))
+                }
+                is UseCaseResults.Failed -> {
+                    _appData.postValue(AppDataState.Error)
+                }
+            }
+        }
+    }
+
+    fun loadAppInfo(packageName: String) {
+        viewModelScope.launch { _appInfo.postValue(appInfoReader.getAppMetadata(packageName)) }
+    }
+
+    sealed class AppDataState {
+        object Loading : AppDataState()
+
+        object Error : AppDataState()
+
+        data class WithData(val dataMap: List<PermissionTypesPerCategory>) : AppDataState()
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/data/entries/AggregationViewBinder.kt b/apk/src/com/android/healthconnect/controller/data/entries/AggregationViewBinder.kt
new file mode 100644
index 0000000..9c85fc2
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/data/entries/AggregationViewBinder.kt
@@ -0,0 +1,51 @@
+/**
+ * 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.data.entries
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import com.android.healthconnect.controller.R
+import com.android.healthconnect.controller.data.entries.FormattedEntry.FormattedAggregation
+import com.android.healthconnect.controller.shared.recyclerview.ViewBinder
+import com.android.healthconnect.controller.utils.logging.DataEntriesElement
+import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
+import com.android.healthconnect.controller.utils.logging.HealthConnectLoggerEntryPoint
+import dagger.hilt.android.EntryPointAccessors
+
+class AggregationViewBinder : ViewBinder<FormattedAggregation, View> {
+
+    private lateinit var logger: HealthConnectLogger
+
+    override fun newView(parent: ViewGroup): View {
+        val context = parent.context.applicationContext
+        val hiltEntryPoint =
+            EntryPointAccessors.fromApplication(
+                context.applicationContext, HealthConnectLoggerEntryPoint::class.java)
+        logger = hiltEntryPoint.logger()
+        return LayoutInflater.from(parent.context)
+            .inflate(R.layout.item_data_aggregation, parent, false)
+    }
+
+    override fun bind(view: View, data: FormattedAggregation, index: Int) {
+        val apps = view.findViewById<TextView>(R.id.item_data_origin_apps)
+        val aggregation = view.findViewById<TextView>(R.id.item_data_aggregation)
+        logger.logImpression(DataEntriesElement.AGGREGATION_DATA_VIEW)
+
+        aggregation.text = data.aggregation
+        aggregation.contentDescription = data.aggregationA11y
+        apps.text = data.contributingApps
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/data/entries/AllEntriesFragment.kt b/apk/src/com/android/healthconnect/controller/data/entries/AllEntriesFragment.kt
new file mode 100644
index 0000000..d321274
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/data/entries/AllEntriesFragment.kt
@@ -0,0 +1,213 @@
+/*
+ * 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.data.entries
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.core.view.isVisible
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.navigation.fragment.findNavController
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.VERTICAL
+import com.android.healthconnect.controller.R
+import com.android.healthconnect.controller.data.entries.EntriesViewModel.EntriesFragmentState.Empty
+import com.android.healthconnect.controller.data.entries.EntriesViewModel.EntriesFragmentState.Loading
+import com.android.healthconnect.controller.data.entries.EntriesViewModel.EntriesFragmentState.LoadingFailed
+import com.android.healthconnect.controller.data.entries.EntriesViewModel.EntriesFragmentState.With
+import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod
+import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationView
+import com.android.healthconnect.controller.entrydetails.DataEntryDetailsFragment
+import com.android.healthconnect.controller.permissions.data.HealthPermissionStrings.Companion.fromPermissionType
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType
+import com.android.healthconnect.controller.permissiontypes.HealthPermissionTypesFragment.Companion.PERMISSION_TYPE_KEY
+import com.android.healthconnect.controller.shared.recyclerview.RecyclerViewAdapter
+import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
+import com.android.healthconnect.controller.utils.logging.ToolbarElement
+import com.android.healthconnect.controller.utils.setTitle
+import com.android.healthconnect.controller.utils.setupMenu
+import com.android.settingslib.widget.AppHeaderPreference
+import dagger.hilt.android.AndroidEntryPoint
+import java.time.Instant
+import javax.inject.Inject
+
+/** Fragment to show health data entries. */
+@AndroidEntryPoint(Fragment::class)
+class AllEntriesFragment : Hilt_AllEntriesFragment() {
+
+    @Inject lateinit var logger: HealthConnectLogger
+    // TODO(b/291249677): Add logging.
+
+    private lateinit var permissionType: HealthPermissionType
+    private val entriesViewModel: EntriesViewModel by viewModels()
+
+    private lateinit var header: AppHeaderPreference
+    private lateinit var dateNavigationView: DateNavigationView
+    private lateinit var entriesRecyclerView: RecyclerView
+    private lateinit var noDataView: TextView
+    private lateinit var loadingView: View
+    private lateinit var errorView: View
+    private lateinit var adapter: RecyclerViewAdapter
+
+    private val onClickEntryListener by lazy {
+        object : OnClickEntryListener {
+            override fun onItemClicked(id: String, index: Int) {
+                findNavController()
+                    .navigate(
+                        R.id.action_entriesAndAccessFragment_to_dataEntryDetailsFragment,
+                        DataEntryDetailsFragment.createBundle(
+                            permissionType, id, showDataOrigin = true))
+            }
+        }
+    }
+    private val aggregationViewBinder by lazy {
+        com.android.healthconnect.controller.data.entries.AggregationViewBinder()
+    }
+    private val entryViewBinder by lazy { EntryItemViewBinder() }
+    private val sectionTitleViewBinder by lazy { SectionTitleViewBinder() }
+    private val sleepSessionViewBinder by lazy {
+        SleepSessionItemViewBinder(onItemClickedListener = onClickEntryListener)
+    }
+    private val exerciseSessionItemViewBinder by lazy {
+        ExerciseSessionItemViewBinder(onItemClickedListener = onClickEntryListener)
+    }
+    private val seriesDataItemViewBinder by lazy {
+        SeriesDataItemViewBinder(onItemClickedListener = onClickEntryListener)
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+
+        val view = inflater.inflate(R.layout.fragment_entries, container, false)
+        if (requireArguments().containsKey(PERMISSION_TYPE_KEY)) {
+            permissionType =
+                arguments?.getSerializable(PERMISSION_TYPE_KEY, HealthPermissionType::class.java)
+                    ?: throw IllegalArgumentException("PERMISSION_TYPE_KEY can't be null!")
+        }
+        setTitle(fromPermissionType(permissionType).uppercaseLabel)
+        setupMenu(R.menu.set_data_units_with_send_feedback_and_help, viewLifecycleOwner, logger) {
+            menuItem ->
+            when (menuItem.itemId) {
+                R.id.menu_open_units -> {
+                    logger.logImpression(ToolbarElement.TOOLBAR_UNITS_BUTTON)
+                    findNavController()
+                        .navigate(R.id.action_entriesAndAccessFragment_to_unitFragment)
+                    true
+                }
+                else -> false
+            }
+        }
+        logger.logImpression(ToolbarElement.TOOLBAR_SETTINGS_BUTTON)
+
+        dateNavigationView = view.findViewById(R.id.date_navigation_view)
+        noDataView = view.findViewById(R.id.no_data_view)
+        errorView = view.findViewById(R.id.error_view)
+        loadingView = view.findViewById(R.id.loading)
+        adapter =
+            RecyclerViewAdapter.Builder()
+                .setViewBinder(FormattedEntry.FormattedDataEntry::class.java, entryViewBinder)
+                .setViewBinder(FormattedEntry.SleepSessionEntry::class.java, sleepSessionViewBinder)
+                .setViewBinder(
+                    FormattedEntry.ExerciseSessionEntry::class.java, exerciseSessionItemViewBinder)
+                .setViewBinder(FormattedEntry.SeriesDataEntry::class.java, seriesDataItemViewBinder)
+                .setViewBinder(
+                    FormattedEntry.FormattedAggregation::class.java, aggregationViewBinder)
+                .setViewBinder(
+                    FormattedEntry.EntryDateSectionHeader::class.java, sectionTitleViewBinder)
+                .build()
+        entriesRecyclerView =
+            view.findViewById<RecyclerView?>(R.id.data_entries_list).also {
+                it.adapter = adapter
+                it.layoutManager = LinearLayoutManager(context, VERTICAL, false)
+            }
+        return view
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        dateNavigationView.setDateChangedListener(
+            object : DateNavigationView.OnDateChangedListener {
+                override fun onDateChanged(
+                    displayedStartDate: Instant,
+                    period: DateNavigationPeriod
+                ) {
+                    entriesViewModel.loadEntries(permissionType, displayedStartDate, period)
+                }
+            })
+
+        header = AppHeaderPreference(requireContext())
+        observeEntriesUpdates()
+    }
+
+    override fun onResume() {
+        super.onResume()
+        setTitle(fromPermissionType(permissionType).uppercaseLabel)
+        if (entriesViewModel.currentSelectedDate.value != null &&
+            entriesViewModel.period.value != null) {
+            val date = entriesViewModel.currentSelectedDate.value!!
+            val selectedPeriod = entriesViewModel.period.value!!
+            dateNavigationView.setDate(date)
+            dateNavigationView.setPeriod(selectedPeriod)
+            entriesViewModel.loadEntries(permissionType, date, selectedPeriod)
+        } else {
+            entriesViewModel.loadEntries(
+                permissionType, dateNavigationView.getDate(), dateNavigationView.getPeriod())
+        }
+        //
+        //        logger.setPageId(pageName)
+        //        logger.logPageImpression()
+    }
+
+    private fun observeEntriesUpdates() {
+        entriesViewModel.entries.observe(viewLifecycleOwner) { state ->
+            when (state) {
+                is Loading -> {
+                    loadingView.isVisible = true
+                    noDataView.isVisible = false
+                    errorView.isVisible = false
+                    entriesRecyclerView.isVisible = false
+                }
+                is Empty -> {
+                    noDataView.isVisible = true
+                    loadingView.isVisible = false
+                    errorView.isVisible = false
+                    entriesRecyclerView.isVisible = false
+                }
+                is With -> {
+                    entriesRecyclerView.isVisible = true
+                    adapter.updateData(state.entries)
+                    errorView.isVisible = false
+                    noDataView.isVisible = false
+                    loadingView.isVisible = false
+                }
+                is LoadingFailed -> {
+                    errorView.isVisible = true
+                    loadingView.isVisible = false
+                    noDataView.isVisible = false
+                    entriesRecyclerView.isVisible = false
+                }
+            }
+        }
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/data/entries/AppEntriesFragment.kt b/apk/src/com/android/healthconnect/controller/data/entries/AppEntriesFragment.kt
new file mode 100644
index 0000000..99f7d26
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/data/entries/AppEntriesFragment.kt
@@ -0,0 +1,243 @@
+/**
+ * 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.data.entries
+
+import android.content.Intent.EXTRA_PACKAGE_NAME
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.core.view.isVisible
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.commitNow
+import androidx.fragment.app.viewModels
+import androidx.navigation.fragment.findNavController
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.VERTICAL
+import com.android.healthconnect.controller.R
+import com.android.healthconnect.controller.data.appdata.AppDataFragment.Companion.PERMISSION_TYPE_KEY
+import com.android.healthconnect.controller.data.entries.EntriesViewModel.EntriesFragmentState.Empty
+import com.android.healthconnect.controller.data.entries.EntriesViewModel.EntriesFragmentState.Loading
+import com.android.healthconnect.controller.data.entries.EntriesViewModel.EntriesFragmentState.LoadingFailed
+import com.android.healthconnect.controller.data.entries.EntriesViewModel.EntriesFragmentState.With
+import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod
+import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationView
+import com.android.healthconnect.controller.deletion.DeletionConstants.FRAGMENT_TAG_DELETION
+import com.android.healthconnect.controller.deletion.DeletionFragment
+import com.android.healthconnect.controller.entrydetails.DataEntryDetailsFragment
+import com.android.healthconnect.controller.permissions.data.HealthPermissionStrings.Companion.fromPermissionType
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType
+import com.android.healthconnect.controller.permissions.shared.Constants
+import com.android.healthconnect.controller.shared.recyclerview.RecyclerViewAdapter
+import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
+import com.android.healthconnect.controller.utils.logging.ToolbarElement
+import com.android.healthconnect.controller.utils.setTitle
+import com.android.healthconnect.controller.utils.setupMenu
+import com.android.settingslib.widget.AppHeaderPreference
+import dagger.hilt.android.AndroidEntryPoint
+import java.time.Instant
+import javax.inject.Inject
+
+/** Fragment to show health data entries by date. */
+@AndroidEntryPoint(Fragment::class)
+class AppEntriesFragment : Hilt_AppEntriesFragment() {
+
+    @Inject lateinit var logger: HealthConnectLogger
+
+    private var packageName: String = ""
+    private var appName: String = ""
+
+    private lateinit var permissionType: HealthPermissionType
+    private val entriesViewModel: EntriesViewModel by viewModels()
+
+    private lateinit var header: AppHeaderPreference
+    private lateinit var dateNavigationView: DateNavigationView
+    private lateinit var entriesRecyclerView: RecyclerView
+    private lateinit var noDataView: TextView
+    private lateinit var loadingView: View
+    private lateinit var errorView: View
+    private lateinit var adapter: RecyclerViewAdapter
+
+    private val onClickEntryListener by lazy {
+        object : OnClickEntryListener {
+            override fun onItemClicked(id: String, index: Int) {
+                findNavController()
+                    .navigate(
+                        R.id.action_appEntriesFragment_to_dataEntryDetailsFragment,
+                        DataEntryDetailsFragment.createBundle(
+                            permissionType, id, showDataOrigin = false))
+            }
+        }
+    }
+    private val aggregationViewBinder by lazy { AggregationViewBinder() }
+    private val entryViewBinder by lazy { EntryItemViewBinder() }
+    private val sectionTitleViewBinder by lazy { SectionTitleViewBinder() }
+    private val sleepSessionViewBinder by lazy {
+        SleepSessionItemViewBinder(onItemClickedListener = onClickEntryListener)
+    }
+    private val exerciseSessionItemViewBinder by lazy {
+        ExerciseSessionItemViewBinder(onItemClickedListener = onClickEntryListener)
+    }
+    private val seriesDataItemViewBinder by lazy {
+        SeriesDataItemViewBinder(onItemClickedListener = onClickEntryListener)
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        // TODO(b/291249677): Log pagename.
+
+        if (requireArguments().containsKey(EXTRA_PACKAGE_NAME) &&
+            requireArguments().getString(EXTRA_PACKAGE_NAME) != null) {
+            packageName = requireArguments().getString(EXTRA_PACKAGE_NAME)!!
+        }
+        if (requireArguments().containsKey(Constants.EXTRA_APP_NAME) &&
+            requireArguments().getString(Constants.EXTRA_APP_NAME) != null) {
+            appName = requireArguments().getString(Constants.EXTRA_APP_NAME)!!
+        }
+
+        val view = inflater.inflate(R.layout.fragment_entries, container, false)
+        if (requireArguments().containsKey(PERMISSION_TYPE_KEY)) {
+            permissionType =
+                arguments?.getSerializable(PERMISSION_TYPE_KEY, HealthPermissionType::class.java)
+                    ?: throw IllegalArgumentException("PERMISSION_TYPE_KEY can't be null!")
+        }
+        setTitle(fromPermissionType(permissionType).uppercaseLabel)
+        setupMenu(R.menu.set_data_units_with_send_feedback_and_help, viewLifecycleOwner, logger) {
+            menuItem ->
+            when (menuItem.itemId) {
+                R.id.menu_open_units -> {
+                    logger.logImpression(ToolbarElement.TOOLBAR_UNITS_BUTTON)
+                    findNavController().navigate(R.id.action_dataEntriesFragment_to_unitsFragment)
+                    true
+                }
+                else -> false
+            }
+        }
+
+        dateNavigationView = view.findViewById(R.id.date_navigation_view)
+        noDataView = view.findViewById(R.id.no_data_view)
+        errorView = view.findViewById(R.id.error_view)
+        loadingView = view.findViewById(R.id.loading)
+        adapter =
+            RecyclerViewAdapter.Builder()
+                .setViewBinder(FormattedEntry.FormattedDataEntry::class.java, entryViewBinder)
+                .setViewBinder(FormattedEntry.SleepSessionEntry::class.java, sleepSessionViewBinder)
+                .setViewBinder(
+                    FormattedEntry.ExerciseSessionEntry::class.java, exerciseSessionItemViewBinder)
+                .setViewBinder(FormattedEntry.SeriesDataEntry::class.java, seriesDataItemViewBinder)
+                .setViewBinder(
+                    FormattedEntry.FormattedAggregation::class.java, aggregationViewBinder)
+                .setViewBinder(
+                    FormattedEntry.EntryDateSectionHeader::class.java, sectionTitleViewBinder)
+                .build()
+        entriesRecyclerView =
+            view.findViewById<RecyclerView?>(R.id.data_entries_list).also {
+                it.adapter = adapter
+                it.layoutManager = LinearLayoutManager(context, VERTICAL, false)
+            }
+
+        if (childFragmentManager.findFragmentByTag(FRAGMENT_TAG_DELETION) == null) {
+            childFragmentManager.commitNow { add(DeletionFragment(), FRAGMENT_TAG_DELETION) }
+        }
+
+        return view
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        dateNavigationView.setDateChangedListener(
+            object : DateNavigationView.OnDateChangedListener {
+                override fun onDateChanged(
+                    displayedStartDate: Instant,
+                    period: DateNavigationPeriod
+                ) {
+                    entriesViewModel.loadEntries(
+                        permissionType, packageName, displayedStartDate, period)
+                }
+            })
+
+        header = AppHeaderPreference(requireContext())
+        entriesViewModel.loadAppInfo(packageName)
+
+        entriesViewModel.appInfo.observe(viewLifecycleOwner) { appMetadata ->
+            header.apply {
+                icon = appMetadata.icon
+                title = appMetadata.appName
+            }
+        }
+
+        observeEntriesUpdates()
+    }
+
+    override fun onResume() {
+        super.onResume()
+        setTitle(fromPermissionType(permissionType).uppercaseLabel)
+        if (entriesViewModel.currentSelectedDate.value != null &&
+            entriesViewModel.period.value != null) {
+            val date = entriesViewModel.currentSelectedDate.value!!
+            val selectedPeriod = entriesViewModel.period.value!!
+            dateNavigationView.setDate(date)
+            dateNavigationView.setPeriod(selectedPeriod)
+            entriesViewModel.loadEntries(permissionType, packageName, date, selectedPeriod)
+        } else {
+            entriesViewModel.loadEntries(
+                permissionType,
+                packageName,
+                dateNavigationView.getDate(),
+                dateNavigationView.getPeriod())
+        }
+
+        // TODO(b/291249677): Log pagename.
+    }
+
+    private fun observeEntriesUpdates() {
+        entriesViewModel.entries.observe(viewLifecycleOwner) { state ->
+            when (state) {
+                is Loading -> {
+                    loadingView.isVisible = true
+                    noDataView.isVisible = false
+                    errorView.isVisible = false
+                    entriesRecyclerView.isVisible = false
+                }
+                is Empty -> {
+                    noDataView.isVisible = true
+                    loadingView.isVisible = false
+                    errorView.isVisible = false
+                    entriesRecyclerView.isVisible = false
+                }
+                is With -> {
+                    entriesRecyclerView.isVisible = true
+                    adapter.updateData(state.entries)
+                    errorView.isVisible = false
+                    noDataView.isVisible = false
+                    loadingView.isVisible = false
+                }
+                is LoadingFailed -> {
+                    errorView.isVisible = true
+                    loadingView.isVisible = false
+                    noDataView.isVisible = false
+                    entriesRecyclerView.isVisible = false
+                }
+            }
+        }
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/data/entries/EntriesViewModel.kt b/apk/src/com/android/healthconnect/controller/data/entries/EntriesViewModel.kt
new file mode 100644
index 0000000..e4b6ffc
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/data/entries/EntriesViewModel.kt
@@ -0,0 +1,199 @@
+/**
+ * Copyright (C) 2022 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.data.entries
+
+import android.util.Log
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+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.LoadAggregationInput
+import com.android.healthconnect.controller.data.entries.api.LoadDataEntriesInput
+import com.android.healthconnect.controller.data.entries.api.LoadMenstruationDataInput
+import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType.DISTANCE
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType.STEPS
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType.TOTAL_CALORIES_BURNED
+import com.android.healthconnect.controller.shared.app.AppInfoReader
+import com.android.healthconnect.controller.shared.app.AppMetadata
+import com.android.healthconnect.controller.shared.usecase.UseCaseResults
+import dagger.hilt.android.lifecycle.HiltViewModel
+import java.time.Instant
+import javax.inject.Inject
+import kotlinx.coroutines.launch
+
+/** View model for [AppEntriesFragment] and [AllEntriesFragment]. */
+@HiltViewModel
+class EntriesViewModel
+@Inject
+constructor(
+    private val appInfoReader: AppInfoReader,
+    private val loadDataEntriesUseCase: ILoadDataEntriesUseCase,
+    private val loadMenstruationDataUseCase: ILoadMenstruationDataUseCase,
+    private val loadDataAggregationsUseCase: ILoadDataAggregationsUseCase
+) : ViewModel() {
+
+    companion object {
+        private const val TAG = "EntriesViewModel"
+        private val AGGREGATE_HEADER_DATA_TYPES = listOf(STEPS, DISTANCE, TOTAL_CALORIES_BURNED)
+    }
+
+    private val _entries = MutableLiveData<EntriesFragmentState>()
+    val entries: LiveData<EntriesFragmentState>
+        get() = _entries
+
+    val currentSelectedDate = MutableLiveData<Instant>()
+    val period = MutableLiveData<DateNavigationPeriod>()
+
+    private val _appInfo = MutableLiveData<AppMetadata>()
+    val appInfo: LiveData<AppMetadata>
+        get() = _appInfo
+
+    fun loadEntries(
+        permissionType: HealthPermissionType,
+        selectedDate: Instant,
+        period: DateNavigationPeriod
+    ) {
+        loadData(permissionType, packageName = null, selectedDate, period, showDataOrigin = true)
+    }
+
+    fun loadEntries(
+        permissionType: HealthPermissionType,
+        packageName: String,
+        selectedDate: Instant,
+        period: DateNavigationPeriod
+    ) {
+        loadData(permissionType, packageName, selectedDate, period, showDataOrigin = false)
+    }
+
+    private fun loadData(
+        permissionType: HealthPermissionType,
+        packageName: String?,
+        selectedDate: Instant,
+        period: DateNavigationPeriod,
+        showDataOrigin: Boolean
+    ) {
+        _entries.postValue(EntriesFragmentState.Loading)
+        currentSelectedDate.postValue(selectedDate)
+        this.period.postValue(period)
+
+        viewModelScope.launch {
+            val list = ArrayList<FormattedEntry>()
+            val entriesResults =
+                when (permissionType) {
+                    // Special-casing Menstruation as it spans multiple days
+                    HealthPermissionType.MENSTRUATION -> {
+                        loadMenstruation(packageName, selectedDate, period, showDataOrigin)
+                    }
+                    else -> {
+                        loadAppEntries(
+                            permissionType, packageName, selectedDate, period, showDataOrigin)
+                    }
+                }
+            when (entriesResults) {
+                is UseCaseResults.Success -> {
+                    list.addAll(entriesResults.data)
+                    if (list.isEmpty()) {
+                        _entries.postValue(EntriesFragmentState.Empty)
+                    } else {
+                        addAggregation(
+                            permissionType, packageName, selectedDate, period, list, showDataOrigin)
+                        _entries.postValue(EntriesFragmentState.With(list))
+                    }
+                }
+                is UseCaseResults.Failed -> {
+                    Log.e(TAG, "Loading error ", entriesResults.exception)
+                    _entries.postValue(EntriesFragmentState.LoadingFailed)
+                }
+            }
+        }
+    }
+
+    private suspend fun loadAppEntries(
+        permissionType: HealthPermissionType,
+        packageName: String?,
+        selectedDate: Instant,
+        period: DateNavigationPeriod,
+        showDataOrigin: Boolean
+    ): UseCaseResults<List<FormattedEntry>> {
+        val input =
+            LoadDataEntriesInput(permissionType, packageName, selectedDate, period, showDataOrigin)
+        return loadDataEntriesUseCase.invoke(input)
+    }
+
+    private suspend fun loadMenstruation(
+        packageName: String?,
+        selectedDate: Instant,
+        period: DateNavigationPeriod,
+        showDataOrigin: Boolean
+    ): UseCaseResults<List<FormattedEntry>> {
+        val input = LoadMenstruationDataInput(packageName, selectedDate, period, showDataOrigin)
+        return loadMenstruationDataUseCase.invoke(input)
+    }
+
+    private suspend fun loadAggregation(
+        permissionType: HealthPermissionType,
+        packageName: String?,
+        selectedDate: Instant,
+        period: DateNavigationPeriod,
+        showDataOrigin: Boolean
+    ): UseCaseResults<FormattedEntry.FormattedAggregation> {
+        val input =
+            LoadAggregationInput.PeriodAggregation(
+                permissionType, packageName, selectedDate, period, showDataOrigin)
+        return loadDataAggregationsUseCase.invoke(input)
+    }
+
+    fun loadAppInfo(packageName: String) {
+        viewModelScope.launch { _appInfo.postValue(appInfoReader.getAppMetadata(packageName)) }
+    }
+
+    private suspend fun addAggregation(
+        permissionType: HealthPermissionType,
+        packageName: String?,
+        selectedDate: Instant,
+        period: DateNavigationPeriod,
+        list: ArrayList<FormattedEntry>,
+        showDataOrigin: Boolean
+    ) {
+        if (permissionType in AGGREGATE_HEADER_DATA_TYPES) {
+            when (val aggregationResult =
+                loadAggregation(
+                    permissionType, packageName, selectedDate, period, showDataOrigin)) {
+                is UseCaseResults.Success -> {
+                    list.add(0, aggregationResult.data)
+                }
+                is UseCaseResults.Failed -> {
+                    Log.e(TAG, "Failed to load aggregation!", aggregationResult.exception)
+                }
+            }
+        }
+    }
+
+    sealed class EntriesFragmentState {
+        object Loading : EntriesFragmentState()
+
+        object Empty : EntriesFragmentState()
+
+        object LoadingFailed : EntriesFragmentState()
+
+        data class With(val entries: List<FormattedEntry>) : EntriesFragmentState()
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/data/entries/EntryItemViewBinder.kt b/apk/src/com/android/healthconnect/controller/data/entries/EntryItemViewBinder.kt
new file mode 100644
index 0000000..ed9bced
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/data/entries/EntryItemViewBinder.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2022 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.data.entries
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageButton
+import android.widget.TextView
+import com.android.healthconnect.controller.R
+import com.android.healthconnect.controller.data.entries.FormattedEntry.FormattedDataEntry
+import com.android.healthconnect.controller.shared.recyclerview.ViewBinder
+import com.android.healthconnect.controller.utils.logging.DataEntriesElement
+import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
+import com.android.healthconnect.controller.utils.logging.HealthConnectLoggerEntryPoint
+import dagger.hilt.android.EntryPointAccessors
+
+/** ViewBinder for FormattedDataEntry. */
+class EntryItemViewBinder : ViewBinder<FormattedDataEntry, View> {
+
+    private lateinit var logger: HealthConnectLogger
+
+    override fun newView(parent: ViewGroup): View {
+        val context = parent.context.applicationContext
+        val hiltEntryPoint =
+            EntryPointAccessors.fromApplication(
+                context.applicationContext, HealthConnectLoggerEntryPoint::class.java)
+        logger = hiltEntryPoint.logger()
+        return LayoutInflater.from(parent.context).inflate(R.layout.item_data_entry, parent, false)
+    }
+
+    override fun bind(view: View, data: FormattedDataEntry, index: Int) {
+        val header = view.findViewById<TextView>(R.id.item_data_entry_header)
+        val title = view.findViewById<TextView>(R.id.item_data_entry_title)
+        val deleteButton = view.findViewById<ImageButton>(R.id.item_data_entry_delete)
+        logger.logImpression(DataEntriesElement.DATA_ENTRY_VIEW)
+        logger.logImpression(DataEntriesElement.DATA_ENTRY_DELETE_BUTTON)
+
+        title.text = data.title
+        title.contentDescription = data.titleA11y
+
+        header.text = data.header
+        header.contentDescription = data.headerA11y
+
+        deleteButton.visibility = View.GONE
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/data/entries/ExerciseSessionItemViewBinder.kt b/apk/src/com/android/healthconnect/controller/data/entries/ExerciseSessionItemViewBinder.kt
new file mode 100644
index 0000000..121c492
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/data/entries/ExerciseSessionItemViewBinder.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.data.entries
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageButton
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.core.view.isVisible
+import com.android.healthconnect.controller.R
+import com.android.healthconnect.controller.data.entries.FormattedEntry.ExerciseSessionEntry
+import com.android.healthconnect.controller.shared.RoundView
+import com.android.healthconnect.controller.shared.map.MapView
+import com.android.healthconnect.controller.shared.recyclerview.ViewBinder
+import com.android.healthconnect.controller.utils.logging.DataEntriesElement
+import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
+import com.android.healthconnect.controller.utils.logging.HealthConnectLoggerEntryPoint
+import dagger.hilt.android.EntryPointAccessors
+
+/** ViewBinder for ExerciseSessionEntry. */
+class ExerciseSessionItemViewBinder(
+    private val onItemClickedListener: OnClickEntryListener?,
+) : ViewBinder<ExerciseSessionEntry, View> {
+
+    private lateinit var logger: HealthConnectLogger
+
+    override fun newView(parent: ViewGroup): View {
+        val context = parent.context.applicationContext
+        val hiltEntryPoint =
+            EntryPointAccessors.fromApplication(
+                context.applicationContext, HealthConnectLoggerEntryPoint::class.java)
+        logger = hiltEntryPoint.logger()
+        return LayoutInflater.from(parent.context)
+            .inflate(R.layout.item_exercise_session_entry, parent, false)
+    }
+
+    override fun bind(view: View, data: ExerciseSessionEntry, index: Int) {
+        val container = view.findViewById<LinearLayout>(R.id.item_data_entry_container)
+        val divider = view.findViewById<LinearLayout>(R.id.item_data_entry_divider)
+        val header = view.findViewById<TextView>(R.id.item_data_entry_header)
+        val title = view.findViewById<TextView>(R.id.item_data_entry_title)
+        val notes = view.findViewById<TextView>(R.id.item_data_entry_notes)
+        val deleteButton = view.findViewById<ImageButton>(R.id.item_data_entry_delete)
+        val mapView = view.findViewById<MapView>(R.id.map_view)
+        val mapContainer = view.findViewById<RoundView>(R.id.map_round_view)
+        logger.logImpression(DataEntriesElement.EXERCISE_SESSION_ENTRY_BUTTON)
+        title.text = data.title
+        title.contentDescription = data.titleA11y
+        header.text = data.header
+        header.contentDescription = data.headerA11y
+        notes.isVisible = !data.notes.isNullOrBlank()
+        notes.text = data.notes
+        deleteButton.isVisible = false
+        divider.isVisible = false
+        mapContainer.isVisible = (data.route != null)
+        if (data.route != null) {
+            mapView.setRoute(data.route)
+        }
+        container.setOnClickListener {
+            logger.logInteraction(DataEntriesElement.EXERCISE_SESSION_ENTRY_BUTTON)
+            onItemClickedListener?.onItemClicked(data.uuid, index)
+        }
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/dataentries/FormattedDataEntry.kt b/apk/src/com/android/healthconnect/controller/data/entries/FormattedDataEntry.kt
similarity index 93%
rename from apk/src/com/android/healthconnect/controller/dataentries/FormattedDataEntry.kt
rename to apk/src/com/android/healthconnect/controller/data/entries/FormattedDataEntry.kt
index b5833b7..b62ad4d 100644
--- a/apk/src/com/android/healthconnect/controller/dataentries/FormattedDataEntry.kt
+++ b/apk/src/com/android/healthconnect/controller/data/entries/FormattedDataEntry.kt
@@ -1,4 +1,4 @@
-/**
+/*
  * Copyright (C) 2022 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
@@ -13,7 +13,7 @@
  * or implied. See the License for the specific language governing permissions and limitations under
  * the License.
  */
-package com.android.healthconnect.controller.dataentries
+package com.android.healthconnect.controller.data.entries
 
 import android.health.connect.datatypes.ExerciseRoute
 import com.android.healthconnect.controller.shared.DataType
@@ -76,4 +76,8 @@
         val aggregationA11y: String,
         val contributingApps: String
     ) : FormattedEntry(aggregation)
+
+    data class EntryDateSectionHeader(
+        val date: String,
+    ) : FormattedEntry(date)
 }
diff --git a/apk/src/com/android/healthconnect/controller/data/entries/OnClickEntryListener.kt b/apk/src/com/android/healthconnect/controller/data/entries/OnClickEntryListener.kt
new file mode 100644
index 0000000..f55608d
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/data/entries/OnClickEntryListener.kt
@@ -0,0 +1,19 @@
+/*
+ * 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.data.entries
+
+/** OnClickListener for Data entries. */
+interface OnClickEntryListener {
+    fun onItemClicked(id: String, index: Int)
+}
diff --git a/apk/src/com/android/healthconnect/controller/data/entries/OnDeleteEntryListener.kt b/apk/src/com/android/healthconnect/controller/data/entries/OnDeleteEntryListener.kt
new file mode 100644
index 0000000..f39dcde
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/data/entries/OnDeleteEntryListener.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.data.entries
+
+import com.android.healthconnect.controller.shared.DataType
+import java.time.Instant
+
+/** OnDeleteListener for Data entries. */
+interface OnDeleteEntryListener {
+    fun onDeleteEntry(
+        id: String,
+        dataType: DataType,
+        index: Int,
+        startTime: Instant? = null,
+        endTime: Instant? = null
+    )
+}
diff --git a/apk/src/com/android/healthconnect/controller/data/entries/SectionTitleViewBinder.kt b/apk/src/com/android/healthconnect/controller/data/entries/SectionTitleViewBinder.kt
new file mode 100644
index 0000000..e641ddf
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/data/entries/SectionTitleViewBinder.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.data.entries
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import android.widget.TextView
+import com.android.healthconnect.controller.R
+import com.android.healthconnect.controller.data.entries.FormattedEntry.EntryDateSectionHeader
+import com.android.healthconnect.controller.shared.recyclerview.ViewBinder
+
+/** View binder for a section title that looks like a PreferenceCategory. */
+class SectionTitleViewBinder : ViewBinder<EntryDateSectionHeader, LinearLayout> {
+    override fun newView(parent: ViewGroup): LinearLayout {
+        return LayoutInflater.from(parent.context).inflate(R.layout.section_title, parent, false)
+            as LinearLayout
+    }
+
+    override fun bind(view: View, data: EntryDateSectionHeader, index: Int) {
+        val titleView = view.findViewById<TextView>(android.R.id.title)
+        titleView.text = data.date
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/data/entries/SeriesDataItemViewBinder.kt b/apk/src/com/android/healthconnect/controller/data/entries/SeriesDataItemViewBinder.kt
new file mode 100644
index 0000000..5053bc6
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/data/entries/SeriesDataItemViewBinder.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.data.entries
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageButton
+import android.widget.LinearLayout
+import android.widget.RelativeLayout
+import android.widget.TextView
+import androidx.core.view.isVisible
+import com.android.healthconnect.controller.R
+import com.android.healthconnect.controller.data.entries.FormattedEntry.SeriesDataEntry
+import com.android.healthconnect.controller.shared.recyclerview.ViewBinder
+import com.android.healthconnect.controller.utils.logging.DataEntriesElement
+import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
+import com.android.healthconnect.controller.utils.logging.HealthConnectLoggerEntryPoint
+import dagger.hilt.android.EntryPointAccessors
+
+/** ViewBinder for [SeriesDataEntry]. */
+class SeriesDataItemViewBinder(
+    private val onItemClickedListener: OnClickEntryListener?,
+) : ViewBinder<SeriesDataEntry, View> {
+
+    private lateinit var logger: HealthConnectLogger
+
+    override fun newView(parent: ViewGroup): View {
+        val context = parent.context.applicationContext
+        val hiltEntryPoint =
+            EntryPointAccessors.fromApplication(
+                context.applicationContext, HealthConnectLoggerEntryPoint::class.java)
+        logger = hiltEntryPoint.logger()
+        return LayoutInflater.from(parent.context)
+            .inflate(R.layout.item_heart_rate_entry, parent, false)
+    }
+
+    override fun bind(view: View, data: SeriesDataEntry, index: Int) {
+        val container = view.findViewById<RelativeLayout>(R.id.item_data_entry_container)
+        val divider = view.findViewById<LinearLayout>(R.id.item_data_entry_divider)
+        val header = view.findViewById<TextView>(R.id.item_data_entry_header)
+        val title = view.findViewById<TextView>(R.id.item_data_entry_title)
+        val deleteButton = view.findViewById<ImageButton>(R.id.item_data_entry_delete)
+
+        logger.logImpression(DataEntriesElement.DATA_ENTRY_VIEW)
+        logger.logImpression(DataEntriesElement.DATA_ENTRY_DELETE_BUTTON)
+        title.text = data.title
+        title.contentDescription = data.titleA11y
+        header.text = data.header
+        header.contentDescription = data.headerA11y
+        deleteButton.isVisible = false
+        divider.isVisible = false
+        container.setOnClickListener {
+            logger.logInteraction(DataEntriesElement.DATA_ENTRY_VIEW)
+            onItemClickedListener?.onItemClicked(data.uuid, index)
+        }
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/data/entries/SleepSessionItemViewBinder.kt b/apk/src/com/android/healthconnect/controller/data/entries/SleepSessionItemViewBinder.kt
new file mode 100644
index 0000000..a3318f0
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/data/entries/SleepSessionItemViewBinder.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.data.entries
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageButton
+import android.widget.LinearLayout
+import android.widget.RelativeLayout
+import android.widget.TextView
+import androidx.core.view.isVisible
+import com.android.healthconnect.controller.R
+import com.android.healthconnect.controller.data.entries.FormattedEntry.SleepSessionEntry
+import com.android.healthconnect.controller.shared.recyclerview.ViewBinder
+import com.android.healthconnect.controller.utils.logging.DataEntriesElement
+import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
+import com.android.healthconnect.controller.utils.logging.HealthConnectLoggerEntryPoint
+import dagger.hilt.android.EntryPointAccessors
+
+/** ViewBinder for [SleepSessionEntry]. */
+class SleepSessionItemViewBinder(
+    private val onItemClickedListener: OnClickEntryListener?,
+) : ViewBinder<SleepSessionEntry, View> {
+
+    private lateinit var logger: HealthConnectLogger
+
+    override fun newView(parent: ViewGroup): View {
+        val context = parent.context.applicationContext
+        val hiltEntryPoint =
+            EntryPointAccessors.fromApplication(
+                context.applicationContext, HealthConnectLoggerEntryPoint::class.java)
+        logger = hiltEntryPoint.logger()
+        return LayoutInflater.from(parent.context)
+            .inflate(R.layout.item_sleep_session_entry, parent, false)
+    }
+
+    override fun bind(view: View, data: SleepSessionEntry, index: Int) {
+        val container = view.findViewById<RelativeLayout>(R.id.item_data_entry_container)
+        val divider = view.findViewById<LinearLayout>(R.id.item_data_entry_divider)
+        val header = view.findViewById<TextView>(R.id.item_data_entry_header)
+        val title = view.findViewById<TextView>(R.id.item_data_entry_title)
+        val notes = view.findViewById<TextView>(R.id.item_data_entry_notes)
+        val deleteButton = view.findViewById<ImageButton>(R.id.item_data_entry_delete)
+        logger.logImpression(DataEntriesElement.SLEEP_SESSION_ENTRY_BUTTON)
+        logger.logImpression(DataEntriesElement.DATA_ENTRY_DELETE_BUTTON)
+
+        title.text = data.title
+        title.contentDescription = data.titleA11y
+        header.text = data.header
+        header.contentDescription = data.headerA11y
+        notes.isVisible = !data.notes.isNullOrBlank()
+        notes.text = data.notes
+        deleteButton.isVisible = false
+        divider.isVisible = false
+        container.setOnClickListener {
+            logger.logInteraction(DataEntriesElement.SLEEP_SESSION_ENTRY_BUTTON)
+            onItemClickedListener?.onItemClicked(data.uuid, index)
+        }
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/data/entries/api/LoadDataAggregationsUseCase.kt b/apk/src/com/android/healthconnect/controller/data/entries/api/LoadDataAggregationsUseCase.kt
new file mode 100644
index 0000000..512fd06
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/data/entries/api/LoadDataAggregationsUseCase.kt
@@ -0,0 +1,233 @@
+/*
+ * 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.data.entries.api
+
+import android.health.connect.AggregateRecordsRequest
+import android.health.connect.AggregateRecordsResponse
+import android.health.connect.HealthConnectManager
+import android.health.connect.TimeInstantRangeFilter
+import android.health.connect.datatypes.AggregationType
+import android.health.connect.datatypes.DataOrigin
+import android.health.connect.datatypes.DistanceRecord
+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 androidx.core.os.asOutcomeReceiver
+import com.android.healthconnect.controller.data.entries.FormattedEntry.FormattedAggregation
+import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod
+import com.android.healthconnect.controller.dataentries.formatters.DistanceFormatter
+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.permissions.data.HealthPermissionType
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType.DISTANCE
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType.SLEEP
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType.STEPS
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType.TOTAL_CALORIES_BURNED
+import com.android.healthconnect.controller.service.IoDispatcher
+import com.android.healthconnect.controller.shared.app.AppInfoReader
+import com.android.healthconnect.controller.shared.usecase.BaseUseCase
+import com.android.healthconnect.controller.shared.usecase.UseCaseResults
+import java.time.Instant
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/** Use case to load aggregation data on the Entries screens. */
+@Singleton
+class LoadDataAggregationsUseCase
+@Inject
+constructor(
+    private val loadEntriesHelper: LoadEntriesHelper,
+    private val stepsFormatter: StepsFormatter,
+    private val totalCaloriesBurnedFormatter: TotalCaloriesBurnedFormatter,
+    private val distanceFormatter: DistanceFormatter,
+    private val sleepSessionFormatter: SleepSessionFormatter,
+    private val healthConnectManager: HealthConnectManager,
+    private val appInfoReader: AppInfoReader,
+    @IoDispatcher private val dispatcher: CoroutineDispatcher
+) :
+    BaseUseCase<LoadAggregationInput, FormattedAggregation>(dispatcher),
+    ILoadDataAggregationsUseCase {
+
+    override suspend fun execute(input: LoadAggregationInput): FormattedAggregation {
+        val timeFilterRange =
+            when (input) {
+                is LoadAggregationInput.PeriodAggregation -> {
+                    loadEntriesHelper.getTimeFilter(
+                        input.displayedStartTime, input.period, endTimeExclusive = false)
+                }
+                is LoadAggregationInput.CustomAggregation -> {
+                    loadEntriesHelper.getTimeFilter(input.startTime, input.endTime)
+                }
+            }
+        val showDataOrigin = input.showDataOrigin
+        val results =
+            when (input.permissionType) {
+                STEPS -> {
+                    readAggregations<Long>(
+                        timeFilterRange,
+                        StepsRecord.STEPS_COUNT_TOTAL,
+                        input.packageName,
+                        showDataOrigin,
+                        input.permissionType)
+                }
+                DISTANCE -> {
+                    readAggregations<Length>(
+                        timeFilterRange,
+                        DistanceRecord.DISTANCE_TOTAL,
+                        input.packageName,
+                        showDataOrigin,
+                        input.permissionType)
+                }
+                TOTAL_CALORIES_BURNED -> {
+                    readAggregations<Energy>(
+                        timeFilterRange,
+                        TotalCaloriesBurnedRecord.ENERGY_TOTAL,
+                        input.packageName,
+                        showDataOrigin,
+                        input.permissionType)
+                }
+                SLEEP -> {
+                    readAggregations<Long>(
+                        timeFilterRange,
+                        SleepSessionRecord.SLEEP_DURATION_TOTAL,
+                        input.packageName,
+                        showDataOrigin,
+                        input.permissionType)
+                }
+                else ->
+                    throw IllegalArgumentException(
+                        "${input.permissionType} is not supported for aggregations!")
+            }
+
+        return results
+    }
+
+    private suspend fun <T> readAggregations(
+        timeFilterRange: TimeInstantRangeFilter,
+        aggregationType: AggregationType<T>,
+        packageName: String?,
+        showDataOrigin: Boolean,
+        healthPermissionType: HealthPermissionType
+    ): FormattedAggregation {
+        val request =
+            AggregateRecordsRequest.Builder<T>(timeFilterRange).addAggregationType(aggregationType)
+        if (packageName != null) {
+            request.addDataOriginsFilter(DataOrigin.Builder().setPackageName(packageName).build())
+        }
+
+        val response =
+            suspendCancellableCoroutine<AggregateRecordsResponse<T>> { continuation ->
+                healthConnectManager.aggregate(
+                    request.build(), Runnable::run, continuation.asOutcomeReceiver())
+            }
+        val aggregationResult: T = requireNotNull(response.get(aggregationType))
+        val apps = response.getDataOrigins(aggregationType)
+        return formatAggregation(aggregationResult, apps, showDataOrigin, healthPermissionType)
+    }
+
+    private suspend fun <T> formatAggregation(
+        aggregationResult: T,
+        apps: Set<DataOrigin>,
+        showDataOrigin: Boolean,
+        healthPermissionType: HealthPermissionType
+    ): FormattedAggregation {
+        val contributingApps = getContributingApps(apps, showDataOrigin)
+        return when (aggregationResult) {
+            is Long -> {
+                when (healthPermissionType) {
+                    STEPS ->
+                        FormattedAggregation(
+                            aggregation = stepsFormatter.formatUnit(aggregationResult),
+                            aggregationA11y = stepsFormatter.formatA11yUnit(aggregationResult),
+                            contributingApps = contributingApps)
+                    SLEEP ->
+                        FormattedAggregation(
+                            aggregation = sleepSessionFormatter.formatUnit(aggregationResult),
+                            aggregationA11y =
+                                sleepSessionFormatter.formatA11yUnit(aggregationResult),
+                            contributingApps = contributingApps)
+                    else -> {
+                        throw IllegalArgumentException("Unsupported aggregation type!")
+                    }
+                }
+            }
+            is Energy ->
+                FormattedAggregation(
+                    aggregation = totalCaloriesBurnedFormatter.formatUnit(aggregationResult),
+                    aggregationA11y =
+                        totalCaloriesBurnedFormatter.formatA11yUnit(aggregationResult),
+                    contributingApps = contributingApps)
+            is Length ->
+                FormattedAggregation(
+                    aggregation = distanceFormatter.formatUnit(aggregationResult),
+                    aggregationA11y = distanceFormatter.formatA11yUnit(aggregationResult),
+                    contributingApps = contributingApps)
+            else -> {
+                throw IllegalArgumentException("Unsupported aggregation type!")
+            }
+        }
+    }
+
+    private suspend fun getContributingApps(
+        apps: Set<DataOrigin>,
+        showDataOrigin: Boolean
+    ): String {
+        if (!showDataOrigin) {
+            return ""
+        }
+        return apps
+            .map { origin -> appInfoReader.getAppMetadata(origin.packageName) }
+            .joinToString(", ") { it.appName }
+    }
+}
+
+sealed class LoadAggregationInput(
+    open val permissionType: HealthPermissionType,
+    open val packageName: String?,
+    open val showDataOrigin: Boolean
+) {
+    /** Aggregation input which uses a [DateNavigationPeriod] to calculate start and end times */
+    data class PeriodAggregation(
+        override val permissionType: HealthPermissionType,
+        override val packageName: String?,
+        val displayedStartTime: Instant,
+        val period: DateNavigationPeriod,
+        override val showDataOrigin: Boolean
+    ) : LoadAggregationInput(permissionType, packageName, showDataOrigin)
+
+    /** Aggregation input with custom start and end times */
+    data class CustomAggregation(
+        override val permissionType: HealthPermissionType,
+        override val packageName: String?,
+        val startTime: Instant,
+        val endTime: Instant,
+        override val showDataOrigin: Boolean
+    ) : LoadAggregationInput(permissionType, packageName, showDataOrigin)
+}
+
+interface ILoadDataAggregationsUseCase {
+    suspend fun invoke(input: LoadAggregationInput): UseCaseResults<FormattedAggregation>
+
+    suspend fun execute(input: LoadAggregationInput): FormattedAggregation
+}
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
new file mode 100644
index 0000000..475439d
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/data/entries/api/LoadDataEntriesUseCase.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.data.entries.api
+
+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
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlinx.coroutines.CoroutineDispatcher
+
+/** Use case to load data entries except for menstruation data types. */
+@Singleton
+class LoadDataEntriesUseCase
+@Inject
+constructor(
+    @IoDispatcher private val dispatcher: CoroutineDispatcher,
+    private val loadEntriesHelper: LoadEntriesHelper
+) : 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()
+
+        return loadEntriesHelper.maybeAddDateSectionHeaders(
+            entryRecords, input.period, input.showDataOrigin)
+    }
+}
+
+data class LoadDataEntriesInput(
+    val permissionType: HealthPermissionType,
+    val packageName: String?,
+    val displayedStartTime: Instant,
+    val period: DateNavigationPeriod,
+    val showDataOrigin: Boolean
+)
+
+interface ILoadDataEntriesUseCase {
+    suspend fun invoke(input: LoadDataEntriesInput): UseCaseResults<List<FormattedEntry>>
+
+    suspend fun execute(input: LoadDataEntriesInput): List<FormattedEntry>
+}
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
new file mode 100644
index 0000000..c761e3d
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/data/entries/api/LoadEntriesHelper.kt
@@ -0,0 +1,205 @@
+/*
+ * 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.data.entries.api
+
+import android.content.Context
+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.DataOrigin
+import android.health.connect.datatypes.InstantRecord
+import android.health.connect.datatypes.IntervalRecord
+import android.health.connect.datatypes.Record
+import android.util.Log
+import androidx.core.os.asOutcomeReceiver
+import com.android.healthconnect.controller.R
+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.utils.LocalDateTimeFormatter
+import com.android.healthconnect.controller.utils.SystemTimeSource
+import com.android.healthconnect.controller.utils.TimeSource
+import com.android.healthconnect.controller.utils.toLocalDate
+import com.google.common.annotations.VisibleForTesting
+import dagger.hilt.android.qualifiers.ApplicationContext
+import java.time.Duration
+import java.time.Instant
+import java.time.Period
+import java.time.ZoneId
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/**
+ * Helper methods for loading normal data entries ([LoadDataEntriesUseCase], menstruation entries
+ * ([LoadMenstruationDataUseCase]) and aggregations ([LoadDataAggregationsUseCase]).).
+ */
+@Singleton
+class LoadEntriesHelper
+@Inject
+constructor(
+    @ApplicationContext private val context: Context,
+    private val healthDataEntryFormatter: HealthDataEntryFormatter,
+    private val healthConnectManager: HealthConnectManager
+) {
+    private val dateFormatter = LocalDateTimeFormatter(context)
+    private val timeSource: TimeSource = SystemTimeSource
+
+    companion object {
+        private const val TAG = "LoadDataUseCaseHelper"
+    }
+
+    suspend fun readDataType(
+        data: Class<out Record>,
+        timeFilterRange: TimeInstantRangeFilter,
+        packageName: String?
+    ): List<Record> {
+        val filter = buildReadRecordsRequestUsingFilters(data, timeFilterRange, packageName)
+        val records =
+            suspendCancellableCoroutine<ReadRecordsResponse<*>> { continuation ->
+                    healthConnectManager.readRecords(
+                        filter, Runnable::run, continuation.asOutcomeReceiver())
+                }
+                .records
+                .sortedByDescending { record -> getStartTime(record) }
+        return records
+    }
+
+    /**
+     * If more than one day's data is displayed, inserts a section header for each day: 'Today',
+     * 'Yesterday', then date format.
+     */
+    suspend fun maybeAddDateSectionHeaders(
+        entries: List<Record>,
+        period: DateNavigationPeriod,
+        showDataOrigin: Boolean
+    ): List<FormattedEntry> {
+        if (entries.isEmpty()) {
+            return listOf()
+        }
+        if (period == DateNavigationPeriod.PERIOD_DAY) {
+            return entries.mapNotNull { record -> getFormatterRecord(record, showDataOrigin) }
+        }
+
+        val entriesWithSectionHeaders: MutableList<FormattedEntry> = mutableListOf()
+        var lastHeaderDate = Instant.EPOCH
+
+        entries.forEach {
+            val possibleNextHeaderDate = getStartTime(it)
+            if (!areOnSameDay(lastHeaderDate, possibleNextHeaderDate)) {
+                lastHeaderDate = possibleNextHeaderDate
+                val sectionTitle = getSectionTitle(lastHeaderDate)
+                entriesWithSectionHeaders.add(FormattedEntry.EntryDateSectionHeader(sectionTitle))
+            }
+            getFormatterRecord(it, showDataOrigin)?.let { formattedRecord ->
+                entriesWithSectionHeaders.add(formattedRecord)
+            }
+        }
+        return entriesWithSectionHeaders.toList()
+    }
+
+    private fun getSectionTitle(date: Instant): String {
+        val today =
+            Instant.ofEpochMilli(timeSource.currentTimeMillis())
+                .toLocalDate()
+                .atStartOfDay(timeSource.deviceZoneOffset())
+                .toInstant()
+        val yesterday =
+            today
+                .toLocalDate()
+                .minus(Period.ofDays(1))
+                .atStartOfDay(timeSource.deviceZoneOffset())
+                .toInstant()
+
+        return if (areOnSameDay(date, today)) {
+            context.getString(R.string.today_header)
+        } else if (areOnSameDay(date, yesterday)) {
+            context.getString(R.string.yesterday_header)
+        } else {
+            dateFormatter.formatLongDate(date)
+        }
+    }
+
+    private fun areOnSameDay(instant1: Instant, instant2: Instant): Boolean {
+        val localDate1 = instant1.atZone(timeSource.deviceZoneOffset()).toLocalDate()
+        val localDate2 = instant2.atZone(timeSource.deviceZoneOffset()).toLocalDate()
+        return localDate1 == localDate2
+    }
+
+    fun getStartTime(record: Record): Instant {
+        return when (record) {
+            is InstantRecord -> {
+                record.time
+            }
+            is IntervalRecord -> {
+                record.startTime
+            }
+            else -> {
+                throw IllegalArgumentException("unsupported record type!")
+            }
+        }
+    }
+
+    private suspend fun getFormatterRecord(
+        record: Record,
+        showDataOrigin: Boolean
+    ): FormattedEntry? {
+        return try {
+            healthDataEntryFormatter.format(record, showDataOrigin)
+        } catch (ex: Exception) {
+            Log.i(TAG, "Failed to format record!")
+            null
+        }
+    }
+
+    fun getTimeFilter(
+        startTime: Instant,
+        period: DateNavigationPeriod,
+        endTimeExclusive: Boolean
+    ): TimeInstantRangeFilter {
+        val start =
+            startTime
+                .atZone(ZoneId.systemDefault())
+                .toLocalDate()
+                .atStartOfDay(ZoneId.systemDefault())
+                .toInstant()
+        var end = start.atZone(ZoneId.systemDefault()).plus(toPeriod(period)).toInstant()
+        if (endTimeExclusive) {
+            end = end.minus(Duration.ofMillis(1))
+        }
+        return TimeInstantRangeFilter.Builder().setStartTime(start).setEndTime(end).build()
+    }
+
+    fun getTimeFilter(startTime: Instant, endTime: Instant): TimeInstantRangeFilter {
+        return TimeInstantRangeFilter.Builder().setStartTime(startTime).setEndTime(endTime).build()
+    }
+
+    @VisibleForTesting
+    fun buildReadRecordsRequestUsingFilters(
+        data: Class<out Record>,
+        timeFilterRange: TimeInstantRangeFilter,
+        packageName: String?
+    ): ReadRecordsRequestUsingFilters<out Record> {
+        val filter =
+            ReadRecordsRequestUsingFilters.Builder(data).setTimeRangeFilter(timeFilterRange)
+        if (packageName != null) {
+            filter.addDataOrigins(DataOrigin.Builder().setPackageName(packageName).build()).build()
+        }
+        return filter.build()
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/data/entries/api/LoadMenstruationDataUseCase.kt b/apk/src/com/android/healthconnect/controller/data/entries/api/LoadMenstruationDataUseCase.kt
new file mode 100644
index 0000000..47b7c0a
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/data/entries/api/LoadMenstruationDataUseCase.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.data.entries.api
+
+import android.health.connect.TimeInstantRangeFilter
+import android.health.connect.datatypes.MenstruationFlowRecord
+import android.health.connect.datatypes.MenstruationPeriodRecord
+import com.android.healthconnect.controller.data.entries.FormattedEntry
+import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod
+import com.android.healthconnect.controller.dataentries.formatters.MenstruationPeriodFormatter
+import com.android.healthconnect.controller.service.IoDispatcher
+import com.android.healthconnect.controller.shared.usecase.BaseUseCase
+import com.android.healthconnect.controller.shared.usecase.UseCaseResults
+import java.time.Duration.ofDays
+import java.time.Instant
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+
+/** Use case to load menstruation data entries. */
+class LoadMenstruationDataUseCase
+@Inject
+constructor(
+    private val loadEntriesHelper: LoadEntriesHelper,
+    private val menstruationPeriodFormatter: MenstruationPeriodFormatter,
+    @IoDispatcher private val dispatcher: CoroutineDispatcher,
+) :
+    BaseUseCase<LoadMenstruationDataInput, List<FormattedEntry>>(dispatcher),
+    ILoadMenstruationDataUseCase {
+
+    companion object {
+        private val SEARCH_RANGE = ofDays(30)
+    }
+
+    override suspend fun execute(input: LoadMenstruationDataInput): List<FormattedEntry> {
+        val packageName = input.packageName
+        val selectedDate = input.displayedStartTime
+        val period = input.period
+        val showDataOrigin = input.showDataOrigin
+        val data = buildList {
+            addAll(getMenstruationPeriodRecords(packageName, selectedDate, period, showDataOrigin))
+            addAll(getMenstruationFlowRecords(packageName, selectedDate, period, showDataOrigin))
+        }
+        return data
+    }
+
+    private suspend fun getMenstruationPeriodRecords(
+        packageName: String?,
+        selectedDate: Instant,
+        period: DateNavigationPeriod,
+        showDataOrigin: Boolean
+    ): List<FormattedEntry> {
+        val exactTimeRange =
+            loadEntriesHelper.getTimeFilter(selectedDate, period, endTimeExclusive = true)
+        val exactEnd = exactTimeRange.endTime!!
+        val exactStart = exactTimeRange.startTime!!
+
+        // Special-casing MenstruationPeriod as it spans multiple days and we show it on all these
+        // days in the UI (not just the first day).
+        // Hardcode max period length to 30 days (completely arbitrary number).
+        val extendedSearchTimeRange =
+            TimeInstantRangeFilter.Builder()
+                .setStartTime(exactEnd.minus(SEARCH_RANGE))
+                .setEndTime(exactEnd)
+                .build()
+
+        val records =
+            loadEntriesHelper
+                .readDataType(
+                    MenstruationPeriodRecord::class.java, extendedSearchTimeRange, packageName)
+                .filter { menstruationPeriodRecord ->
+                    menstruationPeriodRecord is MenstruationPeriodRecord &&
+                        menstruationPeriodRecord.startTime.isBefore(exactEnd) &&
+                        menstruationPeriodRecord.endTime.isAfter(exactStart)
+                }
+
+        return records.map { record ->
+            menstruationPeriodFormatter.format(
+                exactStart, record as MenstruationPeriodRecord, showDataOrigin)
+        }
+    }
+
+    private suspend fun getMenstruationFlowRecords(
+        packageName: String?,
+        selectedDate: Instant,
+        period: DateNavigationPeriod,
+        showDataOrigin: Boolean
+    ): List<FormattedEntry> {
+        val timeRange =
+            loadEntriesHelper.getTimeFilter(selectedDate, period, endTimeExclusive = true)
+        val records =
+            loadEntriesHelper.readDataType(
+                MenstruationFlowRecord::class.java, timeRange, packageName)
+
+        return loadEntriesHelper.maybeAddDateSectionHeaders(records, period, showDataOrigin)
+    }
+}
+
+data class LoadMenstruationDataInput(
+    val packageName: String?,
+    val displayedStartTime: Instant,
+    val period: DateNavigationPeriod,
+    val showDataOrigin: Boolean
+)
+
+interface ILoadMenstruationDataUseCase {
+    suspend fun invoke(input: LoadMenstruationDataInput): UseCaseResults<List<FormattedEntry>>
+
+    suspend fun execute(input: LoadMenstruationDataInput): List<FormattedEntry>
+}
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
new file mode 100644
index 0000000..d8f865c
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/data/entries/api/LoadSleepDataUseCase.kt
@@ -0,0 +1,42 @@
+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/data/entries/datenavigation/DateNavigationPeriod.kt b/apk/src/com/android/healthconnect/controller/data/entries/datenavigation/DateNavigationPeriod.kt
new file mode 100644
index 0000000..cde0412
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/data/entries/datenavigation/DateNavigationPeriod.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.data.entries.datenavigation
+
+import java.time.Period
+
+/** Supported time periods by [DateNavigationView]. */
+enum class DateNavigationPeriod {
+    PERIOD_DAY,
+    PERIOD_WEEK,
+    PERIOD_MONTH
+}
+
+/** Converts [DateNavigationPeriod] to [Period]. */
+fun toPeriod(dateNavigationPeriod: DateNavigationPeriod): Period {
+    return when (dateNavigationPeriod) {
+        DateNavigationPeriod.PERIOD_DAY -> Period.ofDays(1)
+        DateNavigationPeriod.PERIOD_WEEK -> Period.ofDays(7)
+        DateNavigationPeriod.PERIOD_MONTH -> Period.ofMonths(1)
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/data/entries/datenavigation/DateNavigationView.kt b/apk/src/com/android/healthconnect/controller/data/entries/datenavigation/DateNavigationView.kt
new file mode 100644
index 0000000..8cb1cec
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/data/entries/datenavigation/DateNavigationView.kt
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2022 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.data.entries.datenavigation
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import android.widget.AdapterView
+import android.widget.ImageButton
+import android.widget.Spinner
+import androidx.constraintlayout.widget.ConstraintLayout
+import com.android.healthconnect.controller.R
+import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod.PERIOD_DAY
+import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod.PERIOD_MONTH
+import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod.PERIOD_WEEK
+import com.android.healthconnect.controller.utils.SystemTimeSource
+import com.android.healthconnect.controller.utils.TimeSource
+import com.android.healthconnect.controller.utils.logging.DataEntriesElement
+import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
+import com.android.healthconnect.controller.utils.logging.HealthConnectLoggerEntryPoint
+import com.android.healthconnect.controller.utils.toLocalDate
+import dagger.hilt.android.EntryPointAccessors
+import java.time.DayOfWeek
+import java.time.Instant
+import java.time.LocalDate
+import java.time.Period
+import java.time.ZoneId
+
+/** Allows the user to navigate in time to see their past data. */
+class DateNavigationView
+@JvmOverloads
+constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0,
+    defStyleRes: Int = 0,
+    private val timeSource: TimeSource = SystemTimeSource
+) : ConstraintLayout(context, attrs, defStyleAttr, defStyleRes) {
+
+    private val logger: HealthConnectLogger
+
+    private lateinit var previousDayButton: ImageButton
+    private lateinit var nextDayButton: ImageButton
+    private lateinit var datePickerSpinner: Spinner
+    private var selectedDate = Instant.ofEpochMilli(timeSource.currentTimeMillis())
+    private var period: DateNavigationPeriod = PERIOD_DAY
+    private var onDateChangedListener: OnDateChangedListener? = null
+
+    init {
+        val hiltEntryPoint =
+            EntryPointAccessors.fromApplication(
+                context.applicationContext, HealthConnectLoggerEntryPoint::class.java)
+        logger = hiltEntryPoint.logger()
+
+        val view = inflate(context, R.layout.widget_date_navigation_with_spinner, this)
+        bindDateTextView(view)
+        bindNextDayButton(view)
+        bindPreviousDayButton(view)
+        updateDisplayedDates()
+    }
+
+    fun setDateChangedListener(mDateChangedListener: OnDateChangedListener?) {
+        this.onDateChangedListener = mDateChangedListener
+    }
+
+    fun setDate(date: Instant) {
+        selectedDate = date
+        updateDisplayedDates()
+    }
+
+    fun setPeriod(period: DateNavigationPeriod) {
+        this.period = period
+        updateDisplayedDates()
+    }
+
+    fun getDate(): Instant {
+        return selectedDate
+    }
+
+    fun getPeriod(): DateNavigationPeriod {
+        return period
+    }
+
+    private fun bindNextDayButton(view: View) {
+        nextDayButton = view.findViewById(R.id.navigation_next_day) as ImageButton
+        logger.logImpression(DataEntriesElement.NEXT_DAY_BUTTON)
+        nextDayButton.setOnClickListener {
+            logger.logInteraction(DataEntriesElement.NEXT_DAY_BUTTON)
+            selectedDate =
+                selectedDate.atZone(ZoneId.systemDefault()).plus(toPeriod(period)).toInstant()
+            updateDisplayedDates()
+        }
+    }
+
+    private fun bindPreviousDayButton(view: View) {
+        previousDayButton = view.findViewById(R.id.navigation_previous_day) as ImageButton
+        logger.logImpression(DataEntriesElement.PREVIOUS_DAY_BUTTON)
+        previousDayButton.setOnClickListener {
+            logger.logInteraction(DataEntriesElement.PREVIOUS_DAY_BUTTON)
+            selectedDate =
+                selectedDate.atZone(ZoneId.systemDefault()).minus(toPeriod(period)).toInstant()
+            updateDisplayedDates()
+        }
+    }
+
+    private fun bindDateTextView(view: View) {
+        datePickerSpinner = view.findViewById(R.id.date_picker_spinner) as Spinner
+
+        val adapter =
+            DatePickerSpinnerAdapter(view.context, getDisplayedStartDate(), period, timeSource)
+        adapter.setDropDownViewResource(R.layout.date_navigation_spinner_item)
+        datePickerSpinner.adapter = adapter
+
+        datePickerSpinner.onItemSelectedListener =
+            object : AdapterView.OnItemSelectedListener {
+                override fun onNothingSelected(parent: AdapterView<*>?) = Unit
+
+                override fun onItemSelected(
+                    parent: AdapterView<*>?,
+                    unused: View?,
+                    position: Int,
+                    id: Long
+                ) {
+                    val period: DateNavigationPeriod =
+                        when (position) {
+                            0 -> PERIOD_DAY
+                            1 -> PERIOD_WEEK
+                            2 -> PERIOD_MONTH
+                            else -> throw IllegalStateException("Not supported time period.")
+                        }
+                    setPeriod(period)
+                    updateDisplayedDates()
+                }
+            }
+    }
+
+    private fun updateDisplayedDates() {
+        onDateChangedListener?.onDateChanged(getDisplayedStartDate(), period)
+        val today =
+            LocalDate.ofInstant(
+                    Instant.ofEpochMilli(timeSource.currentTimeMillis()),
+                    timeSource.deviceZoneOffset())
+                .atStartOfDay(timeSource.deviceZoneOffset())
+                .toInstant()
+
+        // This can happen if e.g. today is Monday, user navigates back to Sunday, sets the period
+        // from Day to Week (underlying selected day is still Sunday), navigates to the next week
+        // (underlying selected day is next Sunday), sets the period back to Day => displayed day
+        // would be next Sunday. Instead, display today.
+        if (today.isBefore(selectedDate)) {
+            selectedDate = today
+        }
+
+        val displayedEndDate =
+            getDisplayedStartDate()
+                .toLocalDate()
+                .atStartOfDay(ZoneId.systemDefault())
+                .plus(toPeriod(period))
+                .toInstant()
+        nextDayButton.isEnabled = !displayedEndDate.isAfter(today)
+        (datePickerSpinner.adapter as DatePickerSpinnerAdapter).setStartTimeAndPeriod(
+            getDisplayedStartDate(), period)
+    }
+
+    private fun getDisplayedStartDate(): Instant =
+        when (period) {
+            PERIOD_DAY -> {
+                selectedDate
+                    .atZone(ZoneId.systemDefault())
+                    .toLocalDate()
+                    .atStartOfDay(ZoneId.systemDefault())
+                    .toInstant()
+            }
+            PERIOD_WEEK -> {
+                val dayOfWeek: DayOfWeek =
+                    selectedDate.atZone(ZoneId.systemDefault()).toLocalDate().dayOfWeek
+                val dayOfWeekOffset: Int = dayOfWeek.value - 1
+                selectedDate
+                    .atZone(ZoneId.systemDefault())
+                    .minus(Period.ofDays(dayOfWeekOffset))
+                    .toLocalDate()
+                    .atStartOfDay(ZoneId.systemDefault())
+                    .toInstant()
+            }
+            PERIOD_MONTH -> {
+                val dayOfMonth =
+                    selectedDate.atZone(ZoneId.systemDefault()).toLocalDate().dayOfMonth
+                val dayOfMonthOffset: Int = dayOfMonth - 1
+                selectedDate
+                    .atZone(ZoneId.systemDefault())
+                    .minus(Period.ofDays(dayOfMonthOffset))
+                    .toLocalDate()
+                    .atStartOfDay(ZoneId.systemDefault())
+                    .toInstant()
+            }
+        }
+
+    interface OnDateChangedListener {
+        fun onDateChanged(displayedStartDate: Instant, period: DateNavigationPeriod)
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/data/entries/datenavigation/DatePickerSpinnerAdapter.kt b/apk/src/com/android/healthconnect/controller/data/entries/datenavigation/DatePickerSpinnerAdapter.kt
new file mode 100644
index 0000000..2fe7249
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/data/entries/datenavigation/DatePickerSpinnerAdapter.kt
@@ -0,0 +1,189 @@
+package com.android.healthconnect.controller.data.entries.datenavigation
+
+import android.content.Context
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ArrayAdapter
+import android.widget.TextView
+import com.android.healthconnect.controller.R
+import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod.PERIOD_DAY
+import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod.PERIOD_MONTH
+import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod.PERIOD_WEEK
+import com.android.healthconnect.controller.utils.LocalDateTimeFormatter
+import com.android.healthconnect.controller.utils.SystemTimeSource
+import com.android.healthconnect.controller.utils.TimeSource
+import java.time.Instant
+import java.time.LocalDate
+import java.time.Period
+import java.time.temporal.TemporalAdjusters
+import java.time.temporal.WeekFields
+import java.util.Locale
+
+/** Adapter for the date picker in [DateNavigationView]. */
+class DatePickerSpinnerAdapter(
+    context: Context,
+    private var displayedStartDate: Instant,
+    var period: DateNavigationPeriod,
+    private val timeSource: TimeSource = SystemTimeSource
+) :
+    ArrayAdapter<String>(
+        context,
+        R.layout.date_navigation_spinner_item,
+        listOf(
+            context.getString(R.string.date_picker_day),
+            context.getString(R.string.date_picker_week),
+            context.getString(R.string.date_picker_month))) {
+    private val dateFormatter = LocalDateTimeFormatter(context)
+
+    fun setStartTimeAndPeriod(displayedStartTime: Instant, period: DateNavigationPeriod) {
+        this.displayedStartDate = displayedStartTime
+        this.period = period
+        notifyDataSetChanged()
+    }
+
+    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
+        val view = super.getView(position, convertView, parent)
+        if (view is TextView) {
+            getItem(position)?.let {
+                val dateView = formatDateTimeForTimePeriod(displayedStartDate, period)
+                view.text = maybeReplaceWithTemporalDeixis(dateView, displayedStartDate, period)
+            }
+        }
+        return view
+    }
+
+    /**
+     * Formats [startTime] and [period] as follows:
+     * * Day: "Sun, Aug 20" or "Mon, Aug 20, 2022"
+     * * Week: "Aug 21-27" or "Aug 21-27, 2022"
+     * * Month: "August" or "August 2022"
+     */
+    private fun formatDateTimeForTimePeriod(
+        startTime: Instant,
+        period: DateNavigationPeriod
+    ): String {
+        if (areInSameYear(startTime, Instant.ofEpochMilli(timeSource.currentTimeMillis()))) {
+            return when (period) {
+                PERIOD_DAY -> {
+                    dateFormatter.formatWeekdayDateWithoutYear(startTime)
+                }
+                PERIOD_WEEK -> {
+                    dateFormatter.formatDateRangeWithoutYear(
+                        startTime, startTime.plus(Period.ofWeeks(1)))
+                }
+                PERIOD_MONTH -> {
+                    dateFormatter.formatMonthWithoutYear(startTime)
+                }
+            }
+        }
+        return when (period) {
+            PERIOD_DAY -> {
+                dateFormatter.formatWeekdayDateWithYear(startTime)
+            }
+            PERIOD_WEEK -> {
+                dateFormatter.formatDateRangeWithYear(startTime, startTime.plus(Period.ofWeeks(1)))
+            }
+            PERIOD_MONTH -> {
+                dateFormatter.formatMonthWithYear(startTime)
+            }
+        }
+    }
+
+    /**
+     * Replaces recent dates with:
+     * * Day: "Today", "Yesterday"
+     * * Week: "This week", "Last week"
+     * * Month: "This month", "Last month"
+     *
+     * <p>No-op for other dates.
+     */
+    private fun maybeReplaceWithTemporalDeixis(
+        dateView: String,
+        selectedDate: Instant,
+        period: DateNavigationPeriod
+    ): String {
+        val currentPeriod =
+            LocalDate.ofInstant(
+                    Instant.ofEpochMilli(timeSource.currentTimeMillis()),
+                    timeSource.deviceZoneOffset())
+                .atStartOfDay(timeSource.deviceZoneOffset())
+                .toInstant()
+        val previousPeriod =
+            LocalDate.ofInstant(currentPeriod, timeSource.deviceZoneOffset())
+                .minus(toPeriod(period))
+                .atStartOfDay(timeSource.deviceZoneOffset())
+                .toInstant()
+
+        return if (!areInSameYear(selectedDate, currentPeriod)) {
+            dateView
+        } else if (areInSamePeriod(selectedDate, currentPeriod, period)) {
+            temporalDeixisForCurrentPeriod(period)
+        } else if (areInSamePeriod(selectedDate, previousPeriod, period)) {
+            temporalDeixisForLastPeriod(period)
+        } else {
+            dateView
+        }
+    }
+
+    /** Returns "Today", "This week", "This month". */
+    private fun temporalDeixisForCurrentPeriod(period: DateNavigationPeriod): String {
+        return when (period) {
+            PERIOD_DAY -> context.getString(R.string.today_header)
+            PERIOD_WEEK -> context.getString(R.string.this_week_header)
+            PERIOD_MONTH -> context.getString(R.string.this_month_header)
+        }
+    }
+
+    /** Returns "Yesterday", "Last week", "Last month". */
+    private fun temporalDeixisForLastPeriod(period: DateNavigationPeriod): String {
+        return when (period) {
+            PERIOD_DAY -> context.getString(R.string.yesterday_header)
+            PERIOD_WEEK -> context.getString(R.string.last_week_header)
+            PERIOD_MONTH -> context.getString(R.string.last_month_header)
+        }
+    }
+
+    /** Whether [instant1] and [instant2] are in the same [DateNavigationPeriod]. */
+    private fun areInSamePeriod(
+        instant1: Instant,
+        instant2: Instant,
+        period: DateNavigationPeriod
+    ): Boolean {
+        return when (period) {
+            PERIOD_DAY -> areOnSameDay(instant1, instant2)
+            PERIOD_WEEK -> areOnSameWeek(instant1, instant2)
+            PERIOD_MONTH -> areInSameMonth(instant1, instant2)
+        }
+    }
+
+    /** Whether [instant1] and [instant2] are in the same calendar day. */
+    private fun areOnSameDay(instant1: Instant, instant2: Instant): Boolean {
+        val localDate1 = instant1.atZone(timeSource.deviceZoneOffset()).toLocalDate()
+        val localDate2 = instant2.atZone(timeSource.deviceZoneOffset()).toLocalDate()
+        return localDate1 == localDate2
+    }
+
+    /** Whether [instant1] and [instant2] are on the same calendar week. */
+    private fun areOnSameWeek(instant1: Instant, instant2: Instant): Boolean {
+        val firstDayOfWeekField = WeekFields.of(Locale.getDefault()).firstDayOfWeek
+        val localDate1 = instant1.atZone(timeSource.deviceZoneOffset()).toLocalDate()
+        val localDate2 = instant2.atZone(timeSource.deviceZoneOffset()).toLocalDate()
+        val firstDayOfWeek1 = localDate1.with(TemporalAdjusters.previousOrSame(firstDayOfWeekField))
+        val firstDayOfWeek2 = localDate2.with(TemporalAdjusters.previousOrSame(firstDayOfWeekField))
+        return firstDayOfWeek1 == firstDayOfWeek2
+    }
+
+    /** Whether [instant1] and [instant2] are inn the same calendar month. */
+    private fun areInSameMonth(instant1: Instant, instant2: Instant): Boolean {
+        val monthOfYear1 = instant1.atZone(timeSource.deviceZoneOffset()).toLocalDate().month
+        val monthOfYear2 = instant2.atZone(timeSource.deviceZoneOffset()).toLocalDate().month
+        return monthOfYear1 == monthOfYear2
+    }
+
+    /** Whether [instant1] and [instant2] are inn the same calendar year. */
+    private fun areInSameYear(instant1: Instant, instant2: Instant): Boolean {
+        val year1 = instant1.atZone(timeSource.deviceZoneOffset()).toLocalDate().year
+        val year2 = instant2.atZone(timeSource.deviceZoneOffset()).toLocalDate().year
+        return year1 == year2
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/data/entriesandaccess/EntiresAndAccessFragment.kt b/apk/src/com/android/healthconnect/controller/data/entriesandaccess/EntiresAndAccessFragment.kt
new file mode 100644
index 0000000..c0b37e6
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/data/entriesandaccess/EntiresAndAccessFragment.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.data.entriesandaccess
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.os.bundleOf
+import androidx.fragment.app.Fragment
+import androidx.viewpager2.adapter.FragmentStateAdapter
+import androidx.viewpager2.widget.ViewPager2
+import com.android.healthconnect.controller.R
+import com.android.healthconnect.controller.data.access.AccessFragment
+import com.android.healthconnect.controller.data.appdata.AppDataFragment.Companion.PERMISSION_TYPE_KEY
+import com.android.healthconnect.controller.data.entries.AllEntriesFragment
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType
+import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
+import com.google.android.material.tabs.TabLayout
+import com.google.android.material.tabs.TabLayoutMediator
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
+
+/** Fragment with [AllEntriesFragment] tab and [AccessFragment] tab. */
+@AndroidEntryPoint(Fragment::class)
+class EntriesAndAccessFragment : Hilt_EntriesAndAccessFragment() {
+
+    @Inject lateinit var logger: HealthConnectLogger
+
+    private lateinit var permissionType: HealthPermissionType
+    private lateinit var viewPager: ViewPager2
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        // TODO(b/291249677): Add logging.
+        // logger.setPageId(pageName)
+
+        if (requireArguments().containsKey(PERMISSION_TYPE_KEY)) {
+            permissionType =
+                arguments?.getSerializable(PERMISSION_TYPE_KEY, HealthPermissionType::class.java)
+                    ?: throw IllegalArgumentException("PERMISSION_TYPE_KEY can't be null!")
+        }
+        return inflater.inflate(R.layout.fragment_entries_access, container, false)
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        viewPager = view.findViewById(R.id.view_pager)
+        viewPager.adapter = ViewPagerAdapter(this, permissionType)
+        val tabLayout: TabLayout = view.findViewById(R.id.tab_layout)
+        TabLayoutMediator(tabLayout, viewPager) { tab, position ->
+                if (position == 0) {
+                    tab.text = getString(R.string.tab_entries)
+                } else {
+                    tab.text = getString(R.string.tab_access)
+                }
+            }
+            .attach()
+    }
+
+    override fun onResume() {
+        super.onResume()
+    }
+
+    class ViewPagerAdapter(
+        fragment: EntriesAndAccessFragment,
+        private val permissionType: HealthPermissionType
+    ) : FragmentStateAdapter(fragment) {
+
+        override fun getItemCount(): Int = 2
+
+        override fun createFragment(position: Int): Fragment {
+            val fragment: Fragment = if (position == 0) AllEntriesFragment() else AccessFragment()
+            fragment.arguments = bundleOf(PERMISSION_TYPE_KEY to permissionType)
+            return fragment
+        }
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/dataaccess/HealthDataAccessFragment.kt b/apk/src/com/android/healthconnect/controller/dataaccess/HealthDataAccessFragment.kt
index 9575600..d25ee59 100644
--- a/apk/src/com/android/healthconnect/controller/dataaccess/HealthDataAccessFragment.kt
+++ b/apk/src/com/android/healthconnect/controller/dataaccess/HealthDataAccessFragment.kt
@@ -25,7 +25,9 @@
 import androidx.preference.Preference
 import androidx.preference.PreferenceGroup
 import com.android.healthconnect.controller.R
-import com.android.healthconnect.controller.dataaccess.HealthDataAccessViewModel.DataAccessScreenState
+import com.android.healthconnect.controller.data.access.AccessViewModel
+import com.android.healthconnect.controller.data.access.AccessViewModel.AccessScreenState
+import com.android.healthconnect.controller.data.access.AppAccessState
 import com.android.healthconnect.controller.deletion.DeletionConstants.DELETION_TYPE
 import com.android.healthconnect.controller.deletion.DeletionConstants.FRAGMENT_TAG_DELETION
 import com.android.healthconnect.controller.deletion.DeletionConstants.START_DELETION_EVENT
@@ -72,7 +74,7 @@
 
     @Inject lateinit var logger: HealthConnectLogger
 
-    private val viewModel: HealthDataAccessViewModel by viewModels()
+    private val viewModel: AccessViewModel by viewModels()
 
     private lateinit var permissionType: HealthPermissionType
 
@@ -172,13 +174,13 @@
         viewModel.loadAppMetaDataMap(permissionType)
         viewModel.appMetadataMap.observe(viewLifecycleOwner) { state ->
             when (state) {
-                is DataAccessScreenState.Loading -> {
+                is AccessScreenState.Loading -> {
                     setLoading(isLoading = true)
                 }
-                is DataAccessScreenState.Error -> {
+                is AccessScreenState.Error -> {
                     setError(hasError = true)
                 }
-                is DataAccessScreenState.WithData -> {
+                is AccessScreenState.WithData -> {
                     setLoading(isLoading = false, animate = false)
                     updateDataAccess(state.appMetadata)
                 }
@@ -199,33 +201,33 @@
         }
     }
 
-    private fun updateDataAccess(appMetadataMap: Map<DataAccessAppState, List<AppMetadata>>) {
+    private fun updateDataAccess(appMetadataMap: Map<AppAccessState, List<AppMetadata>>) {
         mCanReadSection?.removeAll()
         mCanWriteSection?.removeAll()
         mInactiveSection?.removeAll()
 
-        if (appMetadataMap.containsKey(DataAccessAppState.Read)) {
-            if (appMetadataMap[DataAccessAppState.Read]!!.isEmpty()) {
+        if (appMetadataMap.containsKey(AppAccessState.Read)) {
+            if (appMetadataMap[AppAccessState.Read]!!.isEmpty()) {
                 mCanReadSection?.isVisible = false
             } else {
                 mCanReadSection?.isVisible = true
-                appMetadataMap[DataAccessAppState.Read]!!.forEach { _appMetadata ->
+                appMetadataMap[AppAccessState.Read]!!.forEach { _appMetadata ->
                     mCanReadSection?.addPreference(createAppPreference(_appMetadata))
                 }
             }
         }
-        if (appMetadataMap.containsKey(DataAccessAppState.Write)) {
-            if (appMetadataMap[DataAccessAppState.Write]!!.isEmpty()) {
+        if (appMetadataMap.containsKey(AppAccessState.Write)) {
+            if (appMetadataMap[AppAccessState.Write]!!.isEmpty()) {
                 mCanWriteSection?.isVisible = false
             } else {
                 mCanWriteSection?.isVisible = true
-                appMetadataMap[DataAccessAppState.Write]!!.forEach { _appMetadata ->
+                appMetadataMap[AppAccessState.Write]!!.forEach { _appMetadata ->
                     mCanWriteSection?.addPreference(createAppPreference(_appMetadata))
                 }
             }
         }
-        if (appMetadataMap.containsKey(DataAccessAppState.Inactive)) {
-            if (appMetadataMap[DataAccessAppState.Inactive]!!.isEmpty()) {
+        if (appMetadataMap.containsKey(AppAccessState.Inactive)) {
+            if (appMetadataMap[AppAccessState.Inactive]!!.isEmpty()) {
                 mInactiveSection?.isVisible = false
             } else {
                 mInactiveSection?.isVisible = true
@@ -236,7 +238,7 @@
                                 R.string.inactive_apps_message,
                                 getString(fromPermissionType(permissionType).lowercaseLabel))
                     })
-                appMetadataMap[DataAccessAppState.Inactive]?.forEach { _appMetadata ->
+                appMetadataMap[AppAccessState.Inactive]?.forEach { _appMetadata ->
                     mInactiveSection?.addPreference(
                         InactiveAppPreference(requireContext()).also {
                             it.title = _appMetadata.appName
diff --git a/apk/src/com/android/healthconnect/controller/dataentries/AggregationViewBinder.kt b/apk/src/com/android/healthconnect/controller/dataentries/AggregationViewBinder.kt
index ff25543..4d753e0 100644
--- a/apk/src/com/android/healthconnect/controller/dataentries/AggregationViewBinder.kt
+++ b/apk/src/com/android/healthconnect/controller/dataentries/AggregationViewBinder.kt
@@ -18,7 +18,7 @@
 import android.view.ViewGroup
 import android.widget.TextView
 import com.android.healthconnect.controller.R
-import com.android.healthconnect.controller.dataentries.FormattedEntry.FormattedAggregation
+import com.android.healthconnect.controller.data.entries.FormattedEntry.FormattedAggregation
 import com.android.healthconnect.controller.shared.recyclerview.ViewBinder
 import com.android.healthconnect.controller.utils.logging.DataEntriesElement
 import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
diff --git a/apk/src/com/android/healthconnect/controller/dataentries/DataEntriesFragment.kt b/apk/src/com/android/healthconnect/controller/dataentries/DataEntriesFragment.kt
index 7e74f76..8575b51 100644
--- a/apk/src/com/android/healthconnect/controller/dataentries/DataEntriesFragment.kt
+++ b/apk/src/com/android/healthconnect/controller/dataentries/DataEntriesFragment.kt
@@ -31,14 +31,15 @@
 import androidx.recyclerview.widget.RecyclerView
 import androidx.recyclerview.widget.RecyclerView.VERTICAL
 import com.android.healthconnect.controller.R
+import com.android.healthconnect.controller.data.entries.FormattedEntry.ExerciseSessionEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry.FormattedAggregation
+import com.android.healthconnect.controller.data.entries.FormattedEntry.FormattedDataEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry.SeriesDataEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry.SleepSessionEntry
 import com.android.healthconnect.controller.dataentries.DataEntriesFragmentViewModel.DataEntriesFragmentState.Empty
 import com.android.healthconnect.controller.dataentries.DataEntriesFragmentViewModel.DataEntriesFragmentState.Loading
 import com.android.healthconnect.controller.dataentries.DataEntriesFragmentViewModel.DataEntriesFragmentState.LoadingFailed
 import com.android.healthconnect.controller.dataentries.DataEntriesFragmentViewModel.DataEntriesFragmentState.WithData
-import com.android.healthconnect.controller.dataentries.FormattedEntry.ExerciseSessionEntry
-import com.android.healthconnect.controller.dataentries.FormattedEntry.FormattedAggregation
-import com.android.healthconnect.controller.dataentries.FormattedEntry.FormattedDataEntry
-import com.android.healthconnect.controller.dataentries.FormattedEntry.SleepSessionEntry
 import com.android.healthconnect.controller.deletion.DeletionConstants.DELETION_TYPE
 import com.android.healthconnect.controller.deletion.DeletionConstants.END_TIME
 import com.android.healthconnect.controller.deletion.DeletionConstants.FRAGMENT_TAG_DELETION
@@ -87,7 +88,8 @@
                 findNavController()
                     .navigate(
                         R.id.action_dataEntriesFragment_to_dataEntryDetailsFragment,
-                        DataEntryDetailsFragment.createBundle(permissionType, id))
+                        DataEntryDetailsFragment.createBundle(
+                            permissionType, id, showDataOrigin = true))
             }
         }
     }
@@ -158,7 +160,7 @@
                 .setViewBinder(FormattedDataEntry::class.java, entryViewBinder)
                 .setViewBinder(SleepSessionEntry::class.java, sleepSessionViewBinder)
                 .setViewBinder(ExerciseSessionEntry::class.java, exerciseSessionItemViewBinder)
-                .setViewBinder(FormattedEntry.SeriesDataEntry::class.java, seriesDataItemViewBinder)
+                .setViewBinder(SeriesDataEntry::class.java, seriesDataItemViewBinder)
                 .setViewBinder(FormattedAggregation::class.java, aggregationViewBinder)
                 .build()
         entriesRecyclerView =
diff --git a/apk/src/com/android/healthconnect/controller/dataentries/DataEntriesFragmentViewModel.kt b/apk/src/com/android/healthconnect/controller/dataentries/DataEntriesFragmentViewModel.kt
index ea5cfd7..014838e 100644
--- a/apk/src/com/android/healthconnect/controller/dataentries/DataEntriesFragmentViewModel.kt
+++ b/apk/src/com/android/healthconnect/controller/dataentries/DataEntriesFragmentViewModel.kt
@@ -20,7 +20,8 @@
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
-import com.android.healthconnect.controller.dataentries.FormattedEntry.FormattedAggregation
+import com.android.healthconnect.controller.data.entries.FormattedEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry.FormattedAggregation
 import com.android.healthconnect.controller.permissions.data.HealthPermissionType
 import com.android.healthconnect.controller.permissions.data.HealthPermissionType.DISTANCE
 import com.android.healthconnect.controller.permissions.data.HealthPermissionType.STEPS
@@ -49,6 +50,7 @@
     private val _dataEntries = MutableLiveData<DataEntriesFragmentState>()
     val dataEntries: LiveData<DataEntriesFragmentState>
         get() = _dataEntries
+
     val currentSelectedDate = MutableLiveData<Instant>()
 
     fun loadData(permissionType: HealthPermissionType, selectedDate: Instant) {
@@ -119,8 +121,11 @@
 
     sealed class DataEntriesFragmentState {
         object Loading : DataEntriesFragmentState()
+
         object Empty : DataEntriesFragmentState()
+
         object LoadingFailed : DataEntriesFragmentState()
+
         data class WithData(val entries: List<FormattedEntry>) : DataEntriesFragmentState()
     }
 }
diff --git a/apk/src/com/android/healthconnect/controller/dataentries/DateNavigationView.kt b/apk/src/com/android/healthconnect/controller/dataentries/DateNavigationView.kt
index a2e515f..811c113 100644
--- a/apk/src/com/android/healthconnect/controller/dataentries/DateNavigationView.kt
+++ b/apk/src/com/android/healthconnect/controller/dataentries/DateNavigationView.kt
@@ -104,6 +104,7 @@
     }
 
     private fun bindDateTextView(view: View) {
+        // TODO(b/291249677): Add log in upcoming CL.
         selectedDateView = view.findViewById(R.id.selected_date) as TextView
         logger.logImpression(DataEntriesElement.SELECT_DATE_BUTTON)
         selectedDateView.setOnClickListener {
diff --git a/apk/src/com/android/healthconnect/controller/dataentries/EntryItemViewBinder.kt b/apk/src/com/android/healthconnect/controller/dataentries/EntryItemViewBinder.kt
index e81c525..228750b 100644
--- a/apk/src/com/android/healthconnect/controller/dataentries/EntryItemViewBinder.kt
+++ b/apk/src/com/android/healthconnect/controller/dataentries/EntryItemViewBinder.kt
@@ -19,7 +19,7 @@
 import android.widget.ImageButton
 import android.widget.TextView
 import com.android.healthconnect.controller.R
-import com.android.healthconnect.controller.dataentries.FormattedEntry.FormattedDataEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry.FormattedDataEntry
 import com.android.healthconnect.controller.shared.recyclerview.ViewBinder
 import com.android.healthconnect.controller.utils.logging.DataEntriesElement
 import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
diff --git a/apk/src/com/android/healthconnect/controller/dataentries/ExerciseSessionItemViewBinder.kt b/apk/src/com/android/healthconnect/controller/dataentries/ExerciseSessionItemViewBinder.kt
index 8108482..b634aae 100644
--- a/apk/src/com/android/healthconnect/controller/dataentries/ExerciseSessionItemViewBinder.kt
+++ b/apk/src/com/android/healthconnect/controller/dataentries/ExerciseSessionItemViewBinder.kt
@@ -21,7 +21,7 @@
 import android.widget.TextView
 import androidx.core.view.isVisible
 import com.android.healthconnect.controller.R
-import com.android.healthconnect.controller.dataentries.FormattedEntry.ExerciseSessionEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry.ExerciseSessionEntry
 import com.android.healthconnect.controller.shared.RoundView
 import com.android.healthconnect.controller.shared.map.MapView
 import com.android.healthconnect.controller.shared.recyclerview.ViewBinder
diff --git a/apk/src/com/android/healthconnect/controller/dataentries/LoadDataAggregationsUseCase.kt b/apk/src/com/android/healthconnect/controller/dataentries/LoadDataAggregationsUseCase.kt
index 9a5792f..037a6af 100644
--- a/apk/src/com/android/healthconnect/controller/dataentries/LoadDataAggregationsUseCase.kt
+++ b/apk/src/com/android/healthconnect/controller/dataentries/LoadDataAggregationsUseCase.kt
@@ -26,18 +26,21 @@
 import android.health.connect.datatypes.AggregationType
 import android.health.connect.datatypes.DataOrigin
 import android.health.connect.datatypes.DistanceRecord
+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 androidx.core.os.asOutcomeReceiver
 import com.android.healthconnect.controller.R
-import com.android.healthconnect.controller.dataentries.FormattedEntry.FormattedAggregation
+import com.android.healthconnect.controller.data.entries.FormattedEntry.FormattedAggregation
 import com.android.healthconnect.controller.dataentries.formatters.DistanceFormatter
+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.permissions.data.HealthPermissionType
 import com.android.healthconnect.controller.permissions.data.HealthPermissionType.DISTANCE
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType.SLEEP
 import com.android.healthconnect.controller.permissions.data.HealthPermissionType.STEPS
 import com.android.healthconnect.controller.permissions.data.HealthPermissionType.TOTAL_CALORIES_BURNED
 import com.android.healthconnect.controller.service.IoDispatcher
@@ -59,6 +62,7 @@
     private val stepsFormatter: StepsFormatter,
     private val totalCaloriesBurnedFormatter: TotalCaloriesBurnedFormatter,
     private val distanceFormatter: DistanceFormatter,
+    private val sleepFormatter: SleepSessionFormatter,
     private val healthConnectManager: HealthConnectManager,
     private val appInfoReader: AppInfoReader,
     @IoDispatcher private val dispatcher: CoroutineDispatcher,
@@ -70,14 +74,24 @@
         val results =
             when (input.permissionType) {
                 STEPS -> {
-                    readAggregations<Long>(timeFilterRange, StepsRecord.STEPS_COUNT_TOTAL)
+                    readAggregations<Long>(
+                        timeFilterRange, StepsRecord.STEPS_COUNT_TOTAL, input.permissionType)
                 }
                 DISTANCE -> {
-                    readAggregations<Length>(timeFilterRange, DistanceRecord.DISTANCE_TOTAL)
+                    readAggregations<Length>(
+                        timeFilterRange, DistanceRecord.DISTANCE_TOTAL, input.permissionType)
                 }
                 TOTAL_CALORIES_BURNED -> {
                     readAggregations<Energy>(
-                        timeFilterRange, TotalCaloriesBurnedRecord.ENERGY_TOTAL)
+                        timeFilterRange,
+                        TotalCaloriesBurnedRecord.ENERGY_TOTAL,
+                        input.permissionType)
+                }
+                SLEEP -> {
+                    readAggregations<Long>(
+                        timeFilterRange,
+                        SleepSessionRecord.SLEEP_DURATION_TOTAL,
+                        input.permissionType)
                 }
                 else ->
                     throw IllegalArgumentException(
@@ -89,7 +103,8 @@
 
     private suspend fun <T> readAggregations(
         timeFilterRange: TimeInstantRangeFilter,
-        aggregationType: AggregationType<T>
+        aggregationType: AggregationType<T>,
+        healthPermissionType: HealthPermissionType
     ): FormattedAggregation {
         val request =
             AggregateRecordsRequest.Builder<T>(timeFilterRange)
@@ -103,35 +118,43 @@
             }
         val aggregationResult: T = requireNotNull(response.get(aggregationType))
         val apps = response.getDataOrigins(aggregationType)
-        return formatAggregation(aggregationResult, apps)
+        return formatAggregation(aggregationResult, apps, healthPermissionType)
     }
 
     private suspend fun <T> formatAggregation(
         aggregationResult: T,
-        apps: Set<DataOrigin>
+        apps: Set<DataOrigin>,
+        healthPermissionType: HealthPermissionType
     ): FormattedAggregation {
         val contributingApps = getContributingApps(apps)
-        return when (aggregationResult) {
-            is Long ->
+        return when (healthPermissionType) {
+            STEPS ->
                 FormattedAggregation(
-                    aggregation = stepsFormatter.formatUnit(aggregationResult),
+                    aggregation = stepsFormatter.formatUnit(aggregationResult as Long),
                     aggregationA11y =
                         addAggregationA11yPrefix(stepsFormatter.formatA11yUnit(aggregationResult)),
                     contributingApps = contributingApps)
-            is Energy ->
+            TOTAL_CALORIES_BURNED ->
                 FormattedAggregation(
-                    aggregation = totalCaloriesBurnedFormatter.formatUnit(aggregationResult),
+                    aggregation =
+                        totalCaloriesBurnedFormatter.formatUnit(aggregationResult as Energy),
                     aggregationA11y =
                         addAggregationA11yPrefix(
                             totalCaloriesBurnedFormatter.formatA11yUnit(aggregationResult)),
                     contributingApps = contributingApps)
-            is Length ->
+            DISTANCE ->
                 FormattedAggregation(
-                    aggregation = distanceFormatter.formatUnit(aggregationResult),
+                    aggregation = distanceFormatter.formatUnit(aggregationResult as Length),
                     aggregationA11y =
                         addAggregationA11yPrefix(
                             distanceFormatter.formatA11yUnit(aggregationResult)),
                     contributingApps = contributingApps)
+            SLEEP ->
+                FormattedAggregation(
+                    aggregation = sleepFormatter.formatUnit(aggregationResult as Long),
+                    aggregationA11y =
+                        addAggregationA11yPrefix(sleepFormatter.formatA11yUnit(aggregationResult)),
+                    contributingApps = contributingApps)
             else -> {
                 throw IllegalArgumentException("Unsupported aggregation type!")
             }
diff --git a/apk/src/com/android/healthconnect/controller/dataentries/LoadDataEntriesUseCase.kt b/apk/src/com/android/healthconnect/controller/dataentries/LoadDataEntriesUseCase.kt
index f77ee5e..98e6696 100644
--- a/apk/src/com/android/healthconnect/controller/dataentries/LoadDataEntriesUseCase.kt
+++ b/apk/src/com/android/healthconnect/controller/dataentries/LoadDataEntriesUseCase.kt
@@ -24,6 +24,7 @@
 import android.health.connect.datatypes.Record
 import android.util.Log
 import androidx.core.os.asOutcomeReceiver
+import com.android.healthconnect.controller.data.entries.FormattedEntry
 import com.android.healthconnect.controller.dataentries.formatters.shared.HealthDataEntryFormatter
 import com.android.healthconnect.controller.permissions.data.HealthPermissionType
 import com.android.healthconnect.controller.service.IoDispatcher
diff --git a/apk/src/com/android/healthconnect/controller/dataentries/LoadMenstruationDataUseCase.kt b/apk/src/com/android/healthconnect/controller/dataentries/LoadMenstruationDataUseCase.kt
index 0fb696e..963071e 100644
--- a/apk/src/com/android/healthconnect/controller/dataentries/LoadMenstruationDataUseCase.kt
+++ b/apk/src/com/android/healthconnect/controller/dataentries/LoadMenstruationDataUseCase.kt
@@ -27,6 +27,7 @@
 import android.health.connect.datatypes.Record
 import android.util.Log
 import androidx.core.os.asOutcomeReceiver
+import com.android.healthconnect.controller.data.entries.FormattedEntry
 import com.android.healthconnect.controller.dataentries.formatters.MenstruationPeriodFormatter
 import com.android.healthconnect.controller.dataentries.formatters.shared.HealthDataEntryFormatter
 import com.android.healthconnect.controller.service.IoDispatcher
diff --git a/apk/src/com/android/healthconnect/controller/dataentries/SeriesDataItemViewBinder.kt b/apk/src/com/android/healthconnect/controller/dataentries/SeriesDataItemViewBinder.kt
index 3a20e28..b484299 100644
--- a/apk/src/com/android/healthconnect/controller/dataentries/SeriesDataItemViewBinder.kt
+++ b/apk/src/com/android/healthconnect/controller/dataentries/SeriesDataItemViewBinder.kt
@@ -22,7 +22,7 @@
 import android.widget.TextView
 import androidx.core.view.isVisible
 import com.android.healthconnect.controller.R
-import com.android.healthconnect.controller.dataentries.FormattedEntry.SeriesDataEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry.SeriesDataEntry
 import com.android.healthconnect.controller.shared.recyclerview.ViewBinder
 import com.android.healthconnect.controller.utils.logging.DataEntriesElement
 import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
diff --git a/apk/src/com/android/healthconnect/controller/dataentries/SleepSessionItemViewBinder.kt b/apk/src/com/android/healthconnect/controller/dataentries/SleepSessionItemViewBinder.kt
index d4c25ef..c8df3cc 100644
--- a/apk/src/com/android/healthconnect/controller/dataentries/SleepSessionItemViewBinder.kt
+++ b/apk/src/com/android/healthconnect/controller/dataentries/SleepSessionItemViewBinder.kt
@@ -22,7 +22,7 @@
 import android.widget.TextView
 import androidx.core.view.isVisible
 import com.android.healthconnect.controller.R
-import com.android.healthconnect.controller.dataentries.FormattedEntry.SleepSessionEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry.SleepSessionEntry
 import com.android.healthconnect.controller.shared.recyclerview.ViewBinder
 import com.android.healthconnect.controller.utils.logging.DataEntriesElement
 import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
diff --git a/apk/src/com/android/healthconnect/controller/dataentries/formatters/CyclingPedalingCadenceFormatter.kt b/apk/src/com/android/healthconnect/controller/dataentries/formatters/CyclingPedalingCadenceFormatter.kt
index b32daa9..cdd9903 100644
--- a/apk/src/com/android/healthconnect/controller/dataentries/formatters/CyclingPedalingCadenceFormatter.kt
+++ b/apk/src/com/android/healthconnect/controller/dataentries/formatters/CyclingPedalingCadenceFormatter.kt
@@ -20,7 +20,7 @@
 import android.icu.text.MessageFormat.*
 import androidx.annotation.StringRes
 import com.android.healthconnect.controller.R
-import com.android.healthconnect.controller.dataentries.FormattedEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry
 import com.android.healthconnect.controller.dataentries.formatters.shared.EntryFormatter
 import com.android.healthconnect.controller.dataentries.formatters.shared.SessionDetailsFormatter
 import com.android.healthconnect.controller.dataentries.units.UnitPreferences
diff --git a/apk/src/com/android/healthconnect/controller/dataentries/formatters/DurationFormatter.kt b/apk/src/com/android/healthconnect/controller/dataentries/formatters/DurationFormatter.kt
index b1f0910..617bcac 100644
--- a/apk/src/com/android/healthconnect/controller/dataentries/formatters/DurationFormatter.kt
+++ b/apk/src/com/android/healthconnect/controller/dataentries/formatters/DurationFormatter.kt
@@ -45,7 +45,7 @@
     }
 
     /**
-     * Formats the given duration in the format 'X hours, X minutes'.
+     * Formats the given duration in the format 'X hours X minutes'.
      *
      * <p>Returns '0 minutes' for durations less than a minute. Returns only the minute component
      * for durations less than an hour. Returns only the hour component for durations with hours and
diff --git a/apk/src/com/android/healthconnect/controller/dataentries/formatters/ExerciseSessionFormatter.kt b/apk/src/com/android/healthconnect/controller/dataentries/formatters/ExerciseSessionFormatter.kt
index c7d85f1..a3cd8fa 100644
--- a/apk/src/com/android/healthconnect/controller/dataentries/formatters/ExerciseSessionFormatter.kt
+++ b/apk/src/com/android/healthconnect/controller/dataentries/formatters/ExerciseSessionFormatter.kt
@@ -152,10 +152,10 @@
 import android.health.connect.datatypes.ExerciseSessionType.EXERCISE_SESSION_TYPE_YOGA
 import android.icu.text.MessageFormat.format
 import com.android.healthconnect.controller.R
-import com.android.healthconnect.controller.dataentries.FormattedEntry
-import com.android.healthconnect.controller.dataentries.FormattedEntry.ExerciseSessionEntry
-import com.android.healthconnect.controller.dataentries.FormattedEntry.FormattedSessionDetail
-import com.android.healthconnect.controller.dataentries.FormattedEntry.SessionHeader
+import com.android.healthconnect.controller.data.entries.FormattedEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry.ExerciseSessionEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry.FormattedSessionDetail
+import com.android.healthconnect.controller.data.entries.FormattedEntry.SessionHeader
 import com.android.healthconnect.controller.dataentries.formatters.DurationFormatter.formatDurationLong
 import com.android.healthconnect.controller.dataentries.formatters.DurationFormatter.formatDurationShort
 import com.android.healthconnect.controller.dataentries.formatters.shared.BaseFormatter
diff --git a/apk/src/com/android/healthconnect/controller/dataentries/formatters/HeartRateFormatter.kt b/apk/src/com/android/healthconnect/controller/dataentries/formatters/HeartRateFormatter.kt
index f2f175d..56e2dd8 100644
--- a/apk/src/com/android/healthconnect/controller/dataentries/formatters/HeartRateFormatter.kt
+++ b/apk/src/com/android/healthconnect/controller/dataentries/formatters/HeartRateFormatter.kt
@@ -20,7 +20,7 @@
 import android.icu.text.MessageFormat
 import androidx.annotation.StringRes
 import com.android.healthconnect.controller.R
-import com.android.healthconnect.controller.dataentries.FormattedEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry
 import com.android.healthconnect.controller.dataentries.formatters.shared.EntryFormatter
 import com.android.healthconnect.controller.dataentries.formatters.shared.SessionDetailsFormatter
 import com.android.healthconnect.controller.dataentries.units.UnitPreferences
diff --git a/apk/src/com/android/healthconnect/controller/dataentries/formatters/MenstruationPeriodFormatter.kt b/apk/src/com/android/healthconnect/controller/dataentries/formatters/MenstruationPeriodFormatter.kt
index 2323525..e024eac 100644
--- a/apk/src/com/android/healthconnect/controller/dataentries/formatters/MenstruationPeriodFormatter.kt
+++ b/apk/src/com/android/healthconnect/controller/dataentries/formatters/MenstruationPeriodFormatter.kt
@@ -22,7 +22,7 @@
 import android.health.connect.datatypes.MenstruationPeriodRecord
 import android.health.connect.datatypes.Record
 import com.android.healthconnect.controller.R
-import com.android.healthconnect.controller.dataentries.FormattedEntry.FormattedDataEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry.FormattedDataEntry
 import com.android.healthconnect.controller.shared.DataType
 import com.android.healthconnect.controller.shared.app.AppInfoReader
 import com.android.healthconnect.controller.utils.toLocalDate
@@ -40,11 +40,15 @@
     @ApplicationContext private val context: Context
 ) {
 
-    suspend fun format(day: Instant, record: MenstruationPeriodRecord): FormattedDataEntry {
+    suspend fun format(
+        day: Instant,
+        record: MenstruationPeriodRecord,
+        showDataOrigin: Boolean = true
+    ): FormattedDataEntry {
         val dayOfPeriod = dayOfPeriod(record, day)
         val totalDays = totalDaysOfPeriod(record)
         val title = context.getString(R.string.period_day, dayOfPeriod, totalDays)
-        val appName = getAppName(record)
+        val appName = if (showDataOrigin) getAppName(record) else ""
 
         return FormattedDataEntry(
             uuid = record.metadata.id,
diff --git a/apk/src/com/android/healthconnect/controller/dataentries/formatters/PowerFormatter.kt b/apk/src/com/android/healthconnect/controller/dataentries/formatters/PowerFormatter.kt
index ee63736..c751ae6 100644
--- a/apk/src/com/android/healthconnect/controller/dataentries/formatters/PowerFormatter.kt
+++ b/apk/src/com/android/healthconnect/controller/dataentries/formatters/PowerFormatter.kt
@@ -21,8 +21,8 @@
 import android.icu.text.MessageFormat
 import androidx.annotation.StringRes
 import com.android.healthconnect.controller.R
-import com.android.healthconnect.controller.dataentries.FormattedEntry
-import com.android.healthconnect.controller.dataentries.FormattedEntry.FormattedSessionDetail
+import com.android.healthconnect.controller.data.entries.FormattedEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry.FormattedSessionDetail
 import com.android.healthconnect.controller.dataentries.formatters.shared.EntryFormatter
 import com.android.healthconnect.controller.dataentries.formatters.shared.SessionDetailsFormatter
 import com.android.healthconnect.controller.dataentries.units.UnitPreferences
diff --git a/apk/src/com/android/healthconnect/controller/dataentries/formatters/SleepSessionFormatter.kt b/apk/src/com/android/healthconnect/controller/dataentries/formatters/SleepSessionFormatter.kt
index a636675..1286a08 100644
--- a/apk/src/com/android/healthconnect/controller/dataentries/formatters/SleepSessionFormatter.kt
+++ b/apk/src/com/android/healthconnect/controller/dataentries/formatters/SleepSessionFormatter.kt
@@ -24,12 +24,13 @@
 import android.health.connect.datatypes.SleepSessionRecord.StageType.STAGE_TYPE_SLEEPING_REM
 import android.health.connect.datatypes.SleepSessionRecord.StageType.STAGE_TYPE_UNKNOWN
 import com.android.healthconnect.controller.R
-import com.android.healthconnect.controller.dataentries.FormattedEntry
-import com.android.healthconnect.controller.dataentries.FormattedEntry.FormattedSessionDetail
+import com.android.healthconnect.controller.data.entries.FormattedEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry.FormattedSessionDetail
 import com.android.healthconnect.controller.dataentries.formatters.DurationFormatter.formatDurationLong
 import com.android.healthconnect.controller.dataentries.formatters.DurationFormatter.formatDurationShort
 import com.android.healthconnect.controller.dataentries.formatters.shared.BaseFormatter
 import com.android.healthconnect.controller.dataentries.formatters.shared.SessionDetailsFormatter
+import com.android.healthconnect.controller.dataentries.formatters.shared.UnitFormatter
 import com.android.healthconnect.controller.dataentries.units.UnitPreferences
 import com.android.healthconnect.controller.utils.LocalDateTimeFormatter
 import com.google.common.annotations.VisibleForTesting
@@ -39,7 +40,9 @@
 
 /** Formatter for printing SleepSessionRecord data. */
 class SleepSessionFormatter @Inject constructor(@ApplicationContext private val context: Context) :
-    BaseFormatter<SleepSessionRecord>(context), SessionDetailsFormatter<SleepSessionRecord> {
+    BaseFormatter<SleepSessionRecord>(context),
+    SessionDetailsFormatter<SleepSessionRecord>,
+    UnitFormatter<Long> {
 
     private val timeFormatter = LocalDateTimeFormatter(context)
 
@@ -69,6 +72,14 @@
         return formatSleepSession(record) { duration -> formatDurationLong(context, duration) }
     }
 
+    override fun formatA11yUnit(unit: Long): String {
+        return formatDurationLong(context, Duration.ofMillis(unit))
+    }
+
+    override fun formatUnit(unit: Long): String {
+        return formatDurationShort(context, Duration.ofMillis(unit))
+    }
+
     private fun getNotes(record: SleepSessionRecord): String? {
         return record.notes?.toString()
     }
diff --git a/apk/src/com/android/healthconnect/controller/dataentries/formatters/SpeedFormatter.kt b/apk/src/com/android/healthconnect/controller/dataentries/formatters/SpeedFormatter.kt
index b64d170..3ab7205 100644
--- a/apk/src/com/android/healthconnect/controller/dataentries/formatters/SpeedFormatter.kt
+++ b/apk/src/com/android/healthconnect/controller/dataentries/formatters/SpeedFormatter.kt
@@ -21,8 +21,8 @@
 import android.icu.text.MessageFormat
 import androidx.annotation.StringRes
 import com.android.healthconnect.controller.R
-import com.android.healthconnect.controller.dataentries.FormattedEntry
-import com.android.healthconnect.controller.dataentries.FormattedEntry.FormattedSessionDetail
+import com.android.healthconnect.controller.data.entries.FormattedEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry.FormattedSessionDetail
 import com.android.healthconnect.controller.dataentries.formatters.shared.EntryFormatter
 import com.android.healthconnect.controller.dataentries.formatters.shared.SessionDetailsFormatter
 import com.android.healthconnect.controller.dataentries.units.DistanceUnit.KILOMETERS
diff --git a/apk/src/com/android/healthconnect/controller/dataentries/formatters/StepsCadenceFormatter.kt b/apk/src/com/android/healthconnect/controller/dataentries/formatters/StepsCadenceFormatter.kt
index 77d5e91..acb11f1 100644
--- a/apk/src/com/android/healthconnect/controller/dataentries/formatters/StepsCadenceFormatter.kt
+++ b/apk/src/com/android/healthconnect/controller/dataentries/formatters/StepsCadenceFormatter.kt
@@ -21,7 +21,7 @@
 import android.icu.text.MessageFormat
 import androidx.annotation.StringRes
 import com.android.healthconnect.controller.R
-import com.android.healthconnect.controller.dataentries.FormattedEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry
 import com.android.healthconnect.controller.dataentries.formatters.shared.EntryFormatter
 import com.android.healthconnect.controller.dataentries.formatters.shared.SessionDetailsFormatter
 import com.android.healthconnect.controller.dataentries.units.UnitPreferences
diff --git a/apk/src/com/android/healthconnect/controller/dataentries/formatters/shared/BaseFormatter.kt b/apk/src/com/android/healthconnect/controller/dataentries/formatters/shared/BaseFormatter.kt
index 9a61cbc..140f2fb 100644
--- a/apk/src/com/android/healthconnect/controller/dataentries/formatters/shared/BaseFormatter.kt
+++ b/apk/src/com/android/healthconnect/controller/dataentries/formatters/shared/BaseFormatter.kt
@@ -18,7 +18,7 @@
 import android.health.connect.datatypes.IntervalRecord
 import android.health.connect.datatypes.Record
 import com.android.healthconnect.controller.R
-import com.android.healthconnect.controller.dataentries.FormattedEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry
 import com.android.healthconnect.controller.dataentries.units.UnitPreferences
 import com.android.healthconnect.controller.shared.DataType
 import com.android.healthconnect.controller.utils.LocalDateTimeFormatter
@@ -58,11 +58,19 @@
     }
 
     private fun getHeader(record: T, appName: String): String {
-        return context.getString(R.string.data_entry_header, getFormattedTime(record), appName)
+        if (appName == "")
+            return context.getString(
+                R.string.data_entry_header_without_source_app, getFormattedTime(record))
+        return context.getString(
+            R.string.data_entry_header_with_source_app, getFormattedTime(record), appName)
     }
 
     private fun getHeaderA11y(record: T, appName: String): String {
-        return context.getString(R.string.data_entry_header, getFormattedA11yTime(record), appName)
+        if (appName == "")
+            return context.getString(
+                R.string.data_entry_header_without_source_app, getFormattedTime(record))
+        return context.getString(
+            R.string.data_entry_header_with_source_app, getFormattedA11yTime(record), appName)
     }
 
     private fun getFormattedTime(record: T): String {
diff --git a/apk/src/com/android/healthconnect/controller/dataentries/formatters/shared/EntryFormatter.kt b/apk/src/com/android/healthconnect/controller/dataentries/formatters/shared/EntryFormatter.kt
index 4216ac6..dd592a9 100644
--- a/apk/src/com/android/healthconnect/controller/dataentries/formatters/shared/EntryFormatter.kt
+++ b/apk/src/com/android/healthconnect/controller/dataentries/formatters/shared/EntryFormatter.kt
@@ -17,8 +17,8 @@
 
 import android.content.Context
 import android.health.connect.datatypes.Record
-import com.android.healthconnect.controller.dataentries.FormattedEntry
-import com.android.healthconnect.controller.dataentries.FormattedEntry.FormattedDataEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry.FormattedDataEntry
 import com.android.healthconnect.controller.dataentries.units.UnitPreferences
 
 /** Abstract formatter for Records to Formatted Entries. */
diff --git a/apk/src/com/android/healthconnect/controller/dataentries/formatters/shared/Formatter.kt b/apk/src/com/android/healthconnect/controller/dataentries/formatters/shared/Formatter.kt
index fc247bb..cfb6257 100644
--- a/apk/src/com/android/healthconnect/controller/dataentries/formatters/shared/Formatter.kt
+++ b/apk/src/com/android/healthconnect/controller/dataentries/formatters/shared/Formatter.kt
@@ -14,7 +14,7 @@
 package com.android.healthconnect.controller.dataentries.formatters.shared
 
 import android.health.connect.datatypes.Record
-import com.android.healthconnect.controller.dataentries.FormattedEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry
 
 /** Record formatter used by use cases to format Records into Formatted Entries. */
 interface Formatter<T : Record> {
diff --git a/apk/src/com/android/healthconnect/controller/dataentries/formatters/shared/HealthDataEntryDetailsFormatter.kt b/apk/src/com/android/healthconnect/controller/dataentries/formatters/shared/HealthDataEntryDetailsFormatter.kt
index 62d2562..a82daae 100644
--- a/apk/src/com/android/healthconnect/controller/dataentries/formatters/shared/HealthDataEntryDetailsFormatter.kt
+++ b/apk/src/com/android/healthconnect/controller/dataentries/formatters/shared/HealthDataEntryDetailsFormatter.kt
@@ -26,7 +26,7 @@
 import android.health.connect.datatypes.SleepSessionRecord
 import android.health.connect.datatypes.SpeedRecord
 import android.health.connect.datatypes.StepsCadenceRecord
-import com.android.healthconnect.controller.dataentries.FormattedEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry
 import com.android.healthconnect.controller.dataentries.formatters.CyclingPedalingCadenceFormatter
 import com.android.healthconnect.controller.dataentries.formatters.ExerciseSessionFormatter
 import com.android.healthconnect.controller.dataentries.formatters.HeartRateFormatter
diff --git a/apk/src/com/android/healthconnect/controller/dataentries/formatters/shared/HealthDataEntryFormatter.kt b/apk/src/com/android/healthconnect/controller/dataentries/formatters/shared/HealthDataEntryFormatter.kt
index df7fbf6..7b7737d 100644
--- a/apk/src/com/android/healthconnect/controller/dataentries/formatters/shared/HealthDataEntryFormatter.kt
+++ b/apk/src/com/android/healthconnect/controller/dataentries/formatters/shared/HealthDataEntryFormatter.kt
@@ -56,7 +56,7 @@
 import android.health.connect.datatypes.Vo2MaxRecord
 import android.health.connect.datatypes.WeightRecord
 import android.health.connect.datatypes.WheelchairPushesRecord
-import com.android.healthconnect.controller.dataentries.FormattedEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry
 import com.android.healthconnect.controller.dataentries.formatters.ActiveCaloriesBurnedFormatter
 import com.android.healthconnect.controller.dataentries.formatters.BasalBodyTemperatureFormatter
 import com.android.healthconnect.controller.dataentries.formatters.BasalMetabolicRateFormatter
@@ -142,8 +142,8 @@
     private val heartRateVariabilityRmssdFormatter: HeartRateVariabilityRmssdFormatter,
 ) {
 
-    suspend fun format(record: Record): FormattedEntry {
-        val appName = getAppName(record)
+    suspend fun format(record: Record, showDataOrigin: Boolean = true): FormattedEntry {
+        val appName: String = if (showDataOrigin) getAppName(record) else ""
         return when (record) {
             is HeartRateRecord -> heartRateFormatter.format(record, appName)
             is StepsRecord -> stepsFormatter.format(record, appName)
diff --git a/apk/src/com/android/healthconnect/controller/dataentries/formatters/shared/SessionDetailsFormatter.kt b/apk/src/com/android/healthconnect/controller/dataentries/formatters/shared/SessionDetailsFormatter.kt
index 9957aea..455860e 100644
--- a/apk/src/com/android/healthconnect/controller/dataentries/formatters/shared/SessionDetailsFormatter.kt
+++ b/apk/src/com/android/healthconnect/controller/dataentries/formatters/shared/SessionDetailsFormatter.kt
@@ -19,7 +19,7 @@
 package com.android.healthconnect.controller.dataentries.formatters.shared
 
 import android.health.connect.datatypes.Record
-import com.android.healthconnect.controller.dataentries.FormattedEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry
 
 interface SessionDetailsFormatter<T : Record> {
     suspend fun formatRecordDetails(record: T): List<FormattedEntry>
diff --git a/apk/src/com/android/healthconnect/controller/datasources/AddAnAppFragment.kt b/apk/src/com/android/healthconnect/controller/datasources/AddAnAppFragment.kt
new file mode 100644
index 0000000..9628dc9
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/datasources/AddAnAppFragment.kt
@@ -0,0 +1,92 @@
+/*
+ * 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
+
+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
+import com.android.healthconnect.controller.datasources.DataSourcesViewModel.PotentialAppSourcesState
+import com.android.healthconnect.controller.datasources.DataSourcesViewModel.PriorityListState
+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 dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint(HealthPreferenceFragment::class)
+class AddAnAppFragment : Hilt_AddAnAppFragment() {
+
+    private val dataSourcesViewModel: DataSourcesViewModel by activityViewModels()
+    @HealthDataCategoryInt private var category: Int = 0
+
+    private var currentPriority: List<AppMetadata> = listOf()
+
+    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+        super.onCreatePreferences(savedInstanceState, rootKey)
+        setPreferencesFromResource(R.xml.add_an_app_screen, rootKey)
+        if (requireArguments().containsKey(CATEGORY_KEY)) {
+            category = requireArguments().getInt(CATEGORY_KEY)
+        }
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        dataSourcesViewModel.loadData(category)
+        dataSourcesViewModel.dataSourcesInfo.observe(viewLifecycleOwner) { dataSourcesInfoState ->
+            if (dataSourcesInfoState.isLoading()) {
+                setLoading(true)
+            } else if (dataSourcesInfoState.isLoadingFailed()) {
+                setLoading(false)
+                setError(true)
+            } else if (dataSourcesInfoState.isWithData()) {
+                setLoading(false)
+                val currentPriorityList = (dataSourcesInfoState.priorityListState as PriorityListState.WithData).priorityList
+                val potentialAppSources = (dataSourcesInfoState.potentialAppSourcesState as PotentialAppSourcesState.WithData).appSources
+                currentPriorityList.let { currentPriority = it }
+                updateAppsList(potentialAppSources)
+            }
+        }
+    }
+
+    private fun updateAppsList(appSources: List<AppMetadata>) {
+        preferenceScreen.removeAll()
+        appSources
+            .sortedBy { it.appName }
+            .forEach { appMetadata ->
+                preferenceScreen.addPreference(
+                    HealthPreference(requireContext()).also { preference ->
+                        preference.title = appMetadata.appName
+                        preference.icon = appMetadata.icon
+                        preference.setOnPreferenceClickListener {
+                            // add this app to the bottom of the priority list
+                            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
+                            )
+                            true
+                        }
+                    })
+            }
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/datasources/AggregationCardInfo.kt b/apk/src/com/android/healthconnect/controller/datasources/AggregationCardInfo.kt
new file mode 100644
index 0000000..32b8457
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/datasources/AggregationCardInfo.kt
@@ -0,0 +1,26 @@
+/**
+ * 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
+
+import com.android.healthconnect.controller.data.entries.FormattedEntry
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType
+import java.time.Instant
+
+/** A data class holding information displayed on a [AggregationDataCard] */
+data class AggregationCardInfo(
+    val healthPermissionType: HealthPermissionType,
+    val aggregation: FormattedEntry.FormattedAggregation,
+    val startDate: Instant,
+    val endDate: Instant? = null
+)
diff --git a/apk/src/com/android/healthconnect/controller/datasources/DataSourcesFragment.kt b/apk/src/com/android/healthconnect/controller/datasources/DataSourcesFragment.kt
index 4e0cb57..94eafb5 100644
--- a/apk/src/com/android/healthconnect/controller/datasources/DataSourcesFragment.kt
+++ b/apk/src/com/android/healthconnect/controller/datasources/DataSourcesFragment.kt
@@ -1,47 +1,86 @@
+/**
+ * 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
 
 import android.health.connect.HealthDataCategory
 import android.os.Bundle
+import android.view.MenuItem
 import android.view.View
 import android.widget.AdapterView
+import androidx.core.os.bundleOf
 import androidx.fragment.app.activityViewModels
+import androidx.navigation.fragment.findNavController
 import androidx.preference.PreferenceGroup
 import com.android.healthconnect.controller.R
+import com.android.healthconnect.controller.categories.HealthDataCategoriesFragment.Companion.CATEGORY_KEY
+import com.android.healthconnect.controller.datasources.DataSourcesViewModel.AggregationCardsState
+import com.android.healthconnect.controller.datasources.DataSourcesViewModel.PotentialAppSourcesState
+import com.android.healthconnect.controller.datasources.DataSourcesViewModel.PriorityListState
+import com.android.healthconnect.controller.datasources.appsources.AppSourcesAdapter
 import com.android.healthconnect.controller.datasources.appsources.AppSourcesPreference
-import com.android.healthconnect.controller.permissiontypes.HealthPermissionTypesViewModel
 import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.lowercaseTitle
 import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.uppercaseTitle
 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.shared.preference.CardContainerPreference
 import com.android.healthconnect.controller.shared.preference.HeaderPreference
+import com.android.healthconnect.controller.shared.preference.HealthPreference
 import com.android.healthconnect.controller.shared.preference.HealthPreferenceFragment
+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.HealthConnectLogger
+import com.android.healthconnect.controller.utils.setupMenu
+import com.android.healthconnect.controller.utils.setupSharedMenu
 import com.android.settingslib.widget.FooterPreference
 import com.android.settingslib.widget.SettingsSpinnerAdapter
 import com.android.settingslib.widget.SettingsSpinnerPreference
 import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
 
 @AndroidEntryPoint(HealthPreferenceFragment::class)
-class DataSourcesFragment: Hilt_DataSourcesFragment() {
+class DataSourcesFragment :
+    Hilt_DataSourcesFragment(), AppSourcesAdapter.OnAppRemovedFromPriorityListListener {
 
     companion object {
-        private const val DATA_TYPE_SPINNER_PREFERENCE_KEY = "data_type"
         private const val DATA_TOTALS_PREFERENCE_GROUP = "data_totals_group"
+        private const val DATA_TOTALS_PREFERENCE_KEY = "data_totals_preference"
         private const val APP_SOURCES_PREFERENCE_GROUP = "app_sources_group"
         private const val APP_SOURCES_PREFERENCE_KEY = "app_sources"
-        private const val FOOTER_PREFERENCE_KEY = "data_sources_footer"
+        private const val ADD_AN_APP_PREFERENCE_KEY = "add_an_app"
+        private const val NON_EMPTY_FOOTER_PREFERENCE_KEY = "data_sources_footer"
         private const val EMPTY_STATE_HEADER_PREFERENCE_KEY = "empty_state_header"
         private const val EMPTY_STATE_FOOTER_PREFERENCE_KEY = "empty_state_footer"
 
-        private val dataSourcesCategories = arrayListOf(
-                HealthDataCategory.ACTIVITY,
-                HealthDataCategory.SLEEP)
+        private val dataSourcesCategories =
+            arrayListOf(HealthDataCategory.ACTIVITY, HealthDataCategory.SLEEP)
     }
 
-    private val healthPermissionsViewModel: HealthPermissionTypesViewModel by activityViewModels()
+    init {
+        // TODO (b/292270118) update to correct name
+        //        this.setPageName(PageName.MANAGE_DATA_PAGE)
+    }
+
+    @Inject lateinit var logger: HealthConnectLogger
+    @Inject lateinit var appUtils: AppUtils
+
+    private val dataSourcesViewModel: DataSourcesViewModel by activityViewModels()
     private lateinit var spinnerPreference: SettingsSpinnerPreference
     private lateinit var dataSourcesCategoriesStrings: List<String>
     private var currentCategorySelection: @HealthDataCategoryInt Int = HealthDataCategory.ACTIVITY
+    @Inject lateinit var timeSource: TimeSource
 
     private val dataTotalsPreferenceGroup: PreferenceGroup? by lazy {
         preferenceScreen.findPreference(DATA_TOTALS_PREFERENCE_GROUP)
@@ -51,100 +90,284 @@
         preferenceScreen.findPreference(APP_SOURCES_PREFERENCE_GROUP)
     }
 
-    private val footerPreference: FooterPreference? by lazy {
-        preferenceScreen.findPreference(FOOTER_PREFERENCE_KEY)
+    private val nonEmptyFooterPreference: FooterPreference? by lazy {
+        preferenceScreen.findPreference(NON_EMPTY_FOOTER_PREFERENCE_KEY)
+    }
+
+    private val onEditMenuItemSelected: (MenuItem) -> Boolean = { menuItem ->
+        when (menuItem.itemId) {
+            R.id.menu_edit -> {
+                editPriorityList()
+                true
+            }
+            else -> false
+        }
+    }
+
+    private var cardContainerPreference: CardContainerPreference? = null
+
+    override fun onAppRemovedFromPriorityList() {
+        exitEditMode()
     }
 
     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
         super.onCreatePreferences(savedInstanceState, rootKey)
         setPreferencesFromResource(R.xml.data_sources_and_priority_screen, rootKey)
-        dataSourcesCategoriesStrings = dataSourcesCategories.map {
-            category -> getString(category.uppercaseTitle())
-        }
+        dataSourcesCategoriesStrings =
+            dataSourcesCategories.map { category -> getString(category.uppercaseTitle()) }
 
-        spinnerPreference = SettingsSpinnerPreference(context)
-        spinnerPreference.setAdapter(
-                SettingsSpinnerAdapter<String>(context).also {
-                it.addAll(dataSourcesCategoriesStrings)
-            })
-        spinnerPreference.setOnItemSelectedListener(object : AdapterView.OnItemSelectedListener {
-            override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
-                healthPermissionsViewModel.loadData(dataSourcesCategories[position])
-                healthPermissionsViewModel.loadAppsWithData(dataSourcesCategories[position])
-            }
+        setupSpinnerPreference()
+    }
 
-            override fun onNothingSelected(p0: AdapterView<*>?) {}
-        })
-        preferenceScreen.addPreference(spinnerPreference)
+    override fun onResume() {
+        super.onResume()
+        dataSourcesViewModel.loadData(currentCategorySelection)
     }
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
+        val currentStringSelection = spinnerPreference.selectedItem
+        currentCategorySelection =
+            dataSourcesCategories[dataSourcesCategoriesStrings.indexOf(currentStringSelection)]
 
-        val currentStringSelection = spinnerPreference.selectedItem // this gives string
-        currentCategorySelection = dataSourcesCategories[dataSourcesCategoriesStrings.indexOf(currentStringSelection)]
+        dataSourcesViewModel.loadData(currentCategorySelection)
 
-        healthPermissionsViewModel.loadData(currentCategorySelection)
-        healthPermissionsViewModel.loadAppsWithData(currentCategorySelection)
+        dataSourcesViewModel.dataSourcesAndAggregationsInfo.observe(viewLifecycleOwner) {
+            dataSourcesInfo ->
+            if (dataSourcesInfo.isLoading()) {
+                setLoading(true)
+            } else if (dataSourcesInfo.isLoadingFailed()) {
+                setLoading(false)
+                setError(true)
+            } else if (dataSourcesInfo.isWithData()) {
+                setLoading(false)
 
-        healthPermissionsViewModel.priorityList.observe(viewLifecycleOwner) { state ->
-            when (state) {
-            is HealthPermissionTypesViewModel.PriorityListState.Loading -> {
-                appSourcesPreferenceGroup?.removePreferenceRecursively(APP_SOURCES_PREFERENCE_KEY)
-            }
-            is HealthPermissionTypesViewModel.PriorityListState.LoadingFailed -> {}
-            is HealthPermissionTypesViewModel.PriorityListState.WithData -> {
-                updateAppSources(state.priorityList)
+                val priorityList =
+                    (dataSourcesInfo.priorityListState as PriorityListState.WithData).priorityList
+                val potentialAppSources =
+                    (dataSourcesInfo.potentialAppSourcesState as PotentialAppSourcesState.WithData)
+                        .appSources
+                val cardInfos =
+                    (dataSourcesInfo.aggregationCardsState as AggregationCardsState.WithData)
+                        .dataTotals
+
+                if (priorityList.isEmpty() && potentialAppSources.isEmpty()) {
+                    addEmptyState()
+                } else {
+                    updateMenu(priorityList.size > 1)
+                    updateAppSourcesSection(priorityList, potentialAppSources)
+                    updateDataTotalsSection(cardInfos)
+                }
             }
         }
+
+        dataSourcesViewModel.updatedAggregationCardsData.observe(viewLifecycleOwner) {
+            aggregationCardsData ->
+            when (aggregationCardsData) {
+                is AggregationCardsState.Loading -> {
+                    updateAggregations(listOf(), true)
+                }
+                is AggregationCardsState.LoadingFailed -> {
+                    updateDataTotalsSection(listOf())
+                }
+                is AggregationCardsState.WithData -> {
+                    updateAggregations(aggregationCardsData.dataTotals, false)
+                }
+            }
         }
     }
 
-    private fun updateAppSources(appSources: List<AppMetadata>) {
-        val currentStringSelection = spinnerPreference.selectedItem // this gives string
-        currentCategorySelection = dataSourcesCategories[dataSourcesCategoriesStrings.indexOf(currentStringSelection)]
-        if (appSources.isEmpty()) {
-            setupEmptyState(currentCategorySelection)
+    private fun updateMenu(shouldShowEditButton: Boolean) {
+        if (shouldShowEditButton) {
+            setupMenu(R.menu.data_sources, viewLifecycleOwner, logger, onEditMenuItemSelected)
         } else {
-            // Remove empty state
-            preferenceScreen.removePreferenceRecursively(EMPTY_STATE_HEADER_PREFERENCE_KEY)
-            preferenceScreen.removePreferenceRecursively(EMPTY_STATE_FOOTER_PREFERENCE_KEY)
-            setCategoriesVisibility(true)
-            healthPermissionsViewModel.setEditedPriorityList(appSources)
-            appSourcesPreferenceGroup?.addPreference(
-                    AppSourcesPreference(requireContext(),
-                            healthPermissionsViewModel,
-                            currentCategorySelection).also {
-                        it.key = APP_SOURCES_PREFERENCE_KEY
-                    })
+            setupSharedMenu(viewLifecycleOwner, logger)
         }
     }
 
-    private fun setupEmptyState(category: Int) {
-        setCategoriesVisibility(false)
-        preferenceScreen.addPreference(getHeaderPreference())
-        preferenceScreen.addPreference(FooterPreference(context)
-                .also {
-                    it.title = getString(R.string.data_sources_empty_state_footer,
-                            getString(category.lowercaseTitle()))
-                    it.setLearnMoreText(getString(R.string.data_sources_help_link))
-                    it.setLearnMoreAction { DeviceInfoUtilsImpl().openHCGetStartedLink(requireActivity())}
-                    it.key = EMPTY_STATE_FOOTER_PREFERENCE_KEY
-                })
+    private fun editPriorityList() {
+        updateMenu(shouldShowEditButton = false)
+        appSourcesPreferenceGroup?.removePreferenceRecursively(ADD_AN_APP_PREFERENCE_KEY)
+        val appSourcesPreference =
+            preferenceScreen?.findPreference<AppSourcesPreference>(APP_SOURCES_PREFERENCE_KEY)
+        appSourcesPreference?.toggleEditMode(true)
     }
 
-    private fun setCategoriesVisibility(isVisible: Boolean) {
-        dataTotalsPreferenceGroup?.isVisible = isVisible
-        appSourcesPreferenceGroup?.isVisible = isVisible
-        footerPreference?.isVisible = isVisible
+    private fun exitEditMode() {
+        appSourcesPreferenceGroup
+            ?.findPreference<AppSourcesPreference>(APP_SOURCES_PREFERENCE_KEY)
+            ?.toggleEditMode(false)
+        updateMenu(dataSourcesViewModel.getEditedPriorityList().size > 1)
+        updateAddApp(dataSourcesViewModel.getEditedPotentialAppSources().isNotEmpty())
     }
 
-    private fun getHeaderPreference(): HeaderPreference {
+    /** Updates the priority list preference. */
+    private fun updateAppSourcesSection(
+        priorityList: List<AppMetadata>,
+        potentialAppSources: List<AppMetadata>
+    ) {
+        removeEmptyState()
+        appSourcesPreferenceGroup?.isVisible = true
+        appSourcesPreferenceGroup?.removePreferenceRecursively(APP_SOURCES_PREFERENCE_KEY)
 
+        dataSourcesViewModel.setEditedPriorityList(priorityList)
+        appSourcesPreferenceGroup?.addPreference(
+            AppSourcesPreference(
+                    requireContext(),
+                    appUtils,
+                    dataSourcesViewModel,
+                    currentCategorySelection,
+                    this)
+                .also { it.key = APP_SOURCES_PREFERENCE_KEY })
+
+        updateAddApp(potentialAppSources.isNotEmpty())
+        nonEmptyFooterPreference?.isVisible = true
+    }
+
+    /**
+     * Shows the "Add an app" button when there is at least one potential app for the priority list.
+     *
+     * <p> Hides the button in edit mode and when there are no other potential apps for the priority
+     * list.
+     */
+    private fun updateAddApp(shouldShow: Boolean) {
+        appSourcesPreferenceGroup?.removePreferenceRecursively(ADD_AN_APP_PREFERENCE_KEY)
+
+        if (!shouldShow) {
+            return
+        }
+
+        appSourcesPreferenceGroup?.addPreference(
+            HealthPreference(requireContext()).also {
+                it.icon = AttributeResolver.getDrawable(requireContext(), R.attr.addIcon)
+                it.title = getString(R.string.data_sources_add_app)
+                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
+                it.setOnPreferenceClickListener {
+                    findNavController()
+                        .navigate(
+                            R.id.action_dataSourcesFragment_to_addAnAppFragment,
+                            bundleOf(CATEGORY_KEY to currentCategorySelection))
+                    true
+                }
+            })
+    }
+
+    /** Populates the data totals section with aggregation cards if needed. */
+    private fun updateDataTotalsSection(cardInfos: List<AggregationCardInfo>) {
+        dataTotalsPreferenceGroup?.removePreferenceRecursively(DATA_TOTALS_PREFERENCE_KEY)
+        // Do not show data cards when there are no apps on the priority list
+        if (appSourcesPreferenceGroup?.isVisible == false) {
+            return
+        }
+
+        if (cardInfos.isEmpty()) {
+            dataTotalsPreferenceGroup?.isVisible = false
+        } else {
+            dataTotalsPreferenceGroup?.isVisible = true
+            cardContainerPreference =
+                CardContainerPreference(requireContext(), timeSource).also {
+                    it.setAggregationCardInfo(cardInfos)
+                    it.key = DATA_TOTALS_PREFERENCE_KEY
+                }
+            dataTotalsPreferenceGroup?.addPreference(
+                (cardContainerPreference as CardContainerPreference))
+        }
+    }
+
+    /** Updates the aggregation cards after a priority list change. */
+    private fun updateAggregations(cardInfos: List<AggregationCardInfo>, isLoading: Boolean) {
+        if (isLoading) {
+            cardContainerPreference?.setLoading(true)
+        } else {
+            if (cardInfos.isEmpty()) {
+                dataTotalsPreferenceGroup?.isVisible = false
+            } else {
+                dataTotalsPreferenceGroup?.isVisible = true
+                cardContainerPreference?.setLoading(false)
+                cardContainerPreference?.setAggregationCardInfo(cardInfos)
+            }
+        }
+    }
+
+    /**
+     * The empty state of this fragment is represented by:
+     * - no apps with write permissions for this category
+     * - no apps with data for this category
+     */
+    private fun addEmptyState() {
+        removeNonEmptyState()
+        removeEmptyState()
+
+        preferenceScreen.addPreference(getEmptyStateHeaderPreference())
+        preferenceScreen.addPreference(getEmptyStateFooterPreference())
+    }
+
+    private fun removeEmptyState() {
+        preferenceScreen.removePreferenceRecursively(EMPTY_STATE_HEADER_PREFERENCE_KEY)
+        preferenceScreen.removePreferenceRecursively(EMPTY_STATE_FOOTER_PREFERENCE_KEY)
+    }
+
+    private fun removeNonEmptyState() {
+        preferenceScreen.removePreferenceRecursively(APP_SOURCES_PREFERENCE_KEY)
+        preferenceScreen.removePreferenceRecursively(ADD_AN_APP_PREFERENCE_KEY)
+        preferenceScreen.removePreferenceRecursively(DATA_TOTALS_PREFERENCE_KEY)
+
+        // We hide the preference group headers and footer instead of removing them
+        appSourcesPreferenceGroup?.isVisible = false
+        dataTotalsPreferenceGroup?.isVisible = false
+        nonEmptyFooterPreference?.isVisible = false
+    }
+
+    private fun getEmptyStateHeaderPreference(): HeaderPreference {
         return HeaderPreference(requireContext()).also {
-            it.setHeaderText(
-                    getString(R.string.data_sources_empty_state))
+            it.setHeaderText(getString(R.string.data_sources_empty_state))
             it.key = EMPTY_STATE_HEADER_PREFERENCE_KEY
         }
     }
-}
\ No newline at end of file
+
+    private fun getEmptyStateFooterPreference(): FooterPreference {
+        return FooterPreference(context).also {
+            it.title =
+                getString(
+                    R.string.data_sources_empty_state_footer,
+                    getString(currentCategorySelection.lowercaseTitle()))
+            it.setLearnMoreText(getString(R.string.data_sources_help_link))
+            it.setLearnMoreAction { DeviceInfoUtilsImpl().openHCGetStartedLink(requireActivity()) }
+            it.key = EMPTY_STATE_FOOTER_PREFERENCE_KEY
+        }
+    }
+
+    private fun setupSpinnerPreference() {
+        spinnerPreference = SettingsSpinnerPreference(context)
+        spinnerPreference.setAdapter(
+            SettingsSpinnerAdapter<String>(context).also {
+                it.addAll(dataSourcesCategoriesStrings)
+            })
+
+        spinnerPreference.setOnItemSelectedListener(
+            object : AdapterView.OnItemSelectedListener {
+                override fun onItemSelected(
+                    parent: AdapterView<*>?,
+                    view: View?,
+                    position: Int,
+                    id: Long
+                ) {
+                    val currentCategory = dataSourcesCategories[position]
+                    currentCategorySelection = dataSourcesCategories[position]
+
+                    // Reload the data sources information when a new category has been selected
+                    dataSourcesViewModel.loadData(currentCategory)
+                    dataSourcesViewModel.setCurrentSelection(currentCategory)
+                }
+
+                override fun onNothingSelected(p0: AdapterView<*>?) {}
+            })
+
+        spinnerPreference.setSelection(
+            dataSourcesCategories.indexOf(dataSourcesViewModel.getCurrentSelection()))
+
+        preferenceScreen.addPreference(spinnerPreference)
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/datasources/DataSourcesViewModel.kt b/apk/src/com/android/healthconnect/controller/datasources/DataSourcesViewModel.kt
new file mode 100644
index 0000000..32f1984
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/datasources/DataSourcesViewModel.kt
@@ -0,0 +1,330 @@
+/**
+ * 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
+
+import android.health.connect.HealthDataCategory
+import android.util.Log
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MediatorLiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.android.healthconnect.controller.datasources.api.ILoadMostRecentAggregationsUseCase
+import com.android.healthconnect.controller.datasources.api.ILoadPotentialPriorityListUseCase
+import com.android.healthconnect.controller.datasources.api.IUpdatePriorityListUseCase
+import com.android.healthconnect.controller.permissiontypes.api.ILoadPriorityListUseCase
+import com.android.healthconnect.controller.shared.HealthDataCategoryInt
+import com.android.healthconnect.controller.shared.app.AppInfoReader
+import com.android.healthconnect.controller.shared.app.AppMetadata
+import com.android.healthconnect.controller.shared.usecase.UseCaseResults
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.async
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+@HiltViewModel
+class DataSourcesViewModel
+@Inject
+constructor(
+    private val loadDatesWithDataUseCase: ILoadMostRecentAggregationsUseCase,
+    private val loadPotentialAppSourcesUseCase: ILoadPotentialPriorityListUseCase,
+    private val loadPriorityListUseCase: ILoadPriorityListUseCase,
+    private val updatePriorityListUseCase: IUpdatePriorityListUseCase,
+    private val appInfoReader: AppInfoReader
+) : ViewModel() {
+
+    companion object {
+        private const val TAG = "DataSourcesViewModel"
+    }
+
+    private val _aggregationCardsData = MutableLiveData<AggregationCardsState>()
+
+    private val _updatedAggregationCardsData = MutableLiveData<AggregationCardsState>()
+
+    // Used to control the reloading of the aggregation cards after reordering the priority list
+    // To avoid reloading the whole screen when only the cards need updating
+    // TODO (b/305907256) improve flow by observing the aggregationCardsData directly
+    val updatedAggregationCardsData: LiveData<AggregationCardsState>
+        get() = _updatedAggregationCardsData
+
+    private val _potentialAppSources = MutableLiveData<PotentialAppSourcesState>()
+
+    private val _editedPotentialAppSources = MutableLiveData<List<AppMetadata>>()
+
+    private val _currentPriorityList = MutableLiveData<PriorityListState>()
+
+    private val _editedPriorityList = MutableLiveData<List<AppMetadata>>()
+
+    private val _dataSourcesAndAggregationsInfo = MediatorLiveData<DataSourcesAndAggregationsInfo>()
+    val dataSourcesAndAggregationsInfo: LiveData<DataSourcesAndAggregationsInfo>
+        get() = _dataSourcesAndAggregationsInfo
+
+    private val _dataSourcesInfo = MediatorLiveData<DataSourcesInfo>()
+    val dataSourcesInfo: LiveData<DataSourcesInfo>
+        get() = _dataSourcesInfo
+
+    init {
+        _dataSourcesAndAggregationsInfo.addSource(_currentPriorityList) { priorityListState ->
+            if (!priorityListState.shouldObserve) {
+                return@addSource
+            }
+            _dataSourcesAndAggregationsInfo.value =
+                DataSourcesAndAggregationsInfo(
+                    priorityListState = priorityListState,
+                    potentialAppSourcesState = _potentialAppSources.value,
+                    aggregationCardsState = _aggregationCardsData.value)
+        }
+        _dataSourcesAndAggregationsInfo.addSource(_potentialAppSources) { potentialAppSourcesState
+            ->
+            if (!potentialAppSourcesState.shouldObserve) {
+                return@addSource
+            }
+            _dataSourcesAndAggregationsInfo.value =
+                DataSourcesAndAggregationsInfo(
+                    priorityListState = _currentPriorityList.value,
+                    potentialAppSourcesState = potentialAppSourcesState,
+                    aggregationCardsState = _aggregationCardsData.value)
+        }
+        _dataSourcesAndAggregationsInfo.addSource(_aggregationCardsData) { aggregationCardsState ->
+            if (!aggregationCardsState.shouldObserve) {
+                return@addSource
+            }
+            _dataSourcesAndAggregationsInfo.value =
+                DataSourcesAndAggregationsInfo(
+                    priorityListState = _currentPriorityList.value,
+                    potentialAppSourcesState = _potentialAppSources.value,
+                    aggregationCardsState = aggregationCardsState)
+        }
+
+        _dataSourcesInfo.addSource(_currentPriorityList) { priorityListState ->
+            _dataSourcesInfo.value =
+                DataSourcesInfo(
+                    priorityListState = priorityListState,
+                    potentialAppSourcesState = _potentialAppSources.value)
+        }
+
+        _dataSourcesInfo.addSource(_potentialAppSources) { potentialAppSourcesState ->
+            _dataSourcesInfo.value =
+                DataSourcesInfo(
+                    priorityListState = _currentPriorityList.value,
+                    potentialAppSourcesState = potentialAppSourcesState)
+        }
+    }
+
+    private var currentSelection = HealthDataCategory.ACTIVITY
+
+    fun getCurrentSelection(): Int = currentSelection
+
+    fun setCurrentSelection(category: @HealthDataCategoryInt Int) {
+        currentSelection = category
+    }
+
+    fun loadData(category: @HealthDataCategoryInt Int) {
+        loadMostRecentAggregations(category)
+        loadCurrentPriorityList(category)
+        loadPotentialAppSources(category)
+    }
+
+    private fun loadMostRecentAggregations(category: @HealthDataCategoryInt Int) {
+        _aggregationCardsData.postValue(AggregationCardsState.Loading(true))
+        viewModelScope.launch {
+            when (val aggregationInfoResult = loadDatesWithDataUseCase.invoke(category)) {
+                is UseCaseResults.Success -> {
+                    _aggregationCardsData.postValue(
+                        AggregationCardsState.WithData(true, aggregationInfoResult.data))
+                }
+                is UseCaseResults.Failed -> {
+                    Log.e(TAG, "Failed loading dates with data ", aggregationInfoResult.exception)
+                    _aggregationCardsData.postValue(AggregationCardsState.LoadingFailed(true))
+                }
+            }
+        }
+    }
+
+    fun loadPotentialAppSources(
+        category: @HealthDataCategoryInt Int,
+        shouldObserve: Boolean = true
+    ) {
+        _potentialAppSources.postValue(PotentialAppSourcesState.Loading(shouldObserve))
+        viewModelScope.launch {
+            when (val appSourcesResult = loadPotentialAppSourcesUseCase.invoke(category)) {
+                is UseCaseResults.Success -> {
+                    _potentialAppSources.postValue(
+                        PotentialAppSourcesState.WithData(shouldObserve, appSourcesResult.data))
+                }
+                is UseCaseResults.Failed -> {
+                    Log.e(
+                        TAG,
+                        "Failed to load possible priority list candidates",
+                        appSourcesResult.exception)
+                    _potentialAppSources.postValue(
+                        PotentialAppSourcesState.LoadingFailed(shouldObserve))
+                }
+            }
+        }
+    }
+
+    private fun loadCurrentPriorityList(category: @HealthDataCategoryInt Int) {
+        _currentPriorityList.postValue(PriorityListState.Loading(true))
+        viewModelScope.launch {
+            when (val result = loadPriorityListUseCase.invoke(category)) {
+                is UseCaseResults.Success ->
+                    _currentPriorityList.postValue(
+                        if (result.data.isEmpty()) {
+                            PriorityListState.WithData(true, listOf())
+                        } else {
+                            PriorityListState.WithData(true, result.data)
+                        })
+                is UseCaseResults.Failed -> {
+                    Log.e(TAG, "Load error ", result.exception)
+                    _currentPriorityList.postValue(PriorityListState.LoadingFailed(true))
+                }
+            }
+        }
+    }
+
+    fun updatePriorityList(newPriorityList: List<String>, category: @HealthDataCategoryInt Int) {
+        _currentPriorityList.postValue(PriorityListState.Loading(false))
+        viewModelScope.launch {
+            updatePriorityListUseCase.invoke(newPriorityList, category)
+            updateMostRecentAggregations(category)
+            val appMetadataList: List<AppMetadata> =
+                newPriorityList.map { appInfoReader.getAppMetadata(it) }
+            _currentPriorityList.postValue(PriorityListState.WithData(false, appMetadataList))
+        }
+    }
+
+    private fun updateMostRecentAggregations(category: @HealthDataCategoryInt Int) {
+        _aggregationCardsData.postValue(AggregationCardsState.Loading(false))
+        _updatedAggregationCardsData.postValue(AggregationCardsState.Loading(true))
+        viewModelScope.launch {
+            val job = async { loadDatesWithDataUseCase.invoke(category) }
+            delay(1000)
+
+            when (val aggregationInfoResult = job.await()) {
+                is UseCaseResults.Success -> {
+                    _aggregationCardsData.postValue(
+                        AggregationCardsState.WithData(false, aggregationInfoResult.data))
+                    _updatedAggregationCardsData.postValue(
+                        AggregationCardsState.WithData(true, aggregationInfoResult.data))
+                }
+                is UseCaseResults.Failed -> {
+                    Log.e(TAG, "Failed loading dates with data ", aggregationInfoResult.exception)
+                    _aggregationCardsData.postValue(AggregationCardsState.LoadingFailed(false))
+                    _updatedAggregationCardsData.postValue(
+                        AggregationCardsState.LoadingFailed(true))
+                }
+            }
+        }
+    }
+
+    fun setEditedPriorityList(newList: List<AppMetadata>) {
+        _editedPriorityList.value = newList
+    }
+
+    fun setEditedPotentialAppSources(newList: List<AppMetadata>) {
+        _editedPotentialAppSources.value = newList
+    }
+
+    fun getEditedPotentialAppSources(): List<AppMetadata> {
+        return _editedPotentialAppSources.value ?: emptyList()
+    }
+
+    fun getEditedPriorityList(): List<AppMetadata> {
+        return _editedPriorityList.value ?: emptyList()
+    }
+
+    sealed class AggregationCardsState(open val shouldObserve: Boolean) {
+        data class Loading(override val shouldObserve: Boolean) :
+            AggregationCardsState(shouldObserve)
+
+        data class LoadingFailed(override val shouldObserve: Boolean) :
+            AggregationCardsState(shouldObserve)
+
+        data class WithData(
+            override val shouldObserve: Boolean,
+            val dataTotals: List<AggregationCardInfo>
+        ) : AggregationCardsState(shouldObserve)
+    }
+
+    sealed class PotentialAppSourcesState(open val shouldObserve: Boolean) {
+        data class Loading(override val shouldObserve: Boolean) :
+            PotentialAppSourcesState(shouldObserve)
+
+        data class LoadingFailed(override val shouldObserve: Boolean) :
+            PotentialAppSourcesState(shouldObserve)
+
+        data class WithData(
+            override val shouldObserve: Boolean,
+            val appSources: List<AppMetadata>
+        ) : PotentialAppSourcesState(shouldObserve)
+    }
+
+    sealed class PriorityListState(open val shouldObserve: Boolean) {
+        data class Loading(override val shouldObserve: Boolean) : PriorityListState(shouldObserve)
+
+        data class LoadingFailed(override val shouldObserve: Boolean) :
+            PriorityListState(shouldObserve)
+
+        data class WithData(
+            override val shouldObserve: Boolean,
+            val priorityList: List<AppMetadata>
+        ) : PriorityListState(shouldObserve)
+    }
+
+    class DataSourcesInfo(
+        val priorityListState: PriorityListState?,
+        val potentialAppSourcesState: PotentialAppSourcesState?
+    ) {
+        fun isLoading(): Boolean {
+            return priorityListState is PriorityListState.Loading ||
+                potentialAppSourcesState is PotentialAppSourcesState.Loading
+        }
+
+        fun isLoadingFailed(): Boolean {
+            return priorityListState is PriorityListState.LoadingFailed ||
+                potentialAppSourcesState is PotentialAppSourcesState.LoadingFailed
+        }
+
+        fun isWithData(): Boolean {
+            return priorityListState is PriorityListState.WithData &&
+                potentialAppSourcesState is PotentialAppSourcesState.WithData
+        }
+    }
+
+    data class DataSourcesAndAggregationsInfo(
+        val priorityListState: PriorityListState?,
+        val potentialAppSourcesState: PotentialAppSourcesState?,
+        val aggregationCardsState: AggregationCardsState?
+    ) {
+        fun isLoading(): Boolean {
+            return priorityListState is PriorityListState.Loading ||
+                potentialAppSourcesState is PotentialAppSourcesState.Loading ||
+                aggregationCardsState is AggregationCardsState.Loading
+        }
+
+        fun isLoadingFailed(): Boolean {
+            return priorityListState is PriorityListState.LoadingFailed ||
+                potentialAppSourcesState is PotentialAppSourcesState.LoadingFailed ||
+                aggregationCardsState is AggregationCardsState.LoadingFailed
+        }
+
+        fun isWithData(): Boolean {
+            return priorityListState is PriorityListState.WithData &&
+                potentialAppSourcesState is PotentialAppSourcesState.WithData &&
+                aggregationCardsState is AggregationCardsState.WithData
+        }
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/datasources/api/LoadMostRecentAggregationsUseCase.kt b/apk/src/com/android/healthconnect/controller/datasources/api/LoadMostRecentAggregationsUseCase.kt
new file mode 100644
index 0000000..12534aa
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/datasources/api/LoadMostRecentAggregationsUseCase.kt
@@ -0,0 +1,373 @@
+/**
+ * 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 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,
+    @IoDispatcher private val dispatcher: CoroutineDispatcher,
+) : ILoadMostRecentAggregationsUseCase {
+    /** Invoked to provide [AggregationDataCard]s info for Activity and Sleep */
+    override suspend operator fun invoke(
+        healthDataCategory: @HealthDataCategoryInt Int
+    ): UseCaseResults<List<AggregationCardInfo>> =
+        withContext(dispatcher) {
+            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(
+                            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) }
+                    }
+                } 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) }
+                    }
+                }
+
+                UseCaseResults.Success(resultsList.toList())
+            } catch (e: Exception) {
+                UseCaseResults.Failed(e)
+            }
+        }
+
+    private suspend fun getLastAvailableAggregation(
+        datesWithData: List<LocalDate>,
+        healthPermissionType: HealthPermissionType
+    ): AggregationCardInfo? {
+        // Get aggregate for last day
+        val lastDate = datesWithData.maxOf { it }
+        val lastDateInstant = lastDate.atStartOfDay(ZoneId.systemDefault()).toInstant()
+
+        // call for aggregate
+        val input =
+            LoadAggregationInput.PeriodAggregation(
+                permissionType = healthPermissionType,
+                packageName = null,
+                displayedStartTime = lastDateInstant,
+                period = DateNavigationPeriod.PERIOD_DAY,
+                showDataOrigin = false)
+
+        return when (val useCaseResult = loadDataAggregationsUseCase.invoke(input)) {
+            is UseCaseResults.Success -> {
+                // use this aggregation value to construct the card
+                AggregationCardInfo(healthPermissionType, useCaseResult.data, lastDateInstant)
+            }
+            is UseCaseResults.Failed -> {
+                // Something went wrong here, so return nothing
+                null
+            }
+        }
+    }
+
+    private suspend fun getLastAvailableSleepAggregation(
+        datesWithData: List<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)
+        }
+
+        // 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)) {
+            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)
+                }
+            }
+            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)
+            }
+        }
+
+        return Pair(minStartTime, maxEndTime)
+    }
+
+    /**
+     * Returns an [AggregationCardInfo] representing the total sleep time from a list of sleep
+     * sessions starting on a particular day.
+     */
+    private suspend fun computeSleepAggregation(
+        minStartTime: Instant,
+        maxEndTime: Instant
+    ): AggregationCardInfo? {
+        val aggregationInput =
+            LoadAggregationInput.CustomAggregation(
+                permissionType = HealthPermissionType.SLEEP,
+                packageName = null,
+                startTime = minStartTime,
+                endTime = maxEndTime,
+                showDataOrigin = false)
+
+        return when (val useCaseResult = loadDataAggregationsUseCase.invoke(aggregationInput)) {
+            is UseCaseResults.Success -> {
+                // use this aggregation value to construct the card
+                AggregationCardInfo(
+                    HealthPermissionType.SLEEP,
+                    useCaseResult.data,
+                    minStartTime.atStartOfDay(),
+                    maxEndTime.atStartOfDay())
+            }
+            is UseCaseResults.Failed -> {
+                // Something went wrong here, so return nothing
+                null
+            }
+        }
+    }
+}
+
+interface ILoadMostRecentAggregationsUseCase {
+    suspend fun invoke(
+        healthDataCategory: @HealthDataCategoryInt Int
+    ): UseCaseResults<List<AggregationCardInfo>>
+}
diff --git a/apk/src/com/android/healthconnect/controller/datasources/api/LoadPotentialPriorityListUseCase.kt b/apk/src/com/android/healthconnect/controller/datasources/api/LoadPotentialPriorityListUseCase.kt
new file mode 100644
index 0000000..e675dbe
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/datasources/api/LoadPotentialPriorityListUseCase.kt
@@ -0,0 +1,153 @@
+/*
+ * 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.RecordTypeInfoResponse
+import android.health.connect.HealthConnectManager
+import android.health.connect.datatypes.Record
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import com.android.healthconnect.controller.permissions.api.GetGrantedHealthPermissionsUseCase
+import com.android.healthconnect.controller.permissions.data.HealthPermission
+import com.android.healthconnect.controller.permissions.data.PermissionsAccessType
+import com.android.healthconnect.controller.service.IoDispatcher
+import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.healthPermissionTypes
+import com.android.healthconnect.controller.shared.HealthDataCategoryInt
+import com.android.healthconnect.controller.shared.HealthPermissionReader
+import com.android.healthconnect.controller.shared.app.AppInfoReader
+import com.android.healthconnect.controller.shared.app.AppMetadata
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlinx.coroutines.suspendCancellableCoroutine
+import androidx.core.os.asOutcomeReceiver
+import com.android.healthconnect.controller.permissiontypes.api.LoadPriorityListUseCase
+import com.android.healthconnect.controller.shared.usecase.UseCaseResults
+
+@Singleton
+class LoadPotentialPriorityListUseCase
+@Inject
+constructor(
+    private val appInfoReader: AppInfoReader,
+    private val healthConnectManager: HealthConnectManager,
+    private val healthPermissionReader: HealthPermissionReader,
+    private val loadGrantedHealthPermissionsUseCase: GetGrantedHealthPermissionsUseCase,
+    private val loadPriorityListUseCase: LoadPriorityListUseCase,
+    @IoDispatcher private val dispatcher: CoroutineDispatcher
+) : ILoadPotentialPriorityListUseCase {
+
+    private val TAG = "LoadAppSourcesUseCase"
+
+    /**
+     * Returns a list of unique [AppMetadata]s that are potential priority list candidates.
+     */
+    override suspend operator fun invoke(category: @HealthDataCategoryInt Int): UseCaseResults<List<AppMetadata>> =
+        withContext(dispatcher) {
+            val appsWithDataResult = getAppsWithData(category)
+            val appsWithWritePermissionResult = getAppsWithWritePermission(category)
+            val appsOnPriorityListResult = loadPriorityListUseCase.invoke(category)
+
+            // Propagate error if any calls fail
+            if (appsWithDataResult is UseCaseResults.Failed) {
+                UseCaseResults.Failed(appsWithDataResult.exception)
+            } else if (appsWithWritePermissionResult is UseCaseResults.Failed) {
+                UseCaseResults.Failed(appsWithWritePermissionResult.exception)
+            } else if (appsOnPriorityListResult is UseCaseResults.Failed) {
+                UseCaseResults.Failed(appsOnPriorityListResult.exception)
+            } else {
+                val appsWithData = (appsWithDataResult as UseCaseResults.Success).data
+                val appsWithWritePermission = (appsWithWritePermissionResult as UseCaseResults.Success).data
+                val appsOnPriorityList = (appsOnPriorityListResult as UseCaseResults.Success).data
+                    .map { it.packageName }.toSet()
+
+                val potentialPriorityListApps =
+                    appsWithData.union(appsWithWritePermission)
+                        .minus(appsOnPriorityList)
+                        .toList()
+                        .map { appInfoReader.getAppMetadata(it) }
+
+                UseCaseResults.Success(potentialPriorityListApps)
+            }
+
+        }
+
+    /**
+     * Returns a list of unique packageNames that have data in this [HealthDataCategory].
+     */
+    @VisibleForTesting
+    suspend fun getAppsWithData(category: @HealthDataCategoryInt Int): UseCaseResults<Set<String>> =
+        withContext(dispatcher) {
+            try {
+                val recordTypeInfoMap: Map<Class<out Record>, RecordTypeInfoResponse> =
+                    suspendCancellableCoroutine { continuation ->
+                        healthConnectManager.queryAllRecordTypesInfo(
+                            Runnable::run, continuation.asOutcomeReceiver())
+                    }
+                val packages =
+                    recordTypeInfoMap.values
+                        .filter {
+                            it.contributingPackages.isNotEmpty() && it.dataCategory == category
+                        }
+                        .map { it.contributingPackages }
+                        .flatten()
+                UseCaseResults.Success(packages
+                    .map { it.packageName }
+                    .toSet())
+            } catch (e: Exception) {
+                Log.e(TAG, "Failed to get apps with data ", e)
+                UseCaseResults.Failed(e)
+            }
+        }
+
+
+    /** Returns a set of packageNames which have at least one WRITE permission in this [HealthDataCategory] **/
+    @VisibleForTesting
+    suspend fun getAppsWithWritePermission(category: @HealthDataCategoryInt Int): UseCaseResults<Set<String>> =
+        withContext(dispatcher) {
+            try {
+                val writeAppPackageNameSet: MutableSet<String> = mutableSetOf()
+                val appsWithHealthPermissions: List<String> =
+                    healthPermissionReader.getAppsWithHealthPermissions()
+                val healthPermissionsInCategory: List<String> =
+                    category.healthPermissionTypes().map {
+                            healthPermissionType ->
+                        HealthPermission(
+                            healthPermissionType,
+                            PermissionsAccessType.WRITE).toString()
+                    }
+
+                appsWithHealthPermissions.forEach {packageName ->
+                    val permissionsPerPackage: List<String> =
+                        loadGrantedHealthPermissionsUseCase(packageName)
+
+                    // Apps that can WRITE the given HealthDataCategory
+                    if (healthPermissionsInCategory.any { permissionsPerPackage.contains(it) }) {
+                        writeAppPackageNameSet.add(packageName)
+                    }
+                }
+
+                UseCaseResults.Success(writeAppPackageNameSet)
+            } catch (e: Exception) {
+                Log.e(TAG, "Failed to get apps with write permission ", e)
+                UseCaseResults.Failed(e)
+            }
+        }
+}
+
+interface ILoadPotentialPriorityListUseCase {
+    suspend fun invoke(category: @HealthDataCategoryInt Int): UseCaseResults<List<AppMetadata>>
+}
\ No newline at end of file
diff --git a/apk/src/com/android/healthconnect/controller/datasources/api/UpdatePriorityListUseCase.kt b/apk/src/com/android/healthconnect/controller/datasources/api/UpdatePriorityListUseCase.kt
new file mode 100644
index 0000000..5f5e0e3
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/datasources/api/UpdatePriorityListUseCase.kt
@@ -0,0 +1,38 @@
+package com.android.healthconnect.controller.datasources.api
+
+import android.health.connect.HealthConnectManager
+import android.health.connect.HealthDataCategory
+import android.health.connect.UpdateDataOriginPriorityOrderRequest
+import android.health.connect.datatypes.DataOrigin
+import com.android.healthconnect.controller.service.IoDispatcher
+import com.android.healthconnect.controller.shared.HealthDataCategoryInt
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+@Singleton
+class UpdatePriorityListUseCase
+@Inject
+constructor(
+    private val healthConnectManager: HealthConnectManager,
+    @IoDispatcher private val dispatcher: CoroutineDispatcher
+): IUpdatePriorityListUseCase {
+
+    /** Updates the priority list of the stored [DataOrigin]s for given [HealthDataCategory]. */
+    override suspend operator fun invoke(priorityList: List<String>, category: @HealthDataCategoryInt Int) {
+        withContext(dispatcher) {
+            val dataOrigins: List<DataOrigin> =
+                priorityList
+                    .stream()
+                    .map { packageName -> DataOrigin.Builder().setPackageName(packageName).build() }
+                    .toList()
+            healthConnectManager.updateDataOriginPriorityOrder(
+                UpdateDataOriginPriorityOrderRequest(dataOrigins, category), Runnable::run) {}
+        }
+    }
+}
+
+interface IUpdatePriorityListUseCase {
+    suspend fun invoke(priorityList: List<String>, category: @HealthDataCategoryInt Int)
+}
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 fd1684f..c0497c2 100644
--- a/apk/src/com/android/healthconnect/controller/datasources/appsources/AppSourcesAdapter.kt
+++ b/apk/src/com/android/healthconnect/controller/datasources/appsources/AppSourcesAdapter.kt
@@ -1,111 +1,198 @@
+/**
+ * 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.appsources
 
 import android.annotation.SuppressLint
+import android.content.Context
 import android.view.LayoutInflater
 import android.view.MotionEvent
 import android.view.View
 import android.view.ViewGroup
+import android.widget.ImageView
 import android.widget.TextView
 import androidx.recyclerview.widget.ItemTouchHelper
 import androidx.recyclerview.widget.RecyclerView
 import com.android.healthconnect.controller.R
-import com.android.healthconnect.controller.permissiontypes.HealthPermissionTypesViewModel
+import com.android.healthconnect.controller.datasources.DataSourcesViewModel
 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 java.text.NumberFormat
 
 /** RecyclerView adapter that holds the list of app sources for this [HealthDataCategory]. */
 class AppSourcesAdapter(
-        appMetadataList: List<AppMetadata>,
-        private val viewModel: HealthPermissionTypesViewModel,
-        private val category: @HealthDataCategoryInt Int
+    private val context: Context,
+    private val appUtils: AppUtils,
+    priorityList: List<AppMetadata>,
+    potentialAppSourcesList: List<AppMetadata>,
+    private val dataSourcesViewModel: DataSourcesViewModel,
+    private val category: @HealthDataCategoryInt Int,
+    private val onAppRemovedListener: OnAppRemovedFromPriorityListListener,
+    private val itemMoveAttachCallbackListener: ItemMoveAttachCallbackListener,
 ) : RecyclerView.Adapter<AppSourcesAdapter.AppSourcesItemViewHolder?>() {
 
     private var listener: ItemTouchHelper? = null
-    private var appMetadataList = appMetadataList.toMutableList()
+    private var priorityList = priorityList.toMutableList()
+    private var potentialAppSourcesList = potentialAppSourcesList.toMutableList()
+    private var isEditMode = false
 
     private val POSITION_CHANGED_PAYLOAD = Any()
 
-    override fun onCreateViewHolder(
-            viewGroup: ViewGroup,
-            viewType: Int
-    ): AppSourcesItemViewHolder {
+    interface OnAppRemovedFromPriorityListListener {
+        fun onAppRemovedFromPriorityList()
+    }
+
+    /**
+     * Used for re-attaching the onItemMovedCallback to the RecyclerView when we exit the edit mode
+     */
+    interface ItemMoveAttachCallbackListener {
+        fun attachCallback()
+    }
+
+    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): AppSourcesItemViewHolder {
         return AppSourcesItemViewHolder(
-                LayoutInflater.from(viewGroup.context)
-                        .inflate(R.layout.widget_app_source_layout, viewGroup, false),
-                listener)
+            LayoutInflater.from(viewGroup.context)
+                .inflate(R.layout.widget_app_source_layout, viewGroup, false),
+            listener)
     }
 
     override fun onBindViewHolder(viewHolder: AppSourcesItemViewHolder, position: Int) {
-        viewHolder.bind(position, appMetadataList[position].appName, isOnlyApp = appMetadataList.size == 1)
+        viewHolder.bind(position, priorityList[position], isOnlyApp = priorityList.size == 1)
     }
 
     override fun getItemCount(): Int {
-        return appMetadataList.size
+        return priorityList.size
     }
 
     fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
-        val movedAppInfo: AppMetadata = appMetadataList.removeAt(fromPosition)
-        appMetadataList.add(
-                if (toPosition > fromPosition + 1) toPosition - 1 else toPosition, movedAppInfo)
+        val movedAppInfo: AppMetadata = priorityList.removeAt(fromPosition)
+        priorityList.add(
+            if (toPosition > fromPosition + 1) toPosition - 1 else toPosition, movedAppInfo)
         notifyItemMoved(fromPosition, toPosition)
         if (toPosition < fromPosition) {
             notifyItemRangeChanged(
-                    toPosition, fromPosition - toPosition + 1, POSITION_CHANGED_PAYLOAD)
+                toPosition, fromPosition - toPosition + 1, POSITION_CHANGED_PAYLOAD)
         } else {
             notifyItemRangeChanged(
-                    fromPosition, toPosition - fromPosition + 1, POSITION_CHANGED_PAYLOAD)
+                fromPosition, toPosition - fromPosition + 1, POSITION_CHANGED_PAYLOAD)
         }
-        viewModel.setEditedAppSources(appMetadataList, category)
+        dataSourcesViewModel.updatePriorityList(priorityList.map { it.packageName }, category)
         return true
     }
 
-    fun getPackageNameList(): List<String> {
-        return appMetadataList.stream().map(AppMetadata::packageName).toList()
-    }
-
     fun setOnItemDragStartedListener(listener: ItemTouchHelper) {
         this.listener = listener
     }
 
-    fun removeOnItemDragStartedListener() {
+    private fun removeOnItemDragStartedListener() {
         listener = null
     }
 
+    fun toggleEditMode(isEditMode: Boolean) {
+        this.isEditMode = isEditMode
+        if (!isEditMode) {
+            itemMoveAttachCallbackListener.attachCallback()
+        } else {
+            removeOnItemDragStartedListener()
+        }
+        notifyDataSetChanged()
+    }
+
+    fun removeItem(position: Int) {
+        priorityList.removeAt(position)
+        notifyItemRemoved(position)
+    }
+
     /** Shows a single item of the priority list. */
-    class AppSourcesItemViewHolder(itemView: View, onItemDragStartedListener: ItemTouchHelper?) :
-            RecyclerView.ViewHolder(itemView) {
+    inner class AppSourcesItemViewHolder(
+        itemView: View,
+        private val onItemDragStartedListener: ItemTouchHelper?
+    ) : RecyclerView.ViewHolder(itemView) {
         private val appPositionView: TextView
         private val appNameView: TextView
-        private val dragIconView: View
-
-        private val onItemDragStartedListener: ItemTouchHelper?
+        private val appSourceSummary: TextView
+        private val actionView: View
+        private val actionIconBackground: ImageView
 
         init {
             appPositionView = itemView.findViewById(R.id.app_position)
             appNameView = itemView.findViewById(R.id.app_name)
-            dragIconView = itemView.findViewById(R.id.drag_icon)
-            this.onItemDragStartedListener = onItemDragStartedListener
+            actionView = itemView.findViewById(R.id.action_icon)
+            actionIconBackground = itemView.findViewById(R.id.action_icon_background)
+            appSourceSummary = itemView.findViewById(R.id.app_source_summary)
+        }
+
+        fun bind(appPosition: Int, appMetadata: AppMetadata, isOnlyApp: Boolean) {
+            // Adding 1 to position as position starts from 0 but should show to the user starting
+            // from 1.
+            val positionString: String = NumberFormat.getIntegerInstance().format(appPosition + 1)
+            appPositionView.text = positionString
+            appNameView.text = appMetadata.appName
+
+            if (appUtils.isDefaultApp(context, appMetadata.packageName)) {
+                appSourceSummary.visibility = View.VISIBLE
+                appSourceSummary.text = context.getString(R.string.default_app_summary)
+            } else {
+                appSourceSummary.visibility = View.GONE
+            }
+
+            if (isEditMode) {
+                setupItemForEditMode(appPosition)
+            } else {
+                setupItemForDragMode(isOnlyApp)
+            }
+        }
+
+        private fun setupItemForEditMode(appPosition: Int) {
+            actionView.isClickable = true
+            actionView.visibility = View.VISIBLE
+            actionIconBackground.background =
+                AttributeResolver.getDrawable(itemView.context, R.attr.closeIcon)
+            actionView.setOnTouchListener(null)
+            actionView.setOnClickListener {
+                val currentPriority = priorityList.toMutableList()
+                val removedItem = currentPriority.removeAt(appPosition)
+                dataSourcesViewModel.setEditedPriorityList(currentPriority)
+                dataSourcesViewModel.updatePriorityList(
+                    currentPriority.map { it.packageName }, category)
+
+                potentialAppSourcesList.add(removedItem)
+                dataSourcesViewModel.loadPotentialAppSources(category, false)
+                dataSourcesViewModel.setEditedPotentialAppSources(potentialAppSourcesList)
+
+                removeItem(appPosition)
+                onAppRemovedListener.onAppRemovedFromPriorityList()
+            }
         }
 
         // These items are not clickable and so onTouch does not need to reimplement click
         // conditions.
         // Drag&drop in accessibility mode (talk back) is implemented as custom actions.
         @SuppressLint("ClickableViewAccessibility")
-        fun bind(appPosition: Int, appName: String?, isOnlyApp: Boolean) {
-            // Adding 1 to position as position starts from 0 but should show to the user starting
-            // from 1.
-            val positionString: String = NumberFormat.getIntegerInstance().format(appPosition + 1)
-            appPositionView.text = positionString
-            appNameView.text = appName
+        private fun setupItemForDragMode(isOnlyApp: Boolean) {
             // Hide drag icon if this is the only app in the list
             if (isOnlyApp) {
-                dragIconView.visibility = View.INVISIBLE
-                dragIconView.isClickable = false
+                actionView.visibility = View.INVISIBLE
+                actionView.isClickable = false
             } else {
-                dragIconView.setOnTouchListener { _, event ->
+                actionIconBackground.background =
+                    AttributeResolver.getDrawable(itemView.context, R.attr.priorityItemDragIcon)
+                actionView.setOnClickListener(null)
+                actionView.setOnTouchListener { _, event ->
                     if (event.action == MotionEvent.ACTION_DOWN ||
-                            event.action == MotionEvent.ACTION_UP) {
+                        event.action == MotionEvent.ACTION_UP) {
                         onItemDragStartedListener?.startDrag(this)
                     }
                     false
diff --git a/apk/src/com/android/healthconnect/controller/datasources/appsources/AppSourcesItemMoveCallback.kt b/apk/src/com/android/healthconnect/controller/datasources/appsources/AppSourcesItemMoveCallback.kt
index f0d80bd..b7401ce 100644
--- a/apk/src/com/android/healthconnect/controller/datasources/appsources/AppSourcesItemMoveCallback.kt
+++ b/apk/src/com/android/healthconnect/controller/datasources/appsources/AppSourcesItemMoveCallback.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.appsources
 
 import android.graphics.Canvas
diff --git a/apk/src/com/android/healthconnect/controller/datasources/appsources/AppSourcesLinearLayoutManager.kt b/apk/src/com/android/healthconnect/controller/datasources/appsources/AppSourcesLinearLayoutManager.kt
index 7c47ee3..b1a2cf0 100644
--- a/apk/src/com/android/healthconnect/controller/datasources/appsources/AppSourcesLinearLayoutManager.kt
+++ b/apk/src/com/android/healthconnect/controller/datasources/appsources/AppSourcesLinearLayoutManager.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.appsources
 
 import android.content.Context
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 cc26e97..4c7843b 100644
--- a/apk/src/com/android/healthconnect/controller/datasources/appsources/AppSourcesPreference.kt
+++ b/apk/src/com/android/healthconnect/controller/datasources/appsources/AppSourcesPreference.kt
@@ -1,21 +1,44 @@
+/**
+ * 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.appsources
 
 import android.content.Context
-import android.util.Log
 import androidx.preference.Preference
 import androidx.preference.PreferenceViewHolder
 import androidx.recyclerview.widget.ItemTouchHelper
 import androidx.recyclerview.widget.RecyclerView
 import com.android.healthconnect.controller.R
-import com.android.healthconnect.controller.permissiontypes.HealthPermissionTypesViewModel
+import com.android.healthconnect.controller.datasources.DataSourcesFragment
+import com.android.healthconnect.controller.datasources.DataSourcesViewModel
+import com.android.healthconnect.controller.permissions.connectedapps.ComparablePreference
 import com.android.healthconnect.controller.shared.HealthDataCategoryInt
 import com.android.healthconnect.controller.shared.app.AppMetadata
+import com.android.healthconnect.controller.shared.app.AppUtils
 
 class AppSourcesPreference
 constructor(
-        context: Context,
-        val viewModel: HealthPermissionTypesViewModel,
-        val category: @HealthDataCategoryInt Int): Preference(context) {
+    context: Context,
+    private val appUtils: AppUtils,
+    private val dataSourcesViewModel: DataSourcesViewModel,
+    val category: @HealthDataCategoryInt Int,
+    private val fragment: DataSourcesFragment
+) : Preference(context), ComparablePreference, AppSourcesAdapter.ItemMoveAttachCallbackListener {
+
+    private var priorityList: List<AppMetadata> = listOf()
+    private var potentialAppSourcesList: List<AppMetadata> = listOf()
+    private lateinit var priorityListView: RecyclerView
+    private lateinit var adapter: AppSourcesAdapter
 
     init {
         layoutResource = R.layout.widget_linear_layout_preference
@@ -24,16 +47,47 @@
     override fun onBindViewHolder(holder: PreferenceViewHolder) {
         super.onBindViewHolder(holder)
 
-        val priorityList: List<AppMetadata> = viewModel.editedPriorityList.value ?: emptyList()
-        val priorityListView = holder.findViewById(R.id.linear_layout_recycle_view) as RecyclerView
-        val adapter = AppSourcesAdapter(priorityList, viewModel, category)
+        priorityList = dataSourcesViewModel.getEditedPriorityList()
+        potentialAppSourcesList = dataSourcesViewModel.getEditedPotentialAppSources()
+        priorityListView = holder.findViewById(R.id.linear_layout_recycle_view) as RecyclerView
 
-        priorityListView.layoutManager = AppSourcesLinearLayoutManager(context, adapter)
+        adapter =
+            AppSourcesAdapter(
+                context,
+                appUtils,
+                priorityList,
+                potentialAppSourcesList,
+                dataSourcesViewModel,
+                category,
+                fragment,
+                this)
         priorityListView.adapter = adapter
+        priorityListView.layoutManager = AppSourcesLinearLayoutManager(context, adapter)
+        createAndAttachItemMoveCallback()
+    }
+
+    override fun attachCallback() {
+        createAndAttachItemMoveCallback()
+    }
+
+    private fun createAndAttachItemMoveCallback() {
         val callback = AppSourcesItemMoveCallback(adapter)
         val priorityListMover = ItemTouchHelper(callback)
         adapter.setOnItemDragStartedListener(priorityListMover)
         priorityListMover.attachToRecyclerView(priorityListView)
-
     }
-}
\ No newline at end of file
+
+    fun toggleEditMode(isEditMode: Boolean) {
+        adapter.toggleEditMode(isEditMode)
+    }
+
+    override fun isSameItem(preference: Preference): Boolean {
+        return preference is AppSourcesPreference && this == preference
+    }
+
+    override fun hasSameContents(preference: Preference): Boolean {
+        return preference is AppSourcesPreference &&
+            preference.priorityList == this.priorityList &&
+            preference.category == this.category
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/deletion/DeletionAppDataConfirmationDialogFragment.kt b/apk/src/com/android/healthconnect/controller/deletion/DeletionAppDataConfirmationDialogFragment.kt
index 2a02d34..e503c73 100644
--- a/apk/src/com/android/healthconnect/controller/deletion/DeletionAppDataConfirmationDialogFragment.kt
+++ b/apk/src/com/android/healthconnect/controller/deletion/DeletionAppDataConfirmationDialogFragment.kt
@@ -32,6 +32,7 @@
 import javax.inject.Inject
 
 @AndroidEntryPoint(DialogFragment::class)
+@Deprecated("This won't be used once the NEW_INFORMATION_ARCHITECTURE feature is enabled.")
 class DeletionAppDataConfirmationDialogFragment : Hilt_DeletionAppDataConfirmationDialogFragment() {
 
     private val viewModel: DeletionViewModel by activityViewModels()
diff --git a/apk/src/com/android/healthconnect/controller/deletion/DeletionConfirmationDialogFragment.kt b/apk/src/com/android/healthconnect/controller/deletion/DeletionConfirmationDialogFragment.kt
index 19444ce..537fc23 100644
--- a/apk/src/com/android/healthconnect/controller/deletion/DeletionConfirmationDialogFragment.kt
+++ b/apk/src/com/android/healthconnect/controller/deletion/DeletionConfirmationDialogFragment.kt
@@ -34,6 +34,7 @@
  * A deletion {@link DialogFragment} asking confirmation from user for deleting data from from the
  * time range chosen on the previous dialog.
  */
+@Deprecated("This won't be used once the NEW_INFORMATION_ARCHITECTURE feature is enabled.")
 @AndroidEntryPoint(DialogFragment::class)
 class DeletionConfirmationDialogFragment : Hilt_DeletionConfirmationDialogFragment() {
 
diff --git a/apk/src/com/android/healthconnect/controller/deletion/DeletionConstants.kt b/apk/src/com/android/healthconnect/controller/deletion/DeletionConstants.kt
index 4bc9655..33b686f 100644
--- a/apk/src/com/android/healthconnect/controller/deletion/DeletionConstants.kt
+++ b/apk/src/com/android/healthconnect/controller/deletion/DeletionConstants.kt
@@ -16,6 +16,7 @@
 package com.android.healthconnect.controller.deletion
 
 /** Constants used for deletion operations. */
+@Deprecated("This won't be used once the NEW_INFORMATION_ARCHITECTURE feature is enabled.")
 object DeletionConstants {
 
     /** Used for attaching the DeletionFragment. */
diff --git a/apk/src/com/android/healthconnect/controller/deletion/DeletionFragment.kt b/apk/src/com/android/healthconnect/controller/deletion/DeletionFragment.kt
index 33233e3..7aa3227 100644
--- a/apk/src/com/android/healthconnect/controller/deletion/DeletionFragment.kt
+++ b/apk/src/com/android/healthconnect/controller/deletion/DeletionFragment.kt
@@ -51,6 +51,7 @@
  * ```
  * } </pre>
  */
+@Deprecated("This won't be used once the NEW_INFORMATION_ARCHITECTURE feature is enabled.")
 @AndroidEntryPoint(Fragment::class)
 class DeletionFragment : Hilt_DeletionFragment() {
 
diff --git a/apk/src/com/android/healthconnect/controller/deletion/DeletionParameters.kt b/apk/src/com/android/healthconnect/controller/deletion/DeletionParameters.kt
index 18b32a0..20f02dd 100644
--- a/apk/src/com/android/healthconnect/controller/deletion/DeletionParameters.kt
+++ b/apk/src/com/android/healthconnect/controller/deletion/DeletionParameters.kt
@@ -24,6 +24,7 @@
 import java.time.Instant
 
 /** Represents deletion parameters chosen by the user in the deletion dialogs. */
+@Deprecated("This won't be used once the NEW_INFORMATION_ARCHITECTURE feature is enabled.")
 data class DeletionParameters(
     var chosenRange: ChosenRange = ChosenRange.DELETE_RANGE_LAST_24_HOURS,
     val startTimeMs: Long = -1L,
diff --git a/apk/src/com/android/healthconnect/controller/deletion/DeletionType.kt b/apk/src/com/android/healthconnect/controller/deletion/DeletionType.kt
index ec16d2e..1067301 100644
--- a/apk/src/com/android/healthconnect/controller/deletion/DeletionType.kt
+++ b/apk/src/com/android/healthconnect/controller/deletion/DeletionType.kt
@@ -22,6 +22,7 @@
 import com.android.healthconnect.controller.shared.HealthDataCategoryInt
 
 /** Represents the types of deletion that the user can perform. */
+@Deprecated("This won't be used once the NEW_INFORMATION_ARCHITECTURE feature is enabled.")
 sealed class DeletionType : Parcelable {
     class DeletionTypeAllData() : DeletionType() {
 
diff --git a/apk/src/com/android/healthconnect/controller/deletion/DeletionViewModel.kt b/apk/src/com/android/healthconnect/controller/deletion/DeletionViewModel.kt
index 3e7ba03..e670401 100644
--- a/apk/src/com/android/healthconnect/controller/deletion/DeletionViewModel.kt
+++ b/apk/src/com/android/healthconnect/controller/deletion/DeletionViewModel.kt
@@ -32,6 +32,7 @@
 import kotlinx.coroutines.launch
 
 @HiltViewModel
+@Deprecated("This won't be used once the NEW_INFORMATION_ARCHITECTURE feature is enabled.")
 class DeletionViewModel
 @Inject
 constructor(
diff --git a/apk/src/com/android/healthconnect/controller/deletion/FailedDialogFragment.kt b/apk/src/com/android/healthconnect/controller/deletion/FailedDialogFragment.kt
index 86e6792..4ddbccb 100644
--- a/apk/src/com/android/healthconnect/controller/deletion/FailedDialogFragment.kt
+++ b/apk/src/com/android/healthconnect/controller/deletion/FailedDialogFragment.kt
@@ -26,6 +26,7 @@
 import dagger.hilt.android.AndroidEntryPoint
 
 /** A deletion {@link DialogFragment} notifying user about a failed deletion. */
+@Deprecated("This won't be used once the NEW_INFORMATION_ARCHITECTURE feature is enabled.")
 @AndroidEntryPoint(DialogFragment::class)
 class FailedDialogFragment : Hilt_FailedDialogFragment() {
 
diff --git a/apk/src/com/android/healthconnect/controller/deletion/SuccessDialogFragment.kt b/apk/src/com/android/healthconnect/controller/deletion/SuccessDialogFragment.kt
index 300c5e9..cf6e232 100644
--- a/apk/src/com/android/healthconnect/controller/deletion/SuccessDialogFragment.kt
+++ b/apk/src/com/android/healthconnect/controller/deletion/SuccessDialogFragment.kt
@@ -24,6 +24,7 @@
 import dagger.hilt.android.AndroidEntryPoint
 
 /** A deletion {@link DialogFragment} notifying user about a successful deletion. */
+@Deprecated("This won't be used once the NEW_INFORMATION_ARCHITECTURE feature is enabled.")
 @AndroidEntryPoint(DialogFragment::class)
 class SuccessDialogFragment : Hilt_SuccessDialogFragment() {
 
diff --git a/apk/src/com/android/healthconnect/controller/deletion/TimeRangeDialogFragment.kt b/apk/src/com/android/healthconnect/controller/deletion/TimeRangeDialogFragment.kt
index 36eb24e..0fae2e9 100644
--- a/apk/src/com/android/healthconnect/controller/deletion/TimeRangeDialogFragment.kt
+++ b/apk/src/com/android/healthconnect/controller/deletion/TimeRangeDialogFragment.kt
@@ -35,6 +35,7 @@
 import javax.inject.Inject
 
 /** A {@link DialogFragment} for choosing the deletion time range. */
+@Deprecated("This won't be used once the NEW_INFORMATION_ARCHITECTURE feature is enabled.")
 @AndroidEntryPoint(DialogFragment::class)
 class TimeRangeDialogFragment : Hilt_TimeRangeDialogFragment() {
 
diff --git a/apk/src/com/android/healthconnect/controller/deletion/api/DeleteAllDataUseCase.kt b/apk/src/com/android/healthconnect/controller/deletion/api/DeleteAllDataUseCase.kt
index a6fd222..fb617a0 100644
--- a/apk/src/com/android/healthconnect/controller/deletion/api/DeleteAllDataUseCase.kt
+++ b/apk/src/com/android/healthconnect/controller/deletion/api/DeleteAllDataUseCase.kt
@@ -1,5 +1,5 @@
-/**
- * Copyright (C) 2022 The Android Open Source Project
+/*
+ * 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
@@ -24,6 +24,8 @@
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.withContext
 
+/** Use case to delete all records stored in Health Connect without any filter. */
+@Deprecated("This won't be used once the NEW_INFORMATION_ARCHITECTURE feature is enabled.")
 @Singleton
 class DeleteAllDataUseCase
 @Inject
diff --git a/apk/src/com/android/healthconnect/controller/deletion/api/DeleteAppDataUseCase.kt b/apk/src/com/android/healthconnect/controller/deletion/api/DeleteAppDataUseCase.kt
index 6847082..9599a49 100644
--- a/apk/src/com/android/healthconnect/controller/deletion/api/DeleteAppDataUseCase.kt
+++ b/apk/src/com/android/healthconnect/controller/deletion/api/DeleteAppDataUseCase.kt
@@ -27,6 +27,7 @@
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.withContext
 
+@Deprecated("This won't be used once the NEW_INFORMATION_ARCHITECTURE feature is enabled.")
 @Singleton
 class DeleteAppDataUseCase
 @Inject
diff --git a/apk/src/com/android/healthconnect/controller/deletion/api/DeleteCategoryUseCase.kt b/apk/src/com/android/healthconnect/controller/deletion/api/DeleteCategoryUseCase.kt
index 872e643..77eca94 100644
--- a/apk/src/com/android/healthconnect/controller/deletion/api/DeleteCategoryUseCase.kt
+++ b/apk/src/com/android/healthconnect/controller/deletion/api/DeleteCategoryUseCase.kt
@@ -29,6 +29,7 @@
 import kotlinx.coroutines.withContext
 
 @Singleton
+@Deprecated("This won't be used once the NEW_INFORMATION_ARCHITECTURE feature is enabled.")
 class DeleteCategoryUseCase
 @Inject
 constructor(
diff --git a/apk/src/com/android/healthconnect/controller/deletion/api/DeleteEntryUseCase.kt b/apk/src/com/android/healthconnect/controller/deletion/api/DeleteEntryUseCase.kt
index 11eed89..9f40103 100644
--- a/apk/src/com/android/healthconnect/controller/deletion/api/DeleteEntryUseCase.kt
+++ b/apk/src/com/android/healthconnect/controller/deletion/api/DeleteEntryUseCase.kt
@@ -25,6 +25,7 @@
 import kotlinx.coroutines.withContext
 
 @Singleton
+@Deprecated("This won't be used once the NEW_INFORMATION_ARCHITECTURE feature is enabled.")
 class DeleteEntryUseCase
 @Inject
 constructor(
diff --git a/apk/src/com/android/healthconnect/controller/deletion/api/DeletePermissionTypeUseCase.kt b/apk/src/com/android/healthconnect/controller/deletion/api/DeletePermissionTypeUseCase.kt
index 60b0358..5ceb42a 100644
--- a/apk/src/com/android/healthconnect/controller/deletion/api/DeletePermissionTypeUseCase.kt
+++ b/apk/src/com/android/healthconnect/controller/deletion/api/DeletePermissionTypeUseCase.kt
@@ -27,6 +27,7 @@
 import kotlinx.coroutines.withContext
 
 @Singleton
+@Deprecated("This won't be used once the NEW_INFORMATION_ARCHITECTURE feature is enabled.")
 class DeletePermissionTypeUseCase
 @Inject
 constructor(
diff --git a/apk/src/com/android/healthconnect/controller/entrydetails/DataEntryDetailsFragment.kt b/apk/src/com/android/healthconnect/controller/entrydetails/DataEntryDetailsFragment.kt
index 8ea9594..2cad1d4 100644
--- a/apk/src/com/android/healthconnect/controller/entrydetails/DataEntryDetailsFragment.kt
+++ b/apk/src/com/android/healthconnect/controller/entrydetails/DataEntryDetailsFragment.kt
@@ -25,12 +25,12 @@
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
 import com.android.healthconnect.controller.R
+import com.android.healthconnect.controller.data.entries.FormattedEntry.ExerciseSessionEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry.FormattedSessionDetail
+import com.android.healthconnect.controller.data.entries.FormattedEntry.SeriesDataEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry.SessionHeader
+import com.android.healthconnect.controller.data.entries.FormattedEntry.SleepSessionEntry
 import com.android.healthconnect.controller.dataentries.ExerciseSessionItemViewBinder
-import com.android.healthconnect.controller.dataentries.FormattedEntry.ExerciseSessionEntry
-import com.android.healthconnect.controller.dataentries.FormattedEntry.FormattedSessionDetail
-import com.android.healthconnect.controller.dataentries.FormattedEntry.SeriesDataEntry
-import com.android.healthconnect.controller.dataentries.FormattedEntry.SessionHeader
-import com.android.healthconnect.controller.dataentries.FormattedEntry.SleepSessionEntry
 import com.android.healthconnect.controller.dataentries.OnDeleteEntryListener
 import com.android.healthconnect.controller.dataentries.SeriesDataItemViewBinder
 import com.android.healthconnect.controller.dataentries.SleepSessionItemViewBinder
@@ -60,11 +60,20 @@
 
     companion object {
         private const val ENTRY_ID_KEY = "entry_id_key"
+        const val SHOW_DATA_ORIGIN_KEY = "show_data_origin_key"
 
-        fun createBundle(permissionType: HealthPermissionType, entryId: String): Bundle {
-            return bundleOf(PERMISSION_TYPE_KEY to permissionType, ENTRY_ID_KEY to entryId)
+        fun createBundle(
+            permissionType: HealthPermissionType,
+            entryId: String,
+            showDataOrigin: Boolean
+        ): Bundle {
+            return bundleOf(
+                PERMISSION_TYPE_KEY to permissionType,
+                ENTRY_ID_KEY to entryId,
+                SHOW_DATA_ORIGIN_KEY to showDataOrigin)
         }
     }
+
     @Inject lateinit var logger: HealthConnectLogger
 
     private val viewModel: DataEntryDetailsViewModel by viewModels()
@@ -75,6 +84,7 @@
     private lateinit var loadingView: View
     private lateinit var errorView: View
     private lateinit var detailsAdapter: RecyclerViewAdapter
+    private var showDataOrigin: Boolean = false
     private val onDeleteEntryListener by lazy {
         object : OnDeleteEntryListener {
             override fun onDeleteEntry(
@@ -137,6 +147,10 @@
         entryId =
             requireArguments().getString(ENTRY_ID_KEY)
                 ?: throw IllegalArgumentException("ENTRY_ID_KEY can't be null!")
+
+        showDataOrigin =
+            requireArguments().getBoolean(SHOW_DATA_ORIGIN_KEY)
+                ?: throw IllegalArgumentException("SHOW_DATA_ORIGIN_KEY can't be null!")
         errorView = view.findViewById(R.id.error_view)
         loadingView = view.findViewById(R.id.loading)
         detailsAdapter =
@@ -152,7 +166,7 @@
                 layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
                 adapter = detailsAdapter
             }
-        viewModel.loadEntryData(permissionType, entryId)
+        viewModel.loadEntryData(permissionType, entryId, showDataOrigin)
         setupMenu(R.menu.set_data_units_with_send_feedback_and_help, viewLifecycleOwner, logger) {
             menuItem ->
             when (menuItem.itemId) {
diff --git a/apk/src/com/android/healthconnect/controller/entrydetails/DataEntryDetailsViewModel.kt b/apk/src/com/android/healthconnect/controller/entrydetails/DataEntryDetailsViewModel.kt
index 5ccdfae..6141669 100644
--- a/apk/src/com/android/healthconnect/controller/entrydetails/DataEntryDetailsViewModel.kt
+++ b/apk/src/com/android/healthconnect/controller/entrydetails/DataEntryDetailsViewModel.kt
@@ -18,7 +18,7 @@
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
-import com.android.healthconnect.controller.dataentries.FormattedEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry
 import com.android.healthconnect.controller.permissions.data.HealthPermissionType
 import com.android.healthconnect.controller.shared.usecase.UseCaseResults
 import dagger.hilt.android.lifecycle.HiltViewModel
@@ -38,10 +38,15 @@
     val sessionData: LiveData<DateEntryFragmentState>
         get() = _sessionData
 
-    fun loadEntryData(permissionType: HealthPermissionType, entryId: String) {
+    fun loadEntryData(
+        permissionType: HealthPermissionType,
+        entryId: String,
+        showDataOrigin: Boolean
+    ) {
         viewModelScope.launch {
             val response =
-                loadEntryDetailsUseCase.invoke(LoadDataEntryInput(permissionType, entryId))
+                loadEntryDetailsUseCase.invoke(
+                    LoadDataEntryInput(permissionType, entryId, showDataOrigin))
             when (response) {
                 is UseCaseResults.Success -> {
                     _sessionData.postValue(DateEntryFragmentState.WithData(response.data))
@@ -56,7 +61,9 @@
 
     sealed class DateEntryFragmentState {
         object Loading : DateEntryFragmentState()
+
         object LoadingFailed : DateEntryFragmentState()
+
         data class WithData(val data: List<FormattedEntry>) : DateEntryFragmentState()
     }
 }
diff --git a/apk/src/com/android/healthconnect/controller/entrydetails/LoadEntryDetailsUseCase.kt b/apk/src/com/android/healthconnect/controller/entrydetails/LoadEntryDetailsUseCase.kt
index 8a54db8..270c5a0 100644
--- a/apk/src/com/android/healthconnect/controller/entrydetails/LoadEntryDetailsUseCase.kt
+++ b/apk/src/com/android/healthconnect/controller/entrydetails/LoadEntryDetailsUseCase.kt
@@ -18,7 +18,7 @@
 import android.health.connect.ReadRecordsResponse
 import android.health.connect.datatypes.Record
 import androidx.core.os.asOutcomeReceiver
-import com.android.healthconnect.controller.dataentries.FormattedEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry
 import com.android.healthconnect.controller.dataentries.formatters.shared.HealthDataEntryDetailsFormatter
 import com.android.healthconnect.controller.dataentries.formatters.shared.HealthDataEntryFormatter
 import com.android.healthconnect.controller.permissions.data.HealthPermissionType
@@ -46,7 +46,7 @@
         } else if (results.size > 1) {
             throw IllegalStateException("Multiple records found with the same id: ${input.entryId}")
         } else {
-            formatEntry(results[0])
+            formatEntry(results[0], input.showDataOrigin)
         }
     }
 
@@ -60,14 +60,18 @@
         return response.records.orEmpty()
     }
 
-    private suspend fun formatEntry(record: Record): List<FormattedEntry> {
+    private suspend fun formatEntry(record: Record, showDataOrigin: Boolean): List<FormattedEntry> {
         return buildList {
             // header
-            add(entryFormatter.format(record))
+            add(entryFormatter.format(record, showDataOrigin))
             // details
             addAll(entryDetailsFormatter.formatDetails(record))
         }
     }
 }
 
-data class LoadDataEntryInput(val permissionType: HealthPermissionType, val entryId: String)
+data class LoadDataEntryInput(
+    val permissionType: HealthPermissionType,
+    val entryId: String,
+    val showDataOrigin: Boolean
+)
diff --git a/apk/src/com/android/healthconnect/controller/entrydetails/SessionDetailViewBinder.kt b/apk/src/com/android/healthconnect/controller/entrydetails/SessionDetailViewBinder.kt
index ab7fa11..01d7d76 100644
--- a/apk/src/com/android/healthconnect/controller/entrydetails/SessionDetailViewBinder.kt
+++ b/apk/src/com/android/healthconnect/controller/entrydetails/SessionDetailViewBinder.kt
@@ -23,7 +23,7 @@
 import android.view.ViewGroup
 import android.widget.TextView
 import com.android.healthconnect.controller.R
-import com.android.healthconnect.controller.dataentries.FormattedEntry.FormattedSessionDetail
+import com.android.healthconnect.controller.data.entries.FormattedEntry.FormattedSessionDetail
 import com.android.healthconnect.controller.shared.recyclerview.ViewBinder
 import com.android.healthconnect.controller.utils.logging.EntryDetailsElement
 import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
diff --git a/apk/src/com/android/healthconnect/controller/entrydetails/SessionHeaderViewBinder.kt b/apk/src/com/android/healthconnect/controller/entrydetails/SessionHeaderViewBinder.kt
index 4f576bd..740c72f 100644
--- a/apk/src/com/android/healthconnect/controller/entrydetails/SessionHeaderViewBinder.kt
+++ b/apk/src/com/android/healthconnect/controller/entrydetails/SessionHeaderViewBinder.kt
@@ -23,7 +23,7 @@
 import android.view.ViewGroup
 import android.widget.TextView
 import com.android.healthconnect.controller.R
-import com.android.healthconnect.controller.dataentries.FormattedEntry.SessionHeader
+import com.android.healthconnect.controller.data.entries.FormattedEntry.SessionHeader
 import com.android.healthconnect.controller.shared.recyclerview.ViewBinder
 import com.android.healthconnect.controller.utils.logging.EntryDetailsElement
 import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
diff --git a/apk/src/com/android/healthconnect/controller/managedata/ManageDataFragment.kt b/apk/src/com/android/healthconnect/controller/managedata/ManageDataFragment.kt
index 892c64b..d47abd8 100644
--- a/apk/src/com/android/healthconnect/controller/managedata/ManageDataFragment.kt
+++ b/apk/src/com/android/healthconnect/controller/managedata/ManageDataFragment.kt
@@ -10,7 +10,6 @@
 import com.android.healthconnect.controller.autodelete.AutoDeleteViewModel
 import com.android.healthconnect.controller.shared.preference.HealthPreference
 import com.android.healthconnect.controller.shared.preference.HealthPreferenceFragment
-import com.android.healthconnect.controller.utils.AttributeResolver
 import com.android.healthconnect.controller.utils.FeatureUtils
 import com.android.healthconnect.controller.utils.logging.ManageDataElement
 import com.android.healthconnect.controller.utils.logging.PageName
diff --git a/apk/src/com/android/healthconnect/controller/migration/MigrationViewModel.kt b/apk/src/com/android/healthconnect/controller/migration/MigrationViewModel.kt
index 6388512..0d10032 100644
--- a/apk/src/com/android/healthconnect/controller/migration/MigrationViewModel.kt
+++ b/apk/src/com/android/healthconnect/controller/migration/MigrationViewModel.kt
@@ -24,6 +24,7 @@
 import dagger.hilt.android.lifecycle.HiltViewModel
 import javax.inject.Inject
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
 
 @HiltViewModel
 class MigrationViewModel
@@ -40,15 +41,15 @@
         loadHealthConnectMigrationUiState()
     }
 
-    fun loadHealthConnectMigrationUiState() {
+    private fun loadHealthConnectMigrationUiState() {
         viewModelScope.launch {
             _migrationState.postValue(
                 MigrationFragmentState.WithData(loadMigrationStateUseCase.invoke()))
         }
     }
 
-    suspend fun getCurrentMigrationUiState(): MigrationState {
-        return loadMigrationStateUseCase.invoke()
+    fun getCurrentMigrationUiState(): MigrationState {
+        return runBlocking { loadMigrationStateUseCase.invoke() }
     }
 
     sealed class MigrationFragmentState {
diff --git a/apk/src/com/android/healthconnect/controller/permissions/api/GetHealthPermissionsFlagsUseCase.kt b/apk/src/com/android/healthconnect/controller/permissions/api/GetHealthPermissionsFlagsUseCase.kt
new file mode 100644
index 0000000..7f51ecb
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/permissions/api/GetHealthPermissionsFlagsUseCase.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.permissions.api
+
+import android.util.Log
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Use case to get health permissions flags for an app. */
+@Singleton
+class GetHealthPermissionsFlagsUseCase
+@Inject
+constructor(private val healthPermissionManager: HealthPermissionManager) {
+
+    companion object {
+        private const val TAG = "GetHealthPermissionsFla"
+    }
+
+    operator fun invoke(packageName: String, permissions: List<String>): Map<String, Int> {
+        return try {
+            healthPermissionManager.getHealthPermissionsFlags(packageName, permissions)
+        } catch (e: Exception) {
+            Log.e(TAG, "GetHealthPermissionsFlagsUseCase.invoke", e)
+            emptyMap()
+        }
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/permissions/api/HealthPermissionManager.kt b/apk/src/com/android/healthconnect/controller/permissions/api/HealthPermissionManager.kt
index 9e92fb2..f067421 100644
--- a/apk/src/com/android/healthconnect/controller/permissions/api/HealthPermissionManager.kt
+++ b/apk/src/com/android/healthconnect/controller/permissions/api/HealthPermissionManager.kt
@@ -21,8 +21,16 @@
 /** Wrapper for HealthConnectManager permission apis. */
 interface HealthPermissionManager {
     fun getGrantedHealthPermissions(packageName: String): List<String>
+
+    fun getHealthPermissionsFlags(packageName: String, permissions: List<String>): Map<String, Int>
+
+    fun makeHealthPermissionsRequestable(packageName: String, permissions: List<String>)
+
     fun grantHealthPermission(packageName: String, permissionName: String)
+
     fun revokeHealthPermission(packageName: String, permissionName: String)
+
     fun revokeAllHealthPermissions(packageName: String)
+
     fun loadStartAccessDate(packageName: String?): Instant?
 }
diff --git a/apk/src/com/android/healthconnect/controller/permissions/api/HealthPermissionManagerImpl.kt b/apk/src/com/android/healthconnect/controller/permissions/api/HealthPermissionManagerImpl.kt
index 9271e81..962fcd8 100644
--- a/apk/src/com/android/healthconnect/controller/permissions/api/HealthPermissionManagerImpl.kt
+++ b/apk/src/com/android/healthconnect/controller/permissions/api/HealthPermissionManagerImpl.kt
@@ -26,6 +26,17 @@
         return manager.getGrantedHealthPermissions(packageName)
     }
 
+    override fun getHealthPermissionsFlags(
+        packageName: String,
+        permissions: List<String>
+    ): Map<String, Int> {
+        return manager.getHealthPermissionsFlags(packageName, permissions)
+    }
+
+    override fun makeHealthPermissionsRequestable(packageName: String, permissions: List<String>) {
+        manager.makeHealthPermissionsRequestable(packageName, permissions)
+    }
+
     override fun grantHealthPermission(packageName: String, permissionName: String) {
         manager.grantHealthPermission(packageName, permissionName)
     }
diff --git a/apk/src/com/android/healthconnect/controller/permissions/api/MakeHealthPermissionsRequestableUseCase.kt b/apk/src/com/android/healthconnect/controller/permissions/api/MakeHealthPermissionsRequestableUseCase.kt
new file mode 100644
index 0000000..079aa5a
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/permissions/api/MakeHealthPermissionsRequestableUseCase.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.permissions.api
+
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Use case to reset health permissions flags for an app. */
+@Singleton
+class MakeHealthPermissionsRequestableUseCase
+@Inject
+constructor(private val healthPermissionManager: HealthPermissionManager) {
+    operator fun invoke(packageName: String, permissions: List<String>) {
+        healthPermissionManager.makeHealthPermissionsRequestable(packageName, permissions)
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/permissions/app/AppPermissionViewModel.kt b/apk/src/com/android/healthconnect/controller/permissions/app/AppPermissionViewModel.kt
index 331fa02..7ef6744 100644
--- a/apk/src/com/android/healthconnect/controller/permissions/app/AppPermissionViewModel.kt
+++ b/apk/src/com/android/healthconnect/controller/permissions/app/AppPermissionViewModel.kt
@@ -104,19 +104,16 @@
     private var _permissionsStatus: List<HealthPermissionStatus> = listOf()
 
     /**
-     * Flag to prevent {@link SettingManageAppPermissionsFragment} from reloading
-     * the granted permissions on orientation change
+     * Flag to prevent {@link SettingManageAppPermissionsFragment} from reloading the granted
+     * permissions on orientation change
      */
     private var shouldLoadGrantedPermissions = true
 
-    /**
-     * True if the package is supported or if it has any permissions granted
-     */
+    /** True if the package is supported or if it has any permissions granted */
     private val _shouldNavigateToFragment = MutableLiveData(false)
     val shouldNavigateToFragment: LiveData<Boolean>
         get() = _shouldNavigateToFragment
 
-
     fun loadForPackage(packageName: String) {
         if (!isPackageSupported(packageName)) {
             loadGrantedPermissionsForPackage(packageName)
@@ -140,18 +137,20 @@
             viewModelScope.launch {
                 _appInfo.postValue(appInfoReader.getAppMetadata(packageName))
 
-                _permissionsStatus = loadGrantedHealthPermissionsUseCase.invoke(packageName).map {
-                        permission -> HealthPermissionStatus(HealthPermission.fromPermissionString(permission), true)
-                }
+                _permissionsStatus =
+                    loadGrantedHealthPermissionsUseCase.invoke(packageName).map { permission ->
+                        HealthPermissionStatus(
+                            HealthPermission.fromPermissionString(permission), true)
+                    }
                 // Only show app permissions that are granted
-                _appPermissions.postValue(_permissionsStatus.filter { it.isGranted }.map { it.healthPermission })
+                _appPermissions.postValue(
+                    _permissionsStatus.filter { it.isGranted }.map { it.healthPermission })
                 _allAppPermissionsGranted.postValue(_permissionsStatus.all { it.isGranted })
                 _atLeastOnePermissionGranted.postValue(_permissionsStatus.any { it.isGranted })
                 _grantedPermissions.postValue(
                     _permissionsStatus.filter { it.isGranted }.map { it.healthPermission }.toSet())
             }
             shouldLoadGrantedPermissions = false
-
         }
     }
 
@@ -241,27 +240,30 @@
     fun loadShouldNavigateToFragment(packageName: String) {
         viewModelScope.launch {
             val anyPermissionsGranted =
-                loadGrantedHealthPermissionsUseCase.invoke(packageName)
-                    .map {
-                        permission -> HealthPermissionStatus(HealthPermission.fromPermissionString(permission),
-                        true) }
+                loadGrantedHealthPermissionsUseCase
+                    .invoke(packageName)
+                    .map { permission ->
+                        HealthPermissionStatus(
+                            HealthPermission.fromPermissionString(permission), true)
+                    }
                     .filter { it.isGranted }
                     .map { it.healthPermission }
                     .isNotEmpty()
-            _shouldNavigateToFragment.value = anyPermissionsGranted || isPackageSupported(packageName)
+            _shouldNavigateToFragment.value =
+                anyPermissionsGranted || isPackageSupported(packageName)
         }
     }
 
-    /**
-     * Returns True if the packageName declares the Rationale intent, False otherwise
-     */
+    /** Returns True if the packageName declares the Rationale intent, False otherwise */
     fun isPackageSupported(packageName: String): Boolean {
         return healthPermissionReader.isRationalIntentDeclared(packageName)
     }
 
     sealed class RevokeAllState {
         object NotStarted : RevokeAllState()
+
         object Loading : RevokeAllState()
+
         object Updated : RevokeAllState()
     }
 }
diff --git a/apk/src/com/android/healthconnect/controller/permissions/app/ConnectedAppFragment.kt b/apk/src/com/android/healthconnect/controller/permissions/app/ConnectedAppFragment.kt
index a17f9b1..d380c53 100644
--- a/apk/src/com/android/healthconnect/controller/permissions/app/ConnectedAppFragment.kt
+++ b/apk/src/com/android/healthconnect/controller/permissions/app/ConnectedAppFragment.kt
@@ -39,6 +39,7 @@
 import androidx.fragment.app.activityViewModels
 import androidx.fragment.app.commitNow
 import androidx.fragment.app.viewModels
+import androidx.navigation.fragment.findNavController
 import androidx.preference.PreferenceGroup
 import com.android.healthconnect.controller.R
 import com.android.healthconnect.controller.deletion.DeletionConstants.DELETION_TYPE
@@ -63,6 +64,8 @@
 import com.android.healthconnect.controller.shared.preference.HealthPreference
 import com.android.healthconnect.controller.shared.preference.HealthPreferenceFragment
 import com.android.healthconnect.controller.shared.preference.HealthSwitchPreference
+import com.android.healthconnect.controller.utils.AttributeResolver
+import com.android.healthconnect.controller.utils.FeatureUtils
 import com.android.healthconnect.controller.utils.LocalDateTimeFormatter
 import com.android.healthconnect.controller.utils.dismissLoadingDialog
 import com.android.healthconnect.controller.utils.logging.AppAccessElement
@@ -84,7 +87,7 @@
         private const val ALLOW_ALL_PREFERENCE = "allow_all_preference"
         private const val READ_CATEGORY = "read_permission_category"
         private const val WRITE_CATEGORY = "write_permission_category"
-        private const val DELETE_APP_DATA_PREFERENCE = "delete_app_data"
+        private const val MANAGE_DATA_PREFERENCE_KEY = "manage_app"
         private const val FOOTER_KEY = "connected_app_footer"
         private const val PARAGRAPH_SEPARATOR = "\n\n"
     }
@@ -93,6 +96,7 @@
         this.setPageName(PageName.APP_ACCESS_PAGE)
     }
 
+    @Inject lateinit var featureUtils: FeatureUtils
     @Inject lateinit var logger: HealthConnectLogger
     @Inject lateinit var healthPermissionReader: HealthPermissionReader
 
@@ -118,8 +122,8 @@
         preferenceScreen.findPreference(WRITE_CATEGORY)
     }
 
-    private val mDeleteAppDataPreference: HealthPreference? by lazy {
-        preferenceScreen.findPreference(DELETE_APP_DATA_PREFERENCE)
+    private val mManageDataCategory: PreferenceGroup? by lazy {
+        preferenceScreen.findPreference(MANAGE_DATA_PREFERENCE_KEY)
     }
 
     private val mConnectedAppFooter: FooterPreference? by lazy {
@@ -134,7 +138,6 @@
         super.onCreatePreferences(savedInstanceState, rootKey)
         setPreferencesFromResource(R.xml.connected_app_screen, rootKey)
 
-        mDeleteAppDataPreference?.logName = AppAccessElement.DELETE_APP_DATA_BUTTON
         allowAllPreference?.logNameActive = AppAccessElement.ALLOW_ALL_PERMISSIONS_SWITCH_ACTIVE
         allowAllPreference?.logNameInactive = AppAccessElement.ALLOW_ALL_PERMISSIONS_SWITCH_INACTIVE
 
@@ -183,7 +186,7 @@
         }
 
         setupAllowAllPreference()
-        setupDeleteAllPreference()
+        setupManageDataPreferenceCategory()
         setupHeader()
         setupFooter()
     }
@@ -197,12 +200,35 @@
         }
     }
 
-    private fun setupDeleteAllPreference() {
-        mDeleteAppDataPreference?.setOnPreferenceClickListener {
-            val deletionType = DeletionType.DeletionTypeAppData(packageName, appName)
-            childFragmentManager.setFragmentResult(
-                START_DELETION_EVENT, bundleOf(DELETION_TYPE to deletionType))
-            true
+    private fun setupManageDataPreferenceCategory() {
+        mManageDataCategory?.removeAll()
+        if (featureUtils.isNewInformationArchitectureEnabled()) {
+            mManageDataCategory?.addPreference(
+                HealthPreference(requireContext()).also {
+                    it.title = getString(R.string.see_app_data)
+                    it.icon =
+                        AttributeResolver.getDrawable(requireContext(), R.attr.dataAndAccessIcon)
+                    it.setOnPreferenceClickListener {
+                        findNavController()
+                            .navigate(
+                                R.id.action_connectedApp_to_appData,
+                                bundleOf(
+                                    EXTRA_PACKAGE_NAME to packageName, EXTRA_APP_NAME to appName))
+                        true
+                    }
+                })
+        } else {
+            mManageDataCategory?.addPreference(
+                HealthPreference(requireContext()).also {
+                    it.title = getString(R.string.delete_app_data)
+                    it.icon = AttributeResolver.getDrawable(requireContext(), R.attr.deleteIcon)
+                    it.setOnPreferenceClickListener {
+                        val deletionType = DeletionType.DeletionTypeAppData(packageName, appName)
+                        childFragmentManager.setFragmentResult(
+                            START_DELETION_EVENT, bundleOf(DELETION_TYPE to deletionType))
+                        true
+                    }
+                })
         }
     }
 
diff --git a/apk/src/com/android/healthconnect/controller/permissions/data/HealthPermissionType.kt b/apk/src/com/android/healthconnect/controller/permissions/data/HealthPermissionType.kt
index 7572713..087841d 100644
--- a/apk/src/com/android/healthconnect/controller/permissions/data/HealthPermissionType.kt
+++ b/apk/src/com/android/healthconnect/controller/permissions/data/HealthPermissionType.kt
@@ -17,6 +17,7 @@
 
 import android.health.connect.HealthPermissionCategory
 
+// TODO (b/299880830) possibly rename "category" to something else
 enum class HealthPermissionType(val category: Int) {
     // ACTIVITY
     ACTIVE_CALORIES_BURNED(HealthPermissionCategory.ACTIVE_CALORIES_BURNED),
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 e837968..7a4561a 100644
--- a/apk/src/com/android/healthconnect/controller/permissions/shared/SettingsActivity.kt
+++ b/apk/src/com/android/healthconnect/controller/permissions/shared/SettingsActivity.kt
@@ -64,24 +64,26 @@
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_settings)
         setTitle(R.string.permgrouplab_health)
+
+        if (maybeRedirectToOnboardingActivity(this) && savedInstanceState == null) {
+            openOnboardingActivity.launch(1)
+        }
     }
 
     override fun onStart() {
         super.onStart()
 
-        if (maybeRedirectToOnboardingActivity(this)) {
-            openOnboardingActivity.launch(1)
+        if (intent.hasExtra(EXTRA_PACKAGE_NAME)) {
+            val packageName = intent.getStringExtra(
+                EXTRA_PACKAGE_NAME)!!
+
+            viewModel.shouldNavigateToFragment.observe(this) { shouldNavigate ->
+                maybeNavigateToFragment(shouldNavigate)
+            }
+
+            viewModel.loadShouldNavigateToFragment(packageName)
         }
 
-        val packageName = intent.getStringExtra(
-            EXTRA_PACKAGE_NAME)!!
-
-        viewModel.shouldNavigateToFragment.observe(this) { shouldNavigate ->
-            maybeNavigateToFragment(shouldNavigate)
-        }
-
-        viewModel.loadShouldNavigateToFragment(packageName)
-
     }
 
     private fun maybeNavigateToFragment(shouldNavigate: Boolean) {
diff --git a/apk/src/com/android/healthconnect/controller/permissiontypes/HealthPermissionTypesFragment.kt b/apk/src/com/android/healthconnect/controller/permissiontypes/HealthPermissionTypesFragment.kt
index 7cf5a86..b550616 100644
--- a/apk/src/com/android/healthconnect/controller/permissiontypes/HealthPermissionTypesFragment.kt
+++ b/apk/src/com/android/healthconnect/controller/permissiontypes/HealthPermissionTypesFragment.kt
@@ -15,6 +15,7 @@
  */
 package com.android.healthconnect.controller.permissiontypes
 
+import android.health.connect.HealthDataCategory
 import android.os.Bundle
 import android.util.Log
 import android.view.View
@@ -47,6 +48,7 @@
 import com.android.healthconnect.controller.shared.preference.HealthPreference
 import com.android.healthconnect.controller.shared.preference.HealthPreferenceFragment
 import com.android.healthconnect.controller.utils.AttributeResolver
+import com.android.healthconnect.controller.utils.FeatureUtils
 import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
 import com.android.healthconnect.controller.utils.logging.PageName
 import com.android.healthconnect.controller.utils.logging.PermissionTypesElement
@@ -76,6 +78,8 @@
     }
 
     @Inject lateinit var logger: HealthConnectLogger
+    @Inject
+    lateinit var featureUtils: FeatureUtils
 
     @HealthDataCategoryInt private var category: Int = 0
 
@@ -174,16 +178,18 @@
             }
         }
 
-        viewModel.priorityList.observe(viewLifecycleOwner) { state ->
-            when (state) {
-                is HealthPermissionTypesViewModel.PriorityListState.Loading -> {
-                    mManageDataCategory?.removePreferenceRecursively(APP_PRIORITY_BUTTON)
-                }
-                is HealthPermissionTypesViewModel.PriorityListState.LoadingFailed -> {
-                    mManageDataCategory?.removePreferenceRecursively(APP_PRIORITY_BUTTON)
-                }
-                is HealthPermissionTypesViewModel.PriorityListState.WithData -> {
-                    updatePriorityButton(state.priorityList)
+        if (!featureUtils.isNewAppPriorityEnabled()) {
+            viewModel.priorityList.observe(viewLifecycleOwner) { state ->
+                when (state) {
+                    is HealthPermissionTypesViewModel.PriorityListState.Loading -> {
+                        mManageDataCategory?.removePreferenceRecursively(APP_PRIORITY_BUTTON)
+                    }
+                    is HealthPermissionTypesViewModel.PriorityListState.LoadingFailed -> {
+                        mManageDataCategory?.removePreferenceRecursively(APP_PRIORITY_BUTTON)
+                    }
+                    is HealthPermissionTypesViewModel.PriorityListState.WithData -> {
+                        updatePriorityButton(state.priorityList)
+                    }
                 }
             }
         }
@@ -191,26 +197,34 @@
 
     private fun updatePriorityButton(priorityList: List<AppMetadata>) {
         mManageDataCategory?.removePreferenceRecursively(APP_PRIORITY_BUTTON)
-        if (priorityList.size > 1) {
-            val appPriorityButton =
-                HealthPreference(requireContext()).also {
-                    it.title = resources.getString(R.string.app_priority_button)
-                    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
-                    it.order = 4
-                    it.setOnPreferenceClickListener {
-                        viewModel.setEditedPriorityList(priorityList)
-                        viewModel.setCategoryLabel(getString(category.lowercaseTitle()))
-                        PriorityListDialogFragment()
-                            .show(childFragmentManager, PriorityListDialogFragment.TAG)
-                        true
-                    }
-                }
-            mManageDataCategory?.addPreference(appPriorityButton)
+
+        // Only display the priority button for Activity and Sleep categories
+        if (category !in setOf(HealthDataCategory.ACTIVITY, HealthDataCategory.SLEEP)) {
+            return
         }
+
+        if (priorityList.size < 2) {
+            return
+        }
+
+        val appPriorityButton =
+            HealthPreference(requireContext()).also {
+                it.title = resources.getString(R.string.app_priority_button)
+                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
+                it.order = 4
+                it.setOnPreferenceClickListener {
+                    viewModel.setEditedPriorityList(priorityList)
+                    viewModel.setCategoryLabel(getString(category.lowercaseTitle()))
+                    PriorityListDialogFragment()
+                        .show(childFragmentManager, PriorityListDialogFragment.TAG)
+                    true
+                }
+            }
+        mManageDataCategory?.addPreference(appPriorityButton)
     }
 
     private fun updatePermissionTypesList(permissionTypeList: List<HealthPermissionType>) {
diff --git a/apk/src/com/android/healthconnect/controller/permissiontypes/HealthPermissionTypesViewModel.kt b/apk/src/com/android/healthconnect/controller/permissiontypes/HealthPermissionTypesViewModel.kt
index 720e0d3..ce28ca2 100644
--- a/apk/src/com/android/healthconnect/controller/permissiontypes/HealthPermissionTypesViewModel.kt
+++ b/apk/src/com/android/healthconnect/controller/permissiontypes/HealthPermissionTypesViewModel.kt
@@ -68,6 +68,7 @@
     val priorityList: LiveData<PriorityListState>
         get() = _priorityList
 
+
     /** Provides a list of apps with data in [HealthPermissionTypesFragment]. */
     val appsWithData: LiveData<AppsWithDataFragmentState>
         get() = _appsWithData
@@ -96,13 +97,6 @@
         _editedPriorityList.postValue(newList)
     }
 
-    fun setEditedAppSources(newList: List<AppMetadata>, category: Int) {
-        _editedPriorityList.postValue(newList)
-        viewModelScope.launch {
-            updatePriorityListUseCase.invoke(newList.map { it.packageName }, category)
-        }
-    }
-
     fun setCategoryLabel(label: String) {
         _categoryLabel.postValue(label)
     }
@@ -159,6 +153,7 @@
 
     fun updatePriorityList(category: @HealthDataCategoryInt Int, newPriorityList: List<String>) {
         _priorityList.postValue(PriorityListState.Loading)
+
         viewModelScope.launch {
             updatePriorityListUseCase.invoke(newPriorityList, category)
             val appMetadataList: List<AppMetadata> =
diff --git a/apk/src/com/android/healthconnect/controller/permissiontypes/api/LoadPriorityListUseCase.kt b/apk/src/com/android/healthconnect/controller/permissiontypes/api/LoadPriorityListUseCase.kt
index 3a85430..d595521 100644
--- a/apk/src/com/android/healthconnect/controller/permissiontypes/api/LoadPriorityListUseCase.kt
+++ b/apk/src/com/android/healthconnect/controller/permissiontypes/api/LoadPriorityListUseCase.kt
@@ -24,6 +24,7 @@
 import com.android.healthconnect.controller.shared.app.AppInfoReader
 import com.android.healthconnect.controller.shared.app.AppMetadata
 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
@@ -36,7 +37,7 @@
     private val healthConnectManager: HealthConnectManager,
     private val appInfoReader: AppInfoReader,
     @IoDispatcher private val dispatcher: CoroutineDispatcher
-) : BaseUseCase<@HealthDataCategoryInt Int, List<AppMetadata>>(dispatcher) {
+) : BaseUseCase<@HealthDataCategoryInt Int, List<AppMetadata>>(dispatcher), ILoadPriorityListUseCase {
 
     /** Returns list of [AppMetadata]s for given [HealthDataCategory] in priority order. */
     override suspend fun execute(input: @HealthDataCategoryInt Int): List<AppMetadata> {
@@ -50,3 +51,9 @@
         }
     }
 }
+
+interface ILoadPriorityListUseCase {
+    suspend fun invoke(input: @HealthDataCategoryInt Int) : UseCaseResults<List<AppMetadata>>
+
+    suspend fun execute(input: @HealthDataCategoryInt Int) : List<AppMetadata>
+}
diff --git a/apk/src/com/android/healthconnect/controller/permissiontypes/prioritylist/PriorityListAdapter.kt b/apk/src/com/android/healthconnect/controller/permissiontypes/prioritylist/PriorityListAdapter.kt
index c1cdbbd..b0dcf2d 100644
--- a/apk/src/com/android/healthconnect/controller/permissiontypes/prioritylist/PriorityListAdapter.kt
+++ b/apk/src/com/android/healthconnect/controller/permissiontypes/prioritylist/PriorityListAdapter.kt
@@ -116,7 +116,7 @@
             appPositionView = itemView.findViewById(R.id.app_position)
             appNameView = itemView.findViewById(R.id.app_name)
             appIconView = itemView.findViewById(R.id.app_icon)
-            dragIconView = itemView.findViewById(R.id.drag_icon)
+            dragIconView = itemView.findViewById(R.id.action_icon)
             this.onItemDragStartedListener = onItemDragStartedListener
         }
 
diff --git a/apk/src/com/android/healthconnect/controller/recentaccess/LoadRecentAccessUseCase.kt b/apk/src/com/android/healthconnect/controller/recentaccess/LoadRecentAccessUseCase.kt
index 49c8c00..fa4860e 100644
--- a/apk/src/com/android/healthconnect/controller/recentaccess/LoadRecentAccessUseCase.kt
+++ b/apk/src/com/android/healthconnect/controller/recentaccess/LoadRecentAccessUseCase.kt
@@ -21,7 +21,7 @@
 import android.util.Log
 import androidx.core.os.asOutcomeReceiver
 import com.android.healthconnect.controller.service.IoDispatcher
-import com.android.healthconnect.controller.utils.SystemTimeSource
+import com.android.healthconnect.controller.utils.TimeSource
 import java.time.Duration
 import java.time.Instant
 import javax.inject.Inject
@@ -35,15 +35,14 @@
 @Inject
 constructor(
     private val manager: HealthConnectManager,
-    @IoDispatcher private val dispatcher: CoroutineDispatcher
+    @IoDispatcher private val dispatcher: CoroutineDispatcher,
+    private val timeSource: TimeSource
 ) : ILoadRecentAccessUseCase {
 
     companion object {
         private const val TAG = "LoadRecentAccessUseCase"
     }
 
-    private val timeSource = SystemTimeSource
-
     /** Returns a list of apps that have recently accessed Health Connect */
     override suspend fun invoke(): List<AccessLog> =
         withContext(dispatcher) {
diff --git a/apk/src/com/android/healthconnect/controller/recentaccess/RecentAccessViewModel.kt b/apk/src/com/android/healthconnect/controller/recentaccess/RecentAccessViewModel.kt
index 9ea4add..8b13d80 100644
--- a/apk/src/com/android/healthconnect/controller/recentaccess/RecentAccessViewModel.kt
+++ b/apk/src/com/android/healthconnect/controller/recentaccess/RecentAccessViewModel.kt
@@ -27,14 +27,13 @@
 import com.android.healthconnect.controller.shared.app.AppInfoReader
 import com.android.healthconnect.controller.shared.app.ConnectedAppStatus
 import com.android.healthconnect.controller.shared.dataTypeToCategory
-import com.android.healthconnect.controller.utils.SystemTimeSource
 import com.android.healthconnect.controller.utils.TimeSource
 import com.android.healthconnect.controller.utils.postValueIfUpdated
 import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
 import java.time.Duration
 import java.time.Instant
 import javax.inject.Inject
-import kotlinx.coroutines.launch
 
 @HiltViewModel
 class RecentAccessViewModel
@@ -43,6 +42,7 @@
     private val appInfoReader: AppInfoReader,
     private val loadHealthPermissionApps: ILoadHealthPermissionApps,
     private val loadRecentAccessUseCase: ILoadRecentAccessUseCase,
+    private val timeSource: TimeSource
 ) : ViewModel() {
 
     companion object {
@@ -54,14 +54,14 @@
     val recentAccessApps: LiveData<RecentAccessState>
         get() = _recentAccessApps
 
-    fun loadRecentAccessApps(maxNumEntries: Int = -1, timeSource: TimeSource = SystemTimeSource) {
+    fun loadRecentAccessApps(maxNumEntries: Int = -1) {
         // Don't show loading if data was loaded before just refresh.
         if (_recentAccessApps.value !is RecentAccessState.WithData) {
             _recentAccessApps.postValue(Loading)
         }
         viewModelScope.launch {
             try {
-                val clusters = getRecentAccessAppsClusters(maxNumEntries, timeSource)
+                val clusters = getRecentAccessAppsClusters(maxNumEntries)
                 _recentAccessApps.postValueIfUpdated(RecentAccessState.WithData(clusters))
             } catch (ex: Exception) {
                 _recentAccessApps.postValueIfUpdated(RecentAccessState.Error)
@@ -70,8 +70,7 @@
     }
 
     private suspend fun getRecentAccessAppsClusters(
-        maxNumEntries: Int,
-        timeSource: TimeSource
+        maxNumEntries: Int
     ): List<RecentAccessEntry> {
         val accessLogs = loadRecentAccessUseCase.invoke()
         val connectedApps = loadHealthPermissionApps.invoke()
@@ -81,7 +80,7 @@
                 .orEmpty()
                 .map { connectedAppMetadata -> connectedAppMetadata.appMetadata.packageName }
 
-        val clusters = clusterEntries(accessLogs, maxNumEntries, timeSource)
+        val clusters = clusterEntries(accessLogs, maxNumEntries)
         val filteredClusters = mutableListOf<RecentAccessEntry>()
         clusters.forEach {
             if (inactiveApps.contains(it.metadata.packageName)) {
@@ -103,8 +102,7 @@
 
     private suspend fun clusterEntries(
         accessLogs: List<AccessLog>,
-        maxNumEntries: Int,
-        timeSource: TimeSource = SystemTimeSource
+        maxNumEntries: Int
     ): List<RecentAccessEntry> {
         if (accessLogs.isEmpty()) {
             return listOf()
@@ -121,9 +119,9 @@
             if (currentCluster == null) {
                 // If no cluster started for this app yet, init one with the current log
                 currentDataAccessEntryClusters.put(
-                    currentPackageName, initDataAccessEntryCluster(currentLog, timeSource))
+                    currentPackageName, initDataAccessEntryCluster(currentLog))
             } else if (logBelongsToCluster(currentLog, currentCluster)) {
-                updateDataAccessEntryCluster(currentCluster, currentLog, timeSource)
+                updateDataAccessEntryCluster(currentCluster, currentLog)
             } else {
                 // Log doesn't belong to current cluster. Convert current cluster to UI entry and
                 // remove
@@ -134,7 +132,7 @@
                 currentDataAccessEntryClusters.remove(currentPackageName)
 
                 currentDataAccessEntryClusters.put(
-                    currentPackageName, initDataAccessEntryCluster(currentLog, timeSource))
+                    currentPackageName, initDataAccessEntryCluster(currentLog))
 
                 // If we have enough entries already and all clusters that are still being
                 // accumulated are
@@ -165,8 +163,7 @@
     }
 
     private suspend fun initDataAccessEntryCluster(
-        accessLog: AccessLog,
-        timeSource: TimeSource = SystemTimeSource
+        accessLog: AccessLog
     ): DataAccessEntryCluster {
         val newCluster =
             DataAccessEntryCluster(
@@ -177,7 +174,7 @@
                         metadata =
                             appInfoReader.getAppMetadata(packageName = accessLog.packageName)))
 
-        updateDataAccessEntryCluster(newCluster, accessLog, timeSource)
+        updateDataAccessEntryCluster(newCluster, accessLog)
         return newCluster
     }
 
@@ -192,8 +189,7 @@
 
     private fun updateDataAccessEntryCluster(
         cluster: DataAccessEntryCluster,
-        accessLog: AccessLog,
-        timeSource: TimeSource = SystemTimeSource
+        accessLog: AccessLog
     ) {
         val midnight =
             timeSource
diff --git a/apk/src/com/android/healthconnect/controller/selectabledeletion/DeletionType.kt b/apk/src/com/android/healthconnect/controller/selectabledeletion/DeletionType.kt
new file mode 100644
index 0000000..d57d414
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/selectabledeletion/DeletionType.kt
@@ -0,0 +1,193 @@
+/*
+ * 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.selectabledeletion
+
+import android.os.Parcel
+import android.os.Parcelable
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType
+import com.android.healthconnect.controller.shared.DataType
+
+/** Represents the types of deletion that the user can perform. */
+sealed class DeletionType : Parcelable {
+    data class DeletionTypeHealthPermissionTypes(
+        val healthPermissionTypes: List<HealthPermissionType>
+    ) : DeletionType() {
+        constructor(
+            parcel: Parcel
+        ) : this(
+            (parcel.createStringArray()
+                    ?: arrayOf(HealthPermissionType.ACTIVE_CALORIES_BURNED.toString()))
+                .map { string -> HealthPermissionType.valueOf(string) }
+                .toList()) {}
+
+        override fun writeToParcel(parcel: Parcel, flags: Int) {
+            parcel.writeStringArray(
+                healthPermissionTypes
+                    .map { permissionType -> permissionType.toString() }
+                    .toTypedArray())
+        }
+
+        override fun describeContents(): Int {
+            return 0
+        }
+
+        companion object CREATOR : Parcelable.Creator<DeletionTypeHealthPermissionTypes> {
+            override fun createFromParcel(parcel: Parcel): DeletionTypeHealthPermissionTypes {
+                return DeletionTypeHealthPermissionTypes(parcel)
+            }
+
+            override fun newArray(size: Int): Array<DeletionTypeHealthPermissionTypes?> {
+                return arrayOfNulls(size)
+            }
+        }
+    }
+
+    data class DeletionTypeHealthPermissionTypesFromApp(
+        val healthPermissionTypes: List<HealthPermissionType>,
+        val packageName: String,
+        val appName: String
+    ) : DeletionType() {
+        constructor(
+            parcel: Parcel
+        ) : this(
+            (parcel.createStringArray()
+                    ?: arrayOf(HealthPermissionType.ACTIVE_CALORIES_BURNED.toString()))
+                .toList()
+                .map { string -> HealthPermissionType.valueOf(string) },
+            parcel.readString() ?: "",
+            parcel.readString() ?: "") {}
+
+        override fun writeToParcel(parcel: Parcel, flags: Int) {
+            parcel.writeStringArray(
+                healthPermissionTypes
+                    .map { permissionType -> permissionType.toString() }
+                    .toTypedArray())
+            parcel.writeString(packageName)
+            parcel.writeString(appName)
+        }
+
+        override fun describeContents(): Int {
+            return 0
+        }
+
+        companion object CREATOR : Parcelable.Creator<DeletionTypeHealthPermissionTypesFromApp> {
+            override fun createFromParcel(
+                parcel: Parcel
+            ): DeletionTypeHealthPermissionTypesFromApp {
+                return DeletionTypeHealthPermissionTypesFromApp(parcel)
+            }
+
+            override fun newArray(size: Int): Array<DeletionTypeHealthPermissionTypesFromApp?> {
+                return arrayOfNulls(size)
+            }
+        }
+    }
+
+    data class DeletionTypeEntries(val ids: List<String>, val dataType: DataType) : DeletionType() {
+        constructor(
+            parcel: Parcel
+        ) : this(
+            (parcel.createStringArray() ?: arrayOf<String>()).toList(),
+            DataType.valueOf(parcel.readString().orEmpty()))
+
+        override fun describeContents(): Int = 0
+
+        override fun writeToParcel(parcel: Parcel, flags: Int) {
+            parcel.writeStringArray(ids.toTypedArray())
+            parcel.writeString(dataType.name)
+        }
+
+        companion object CREATOR : Parcelable.Creator<DeletionTypeEntries> {
+            override fun createFromParcel(parcel: Parcel): DeletionTypeEntries {
+                return DeletionTypeEntries(parcel)
+            }
+
+            override fun newArray(size: Int): Array<DeletionTypeEntries?> {
+                return arrayOfNulls(size)
+            }
+        }
+    }
+
+    data class DeletionTypeAppData(val packageName: String, val appName: String) : DeletionType() {
+        constructor(parcel: Parcel) : this(parcel.readString() ?: "", parcel.readString() ?: "") {}
+
+        override fun writeToParcel(parcel: Parcel, flags: Int) {
+            parcel.writeString(packageName)
+            parcel.writeString(appName)
+        }
+
+        override fun describeContents(): Int {
+            return 0
+        }
+
+        companion object CREATOR : Parcelable.Creator<DeletionTypeAppData> {
+            override fun createFromParcel(parcel: Parcel): DeletionTypeAppData {
+                return DeletionTypeAppData(parcel)
+            }
+
+            override fun newArray(size: Int): Array<DeletionTypeAppData?> {
+                return arrayOfNulls(size)
+            }
+        }
+    }
+
+    class DeletionTypeAllData() : DeletionType() {
+
+        @Suppress(
+            "UNUSED_PARAMETER") // the class has no data to write but inherits from a Parcelable
+        constructor(parcel: Parcel) : this() {}
+
+        override fun writeToParcel(parcel: Parcel, flags: Int) {}
+
+        override fun describeContents(): Int = 0
+
+        companion object CREATOR : Parcelable.Creator<DeletionTypeAllData> {
+            override fun createFromParcel(parcel: Parcel): DeletionTypeAllData {
+                return DeletionTypeAllData(parcel)
+            }
+
+            override fun newArray(size: Int): Array<DeletionTypeAllData?> {
+                return arrayOfNulls(size)
+            }
+        }
+    }
+
+    val hasPermissionTypes: Boolean
+        get() {
+            return when (this) {
+                is DeletionTypeHealthPermissionTypes,
+                is DeletionTypeHealthPermissionTypesFromApp -> true
+                else -> false
+            }
+        }
+
+    val hasEntryIds: Boolean
+        get() {
+            return when (this) {
+                is DeletionTypeEntries -> true
+                else -> false
+            }
+        }
+
+    val hasAppData: Boolean
+        get() {
+            return when (this) {
+                is DeletionTypeHealthPermissionTypesFromApp -> true
+                is DeletionTypeAppData -> true
+                else -> false
+            }
+        }
+}
diff --git a/apk/src/com/android/healthconnect/controller/selectabledeletion/api/DeleteAllDataUseCase.kt b/apk/src/com/android/healthconnect/controller/selectabledeletion/api/DeleteAllDataUseCase.kt
new file mode 100644
index 0000000..753c122
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/selectabledeletion/api/DeleteAllDataUseCase.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.selectabledeletion.api
+
+import android.health.connect.DeleteUsingFiltersRequest
+import android.health.connect.HealthConnectManager
+import com.android.healthconnect.controller.service.IoDispatcher
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+/** Use case to delete all records stored in Health Connect without any filter. */
+@Singleton
+class DeleteAllDataUseCase
+@Inject
+constructor(
+    private val healthConnectManager: HealthConnectManager,
+    @IoDispatcher private val dispatcher: CoroutineDispatcher
+) {
+    suspend fun invoke() =
+        withContext(dispatcher) {
+            healthConnectManager.deleteRecords(
+                DeleteUsingFiltersRequest.Builder().build(), Runnable::run) {}
+        }
+}
diff --git a/apk/src/com/android/healthconnect/controller/selectabledeletion/api/DeleteAppDataUseCase.kt b/apk/src/com/android/healthconnect/controller/selectabledeletion/api/DeleteAppDataUseCase.kt
new file mode 100644
index 0000000..b3125ae
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/selectabledeletion/api/DeleteAppDataUseCase.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.selectabledeletion.api
+
+import android.health.connect.DeleteUsingFiltersRequest
+import android.health.connect.HealthConnectManager
+import android.health.connect.datatypes.DataOrigin
+import com.android.healthconnect.controller.permissions.api.RevokeAllHealthPermissionsUseCase
+import com.android.healthconnect.controller.selectabledeletion.DeletionType
+import com.android.healthconnect.controller.service.IoDispatcher
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+/** Use case to delete all records written by a given app. */
+@Singleton
+class DeleteAppDataUseCase
+@Inject
+constructor(
+    private val healthConnectManager: HealthConnectManager,
+    private val revokeAllHealthPermissionsUseCase: RevokeAllHealthPermissionsUseCase,
+    @IoDispatcher private val dispatcher: CoroutineDispatcher
+) {
+    suspend fun invoke(
+        deleteAppData: DeletionType.DeletionTypeAppData,
+        removePermissions: Boolean = false
+    ) {
+        val deleteRequest = DeleteUsingFiltersRequest.Builder()
+        deleteRequest.addDataOrigin(
+            DataOrigin.Builder().setPackageName(deleteAppData.packageName).build())
+        withContext(dispatcher) {
+            healthConnectManager.deleteRecords(deleteRequest.build(), Runnable::run) {}
+
+            if (removePermissions) {
+                revokeAllHealthPermissionsUseCase.invoke(deleteAppData.packageName)
+            }
+        }
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/selectabledeletion/api/DeleteEntriesUseCase.kt b/apk/src/com/android/healthconnect/controller/selectabledeletion/api/DeleteEntriesUseCase.kt
new file mode 100644
index 0000000..5fcdec6
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/selectabledeletion/api/DeleteEntriesUseCase.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.selectabledeletion.api
+
+import android.health.connect.HealthConnectManager
+import android.health.connect.RecordIdFilter
+import com.android.healthconnect.controller.selectabledeletion.DeletionType.DeletionTypeEntries
+import com.android.healthconnect.controller.service.IoDispatcher
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+/** Use case to delete a list of records by record ID. */
+@Singleton
+class DeleteEntriesUseCase
+@Inject
+constructor(
+    private val healthConnectManager: HealthConnectManager,
+    @IoDispatcher private val dispatcher: CoroutineDispatcher
+) {
+    suspend fun invoke(deleteEntries: DeletionTypeEntries) =
+        withContext(dispatcher) {
+            val recordIdFilters =
+                deleteEntries.ids.map { id ->
+                    RecordIdFilter.fromId(deleteEntries.dataType.recordClass, id)
+                }
+
+            healthConnectManager.deleteRecords(recordIdFilters, Runnable::run) {}
+        }
+}
diff --git a/apk/src/com/android/healthconnect/controller/selectabledeletion/api/DeletePermissionTypesFromAppUseCase.kt b/apk/src/com/android/healthconnect/controller/selectabledeletion/api/DeletePermissionTypesFromAppUseCase.kt
new file mode 100644
index 0000000..676fc22
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/selectabledeletion/api/DeletePermissionTypesFromAppUseCase.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.selectabledeletion.api
+
+import android.health.connect.DeleteUsingFiltersRequest
+import android.health.connect.HealthConnectManager
+import android.health.connect.datatypes.DataOrigin
+import com.android.healthconnect.controller.selectabledeletion.DeletionType
+import com.android.healthconnect.controller.service.IoDispatcher
+import com.android.healthconnect.controller.shared.HealthPermissionToDatatypeMapper
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+/**
+ * Use case to delete all records from the given permission type (e.g. Steps) written by a given
+ * app.
+ */
+@Singleton
+class DeletePermissionTypesFromAppUseCase
+@Inject
+constructor(
+    private val healthConnectManager: HealthConnectManager,
+    @IoDispatcher private val dispatcher: CoroutineDispatcher
+) {
+
+    suspend operator fun invoke(
+        deletePermissionTypesFromApp: DeletionType.DeletionTypeHealthPermissionTypesFromApp,
+    ) {
+        val deleteRequest = DeleteUsingFiltersRequest.Builder()
+
+        deletePermissionTypesFromApp.healthPermissionTypes.map { permissionType ->
+            HealthPermissionToDatatypeMapper.getDataTypes(permissionType).map { recordType ->
+                deleteRequest.addRecordType(recordType)
+            }
+        }
+
+        deleteRequest.addDataOrigin(
+            DataOrigin.Builder().setPackageName(deletePermissionTypesFromApp.packageName).build())
+
+        withContext(dispatcher) {
+            healthConnectManager.deleteRecords(deleteRequest.build(), Runnable::run) {}
+        }
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/selectabledeletion/api/DeletePermissionTypesUseCase.kt b/apk/src/com/android/healthconnect/controller/selectabledeletion/api/DeletePermissionTypesUseCase.kt
new file mode 100644
index 0000000..78a34be
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/selectabledeletion/api/DeletePermissionTypesUseCase.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.selectabledeletion.api
+
+import android.health.connect.DeleteUsingFiltersRequest
+import android.health.connect.HealthConnectManager
+import com.android.healthconnect.controller.selectabledeletion.DeletionType.DeletionTypeHealthPermissionTypes
+import com.android.healthconnect.controller.service.IoDispatcher
+import com.android.healthconnect.controller.shared.HealthPermissionToDatatypeMapper
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+/** Use case to delete all records from the given permission type (e.g. Steps). */
+@Singleton
+class DeletePermissionTypesUseCase
+@Inject
+constructor(
+    private val healthConnectManager: HealthConnectManager,
+    @IoDispatcher private val dispatcher: CoroutineDispatcher
+) {
+
+    suspend operator fun invoke(
+        deletePermissionTypes: DeletionTypeHealthPermissionTypes,
+    ) {
+        val deleteRequest = DeleteUsingFiltersRequest.Builder()
+
+        deletePermissionTypes.healthPermissionTypes.map { permissionType ->
+            HealthPermissionToDatatypeMapper.getDataTypes(permissionType).map { recordType ->
+                deleteRequest.addRecordType(recordType)
+            }
+        }
+
+        withContext(dispatcher) {
+            healthConnectManager.deleteRecords(deleteRequest.build(), Runnable::run) {}
+        }
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/service/UseCaseModule.kt b/apk/src/com/android/healthconnect/controller/service/UseCaseModule.kt
index 5cb2d52..ebe4364 100644
--- a/apk/src/com/android/healthconnect/controller/service/UseCaseModule.kt
+++ b/apk/src/com/android/healthconnect/controller/service/UseCaseModule.kt
@@ -16,15 +16,37 @@
 package com.android.healthconnect.controller.service
 
 import android.health.connect.HealthConnectManager
+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.LoadDataAggregationsUseCase
+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.ILoadMostRecentAggregationsUseCase
+import com.android.healthconnect.controller.datasources.api.ILoadPotentialPriorityListUseCase
+import com.android.healthconnect.controller.datasources.api.IUpdatePriorityListUseCase
+import com.android.healthconnect.controller.datasources.api.LoadMostRecentAggregationsUseCase
+import com.android.healthconnect.controller.datasources.api.LoadPotentialPriorityListUseCase
+import com.android.healthconnect.controller.datasources.api.UpdatePriorityListUseCase
 import com.android.healthconnect.controller.permissions.api.GetGrantedHealthPermissionsUseCase
 import com.android.healthconnect.controller.permissions.connectedapps.ILoadHealthPermissionApps
 import com.android.healthconnect.controller.permissions.connectedapps.LoadHealthPermissionApps
 import com.android.healthconnect.controller.permissions.shared.QueryRecentAccessLogsUseCase
+import com.android.healthconnect.controller.permissiontypes.api.ILoadPriorityListUseCase
+import com.android.healthconnect.controller.permissiontypes.api.LoadPriorityListUseCase
 import com.android.healthconnect.controller.recentaccess.ILoadRecentAccessUseCase
 import com.android.healthconnect.controller.recentaccess.LoadRecentAccessUseCase
 import com.android.healthconnect.controller.shared.HealthPermissionReader
 import com.android.healthconnect.controller.shared.app.AppInfoReader
 import com.android.healthconnect.controller.shared.app.GetContributorAppInfoUseCase
+import com.android.healthconnect.controller.utils.TimeSource
 import dagger.Module
 import dagger.Provides
 import dagger.hilt.InstallIn
@@ -37,9 +59,10 @@
     @Provides
     fun providesLoadRecentAccessUseCase(
         manager: HealthConnectManager,
-        @IoDispatcher dispatcher: CoroutineDispatcher
+        @IoDispatcher dispatcher: CoroutineDispatcher,
+        timeSource: TimeSource
     ): ILoadRecentAccessUseCase {
-        return LoadRecentAccessUseCase(manager, dispatcher)
+        return LoadRecentAccessUseCase(manager, dispatcher, timeSource)
     }
 
     @Provides
@@ -59,4 +82,90 @@
             appInfoReader,
             dispatcher)
     }
+
+    @Provides
+    fun providesLoadDataEntriesUseCase(
+        @IoDispatcher dispatcher: CoroutineDispatcher,
+        loadEntriesHelper: LoadEntriesHelper
+    ): ILoadDataEntriesUseCase {
+        return LoadDataEntriesUseCase(dispatcher, loadEntriesHelper)
+    }
+
+    @Provides
+    fun providesLoadDataAggregationsUseCase(
+        @IoDispatcher dispatcher: CoroutineDispatcher,
+        stepsFormatter: StepsFormatter,
+        totalCaloriesBurnedFormatter: TotalCaloriesBurnedFormatter,
+        distanceFormatter: DistanceFormatter,
+        sleepSessionFormatter: SleepSessionFormatter,
+        healthConnectManager: HealthConnectManager,
+        appInfoReader: AppInfoReader,
+        loadEntriesHelper: LoadEntriesHelper
+    ): ILoadDataAggregationsUseCase {
+        return LoadDataAggregationsUseCase(
+            loadEntriesHelper,
+            stepsFormatter,
+            totalCaloriesBurnedFormatter,
+            distanceFormatter,
+            sleepSessionFormatter,
+            healthConnectManager,
+            appInfoReader,
+            dispatcher)
+    }
+
+    @Provides
+    fun providesLoadMenstruationDataUseCase(
+        @IoDispatcher dispatcher: CoroutineDispatcher,
+        menstruationPeriodFormatter: MenstruationPeriodFormatter,
+        loadEntriesHelper: LoadEntriesHelper
+    ): ILoadMenstruationDataUseCase {
+        return LoadMenstruationDataUseCase(
+            loadEntriesHelper, menstruationPeriodFormatter, dispatcher)
+    }
+
+    @Provides
+    fun providesMostRecentAggregationsUseCase(
+        healthConnectManager: HealthConnectManager,
+        loadDataAggregationsUseCase: LoadDataAggregationsUseCase,
+        sleepDataUseCase: LoadSleepDataUseCase,
+        @IoDispatcher dispatcher: CoroutineDispatcher
+    ): ILoadMostRecentAggregationsUseCase {
+        return LoadMostRecentAggregationsUseCase(
+            healthConnectManager, loadDataAggregationsUseCase, sleepDataUseCase, dispatcher)
+    }
+
+    @Provides
+    fun providesLoadPotentialPriorityListUseCase(
+        appInfoReader: AppInfoReader,
+        healthConnectManager: HealthConnectManager,
+        healthPermissionReader: HealthPermissionReader,
+        loadGrantedHealthPermissionsUseCase: GetGrantedHealthPermissionsUseCase,
+        loadPriorityListUseCase: LoadPriorityListUseCase,
+        @IoDispatcher dispatcher: CoroutineDispatcher
+    ): ILoadPotentialPriorityListUseCase {
+        return LoadPotentialPriorityListUseCase(
+            appInfoReader,
+            healthConnectManager,
+            healthPermissionReader,
+            loadGrantedHealthPermissionsUseCase,
+            loadPriorityListUseCase,
+            dispatcher)
+    }
+
+    @Provides
+    fun providesPriorityListUseCase(
+        appInfoReader: AppInfoReader,
+        healthConnectManager: HealthConnectManager,
+        @IoDispatcher dispatcher: CoroutineDispatcher
+    ): ILoadPriorityListUseCase {
+        return LoadPriorityListUseCase(healthConnectManager, appInfoReader, dispatcher)
+    }
+
+    @Provides
+    fun updatePriorityListUseCase(
+        healthConnectManager: HealthConnectManager,
+        @IoDispatcher dispatcher: CoroutineDispatcher
+    ): IUpdatePriorityListUseCase {
+        return UpdatePriorityListUseCase(healthConnectManager, dispatcher)
+    }
 }
diff --git a/apk/src/com/android/healthconnect/controller/shared/HealthPermissionReader.kt b/apk/src/com/android/healthconnect/controller/shared/HealthPermissionReader.kt
index c886a50..ee16299 100644
--- a/apk/src/com/android/healthconnect/controller/shared/HealthPermissionReader.kt
+++ b/apk/src/com/android/healthconnect/controller/shared/HealthPermissionReader.kt
@@ -25,6 +25,7 @@
 import android.health.connect.HealthPermissions
 import com.android.healthconnect.controller.permissions.data.HealthPermission
 import com.android.healthconnect.controller.utils.FeatureUtils
+import com.google.common.annotations.VisibleForTesting
 import dagger.hilt.android.qualifiers.ApplicationContext
 import javax.inject.Inject
 
@@ -54,6 +55,12 @@
             listOf(
                 HealthPermissions.WRITE_EXERCISE_ROUTE,
             )
+
+        private val backgroundReadPermissions =
+            listOf(
+                // TODO (b/300270771) use the permission reference.
+                "android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND",
+            )
     }
 
     suspend fun getAppsWithHealthPermissions(): List<String> {
@@ -63,6 +70,7 @@
                     .queryIntentActivities(
                         getRationaleIntent(), ResolveInfoFlags.of(RESOLVE_INFO_FLAG))
                     .map { it.activityInfo.packageName }
+                    .distinct()
 
             appsWithDeclaredIntent.filter { getDeclaredPermissions(it).isNotEmpty() }
         } catch (e: Exception) {
@@ -78,7 +86,7 @@
             val healthPermissions = getHealthPermissions()
             appInfo.requestedPermissions
                 ?.filter { it in healthPermissions }
-                ?.map { permission -> parsePermission(permission) }
+                ?.mapNotNull { permission -> parsePermission(permission) }
                 .orEmpty()
         } catch (e: NameNotFoundException) {
             emptyList()
@@ -102,28 +110,48 @@
         return intent
     }
 
-    private fun parsePermission(permission: String): HealthPermission {
-        return HealthPermission.fromPermissionString(permission)
+    private fun parsePermission(permission: String): HealthPermission? {
+        return try {
+            HealthPermission.fromPermissionString(permission)
+        } catch (e: IllegalArgumentException) {
+            null
+        }
     }
 
-    private fun getHealthPermissions(): List<String> {
+    @VisibleForTesting
+    fun getHealthPermissions(): List<String> {
         val permissions =
             context.packageManager
                 .queryPermissionsByGroup("android.permission-group.HEALTH", 0)
                 .map { permissionInfo -> permissionInfo.name }
-        return permissions.filterNot { permission ->
-            shouldHideExerciseRoute(permission) || shouldHideSessionTypes(permission)
-        }
+        return permissions.filterNot { permission -> shouldHidePermission(permission) }
+    }
+
+    private fun shouldHidePermission(permission: String): Boolean {
+        return shouldHideExerciseRoute(permission) ||
+            shouldHideExerciseRouteAll(permission) ||
+            shouldHideSessionTypes(permission) ||
+            shouldHideBackgroundReadPermission(permission)
     }
 
     private fun shouldHideExerciseRoute(permission: String): Boolean {
         return permission in exerciseRoutePermissions && !featureUtils.isExerciseRouteEnabled()
     }
 
+    private fun shouldHideExerciseRouteAll(permission: String): Boolean {
+        // TODO(b/300270771): use HealthPermissions.READ_EXERCISE_ROUTES_ALL when the API becomes
+        // unhidden.
+        return permission == "android.permission.health.READ_EXERCISE_ROUTES_ALL"
+    }
+
     private fun shouldHideSessionTypes(permission: String): Boolean {
         return permission in sessionTypePermissions && !featureUtils.isSessionTypesEnabled()
     }
 
+    private fun shouldHideBackgroundReadPermission(permission: String): Boolean {
+        return permission in backgroundReadPermissions && !featureUtils.isBackgroundReadEnabled()
+    }
+
     private fun getRationaleIntent(packageName: String? = null): Intent {
         val intent =
             Intent(Intent.ACTION_VIEW_PERMISSION_USAGE).apply {
diff --git a/apk/src/com/android/healthconnect/controller/shared/app/AppUtils.kt b/apk/src/com/android/healthconnect/controller/shared/app/AppUtils.kt
new file mode 100644
index 0000000..4a503e8
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/shared/app/AppUtils.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.shared.app
+
+import android.content.Context
+import android.content.res.Resources
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Inject
+
+/**
+ * Helper class to determine whether a package name is the default Health Connect app on this
+ * device.
+ */
+interface AppUtils {
+    fun isDefaultApp(context: Context, packageName: String): Boolean
+}
+
+class AppUtilsImpl @Inject constructor() : AppUtils {
+    private val DEFAULT_APP_CONFIG_STRING = "android:string/config_defaultHealthConnectApp"
+
+    override fun isDefaultApp(context: Context, packageName: String): Boolean {
+        val defaultApp =
+            context.resources.getString(
+                Resources.getSystem().getIdentifier(DEFAULT_APP_CONFIG_STRING, null, null))
+
+        return packageName == defaultApp
+    }
+}
+
+@Module
+@InstallIn(SingletonComponent::class)
+class AppUtilsModule {
+    @Provides
+    fun providesAppUtils(): AppUtils {
+        return AppUtilsImpl()
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/shared/app/ConnectedAppMetadata.kt b/apk/src/com/android/healthconnect/controller/shared/app/ConnectedAppMetadata.kt
index 5de6c91..d1c2303 100644
--- a/apk/src/com/android/healthconnect/controller/shared/app/ConnectedAppMetadata.kt
+++ b/apk/src/com/android/healthconnect/controller/shared/app/ConnectedAppMetadata.kt
@@ -16,19 +16,6 @@
  *
  */
 
-/**
- * Copyright (C) 2022 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.shared.app
 
 import java.time.Instant
diff --git a/apk/src/com/android/healthconnect/controller/shared/preference/AggregationDataCard.kt b/apk/src/com/android/healthconnect/controller/shared/preference/AggregationDataCard.kt
new file mode 100644
index 0000000..719e806
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/shared/preference/AggregationDataCard.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.shared.preference
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
+import com.android.healthconnect.controller.R
+import com.android.healthconnect.controller.datasources.AggregationCardInfo
+import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.fromHealthPermissionType
+import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.icon
+import com.android.healthconnect.controller.utils.LocalDateTimeFormatter
+import com.android.healthconnect.controller.utils.SystemTimeSource
+import com.android.healthconnect.controller.utils.TimeSource
+import java.time.Instant
+
+/** A custom card to display the latest available data aggregations. */
+class AggregationDataCard
+@JvmOverloads
+constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    cardType: CardTypeEnum,
+    cardInfo: AggregationCardInfo,
+    private val timeSource: TimeSource = SystemTimeSource
+) : LinearLayout(context, attrs) {
+    private val dateFormatter = LocalDateTimeFormatter(context)
+
+    init {
+        LayoutInflater.from(context).inflate(R.layout.widget_aggregation_data_card, this, true)
+
+        val cardIcon = findViewById<ImageView>(R.id.card_icon)
+        val cardTitle = findViewById<TextView>(R.id.card_title_number)
+        val cardDate = findViewById<TextView>(R.id.card_date)
+        val titleAndDateContainer = findViewById<ConstraintLayout>(R.id.title_date_container)
+
+        cardIcon.background = fromHealthPermissionType(cardInfo.healthPermissionType).icon(context)
+        cardTitle.text = cardInfo.aggregation.aggregation
+
+        val totalStartDate = cardInfo.startDate
+        val totalEndDate = cardInfo.endDate
+        cardDate.text = formatDateText(totalStartDate, totalEndDate)
+
+        val constraintSet = ConstraintSet()
+        constraintSet.clone(titleAndDateContainer)
+
+        // Rearrange the textViews based on the type of card
+        if (cardType == CardTypeEnum.SMALL_CARD) {
+            constraintSet.connect(
+                R.id.card_title_number,
+                ConstraintSet.START,
+                ConstraintSet.PARENT_ID,
+                ConstraintSet.START)
+            constraintSet.connect(
+                R.id.card_title_number,
+                ConstraintSet.TOP,
+                ConstraintSet.PARENT_ID,
+                ConstraintSet.TOP)
+            constraintSet.connect(
+                R.id.card_title_number, ConstraintSet.BOTTOM, R.id.card_date, ConstraintSet.TOP)
+
+            constraintSet.connect(
+                R.id.card_date, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)
+            constraintSet.connect(
+                R.id.card_date, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
+            constraintSet.connect(
+                R.id.card_date, ConstraintSet.TOP, R.id.card_title_number, ConstraintSet.BOTTOM)
+        } else {
+            constraintSet.connect(
+                R.id.card_title_number,
+                ConstraintSet.START,
+                ConstraintSet.PARENT_ID,
+                ConstraintSet.START)
+            constraintSet.connect(
+                R.id.card_title_number,
+                ConstraintSet.TOP,
+                ConstraintSet.PARENT_ID,
+                ConstraintSet.TOP)
+            constraintSet.connect(
+                R.id.card_title_number,
+                ConstraintSet.BOTTOM,
+                ConstraintSet.PARENT_ID,
+                ConstraintSet.BOTTOM)
+
+            constraintSet.connect(
+                R.id.card_date, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
+            constraintSet.connect(
+                R.id.card_date, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)
+            constraintSet.connect(
+                R.id.card_date, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
+        }
+
+        constraintSet.applyTo(titleAndDateContainer)
+    }
+
+    private fun isLessThanOneYearAgo(instant: Instant): Boolean {
+        val oneYearAgo =
+            timeSource
+                .currentLocalDateTime()
+                .minusYears(1)
+                .toLocalDate()
+                .atStartOfDay(timeSource.deviceZoneOffset())
+                .toInstant()
+        return instant.isAfter(oneYearAgo)
+    }
+
+    private fun formatDateText(startDate: Instant, endDate: Instant?): String {
+        return if (endDate != null) {
+            // display date range
+            if (isLessThanOneYearAgo(startDate) && isLessThanOneYearAgo(endDate)) {
+                dateFormatter.formatDateRangeWithoutYear(startDate, endDate)
+            } else {
+                dateFormatter.formatDateRangeWithYear(startDate, endDate)
+            }
+        } else {
+            // display only one date
+            if (isLessThanOneYearAgo(startDate)) {
+                dateFormatter.formatShortDate(startDate)
+            } else {
+                dateFormatter.formatLongDate(startDate)
+            }
+        }
+    }
+
+    enum class CardTypeEnum {
+        SMALL_CARD,
+        LARGE_CARD
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/shared/preference/CardContainerPreference.kt b/apk/src/com/android/healthconnect/controller/shared/preference/CardContainerPreference.kt
new file mode 100644
index 0000000..0e6034e
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/shared/preference/CardContainerPreference.kt
@@ -0,0 +1,337 @@
+/**
+ * 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.shared.preference
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
+import androidx.preference.Preference
+import androidx.preference.PreferenceViewHolder
+import com.android.healthconnect.controller.R
+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
+
+class CardContainerPreference constructor(
+        context: Context,
+        private val timeSource: TimeSource = SystemTimeSource
+): Preference(context), ComparablePreference {
+
+    init {
+        layoutResource = R.layout.widget_card_preference
+        isSelectable = false
+    }
+
+    private val mAggregationCardInfo: MutableList<AggregationCardInfo> = mutableListOf()
+    private var container: ConstraintLayout? = null
+    private var holder: PreferenceViewHolder? = null
+    private var isLoading = false
+    private var progressBar: ConstraintLayout? = null
+
+    fun setAggregationCardInfo(aggregationCardInfoList: List<AggregationCardInfo>) {
+        mAggregationCardInfo.clear()
+
+        if (aggregationCardInfoList.isEmpty()) {
+            return
+        }
+        // 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)
+        }
+    }
+
+    fun setLoading(isLoading: Boolean) {
+        this.isLoading = isLoading
+        if (container == null) {
+            return
+        }
+
+        if (!isLoading) {
+            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
+            val height = container?.height
+
+            container?.removeAllViews()
+            val layoutInflater = LayoutInflater.from(context)
+            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)
+            progressBar?.layoutParams = layoutParams
+            container?.addView(progressBar)
+        }
+    }
+
+    override fun onBindViewHolder(holder: PreferenceViewHolder) {
+        super.onBindViewHolder(holder)
+        this.holder = holder
+        container = holder.itemView as ConstraintLayout
+
+        if (!isLoading) {
+            setupCards()
+        } else {
+            setLoading(true)
+        }
+
+    }
+
+    private fun setupCards() {
+
+        if (container == null) {
+            return
+        }
+
+        if (this.mAggregationCardInfo.isEmpty() || this.mAggregationCardInfo.size > 2) {
+            return
+        }
+
+        if (mAggregationCardInfo.size == 1) {
+            addSingleLargeCard(mAggregationCardInfo[0])
+            container?.removeView(progressBar)
+        } else {
+
+            // Add both types of cards to the container (they will be invisible)
+            val (firstSmallCard, secondSmallCard) =
+                addTwoSmallCards(mAggregationCardInfo[0],
+                    mAggregationCardInfo[1])
+
+            val (firstLargeCard, secondLargeCard) =
+                addTwoLargeCards(mAggregationCardInfo[0], mAggregationCardInfo[1])
+
+            val firstCardText = firstSmallCard.findViewById<TextView>(R.id.card_title_number)
+            val secondCardText = secondSmallCard.findViewById<TextView>(R.id.card_title_number)
+            val firstCardDate = firstSmallCard.findViewById<TextView>(R.id.card_date)
+            val secondCardDate = secondSmallCard.findViewById<TextView>(R.id.card_date)
+
+            // Check for the ellipsized text after the first card has been drawn
+            // If there is ellipsized text, remove the small cards and set the large cards to
+            // visible
+            // If there is no ellipsized text, remove the large cards and set the small cards to
+            // visible
+            firstSmallCard.post {
+                if (isTextEllipsized(firstCardText) ||
+                    isTextEllipsized(secondCardText) ||
+                    isTextEllipsized(firstCardDate) ||
+                    isTextEllipsized(secondCardDate)) {
+                    container?.removeView(firstSmallCard)
+                    container?.removeView(secondSmallCard)
+                    container?.removeView(progressBar)
+                    firstLargeCard.visibility = View.VISIBLE
+                    secondLargeCard.visibility = View.VISIBLE
+                } else {
+                    container?.removeView(firstLargeCard)
+                    container?.removeView(secondLargeCard)
+                    container?.removeView(progressBar)
+                    firstSmallCard.visibility = View.VISIBLE
+                    secondSmallCard.visibility = View.VISIBLE
+                }
+            }
+        }
+    }
+
+    /**
+     * 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)
+        singleCard.id = View.generateViewId()
+        val layoutParams = ConstraintLayout.LayoutParams(
+                ConstraintLayout.LayoutParams.MATCH_PARENT,
+                ConstraintLayout.LayoutParams.WRAP_CONTENT)
+        singleCard.layoutParams = layoutParams
+        container?.addView(singleCard)
+    }
+
+    /**
+     * 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> {
+        // Construct the first card
+        val firstCard = constructSmallCard(firstCardInfo, addMargin = true)
+
+        // Construct the second card
+        val secondCard = constructSmallCard(secondCardInfo, addMargin = false)
+
+        firstCard.visibility = View.INVISIBLE
+        secondCard.visibility = View.INVISIBLE
+        container?.addView(firstCard)
+        container?.addView(secondCard)
+
+        applySmallCardConstraints(firstCard, secondCard)
+
+        return Pair(firstCard, secondCard)
+    }
+
+    private fun applySmallCardConstraints(
+        firstCard: AggregationDataCard,
+        secondCard: AggregationDataCard
+    ) {
+        // Add the constraints between the two cards in their ConstraintLayout container
+        val constraintSet = ConstraintSet()
+        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.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.applyTo(container)
+    }
+
+    private fun constructSmallCard(
+        cardInfo: AggregationCardInfo,
+        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)
+
+        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)
+        }
+
+        card.layoutParams = layoutParams
+
+        return card
+    }
+
+    /**
+     * 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> {
+        // Construct the first card
+        val firstLongCard = constructLargeCard(firstCardInfo, addMargin = true)
+        // Construct the second card
+        val secondLongCard = constructLargeCard(secondCardInfo, addMargin = false)
+
+        firstLongCard.visibility = View.GONE
+        secondLongCard.visibility = View.GONE
+
+        container?.addView(firstLongCard)
+        container?.addView(secondLongCard)
+
+        applyLargeCardConstraints(firstLongCard, secondLongCard)
+
+        return Pair(firstLongCard, secondLongCard)
+    }
+
+    private fun applyLargeCardConstraints(
+        firstCard: AggregationDataCard,
+        secondCard: AggregationDataCard
+    ) {
+        // Add the constraints between the two cards in their ConstraintLayout container
+        val constraintSet = ConstraintSet()
+        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, secondCard.id, ConstraintSet.TOP)
+        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.applyTo(container)
+    }
+
+    private fun constructLargeCard(
+        cardInfo: AggregationCardInfo,
+        addMargin: Boolean
+    ): AggregationDataCard {
+        val largeCard = AggregationDataCard(context, null,
+            AggregationDataCard.CardTypeEnum.LARGE_CARD, cardInfo, timeSource)
+        largeCard.id = View.generateViewId()
+
+        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)
+        }
+
+        largeCard.layoutParams = layoutParams
+        return largeCard
+    }
+
+    /**
+     * 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 ) {
+                    return true
+                }
+            }
+        }
+        return false
+    }
+
+    override fun hasSameContents(preference: Preference): Boolean {
+        return preference is CardContainerPreference &&
+                preference.mAggregationCardInfo == this.mAggregationCardInfo
+    }
+
+    override fun isSameItem(preference: Preference): Boolean {
+        return preference is CardContainerPreference &&
+                this == preference
+    }
+}
\ No newline at end of file
diff --git a/apk/src/com/android/healthconnect/controller/shared/preference/NoDataPreference.kt b/apk/src/com/android/healthconnect/controller/shared/preference/NoDataPreference.kt
new file mode 100644
index 0000000..5a03a68
--- /dev/null
+++ b/apk/src/com/android/healthconnect/controller/shared/preference/NoDataPreference.kt
@@ -0,0 +1,22 @@
+package com.android.healthconnect.controller.shared.preference
+
+import android.content.Context
+import androidx.preference.Preference
+import androidx.preference.PreferenceViewHolder
+import com.android.healthconnect.controller.R
+
+/** Custom preference for displaying no search result. */
+class NoDataPreference
+constructor(
+    context: Context,
+) : Preference(context) {
+
+    init {
+        layoutResource = R.layout.widget_no_data
+        key = "no_data_preference"
+    }
+
+    override fun onBindViewHolder(holder: PreferenceViewHolder) {
+        super.onBindViewHolder(holder)
+    }
+}
diff --git a/apk/src/com/android/healthconnect/controller/utils/FeatureUtils.kt b/apk/src/com/android/healthconnect/controller/utils/FeatureUtils.kt
index 8998ce6..e81e988 100644
--- a/apk/src/com/android/healthconnect/controller/utils/FeatureUtils.kt
+++ b/apk/src/com/android/healthconnect/controller/utils/FeatureUtils.kt
@@ -11,10 +11,18 @@
 
 interface FeatureUtils {
     fun isSessionTypesEnabled(): Boolean
+
     fun isExerciseRouteEnabled(): Boolean
+
+    fun isExerciseRouteReadAllEnabled(): Boolean
+
     fun isEntryPointsEnabled(): Boolean
+
     fun isNewAppPriorityEnabled(): Boolean
+
     fun isNewInformationArchitectureEnabled(): Boolean
+
+    fun isBackgroundReadEnabled(): Boolean
 }
 
 class FeatureUtilsImpl(context: Context) : FeatureUtils, DeviceConfig.OnPropertiesChangedListener {
@@ -22,12 +30,14 @@
     companion object {
         private const val HEALTH_FITNESS_FLAGS_NAMESPACE = DeviceConfig.NAMESPACE_HEALTH_FITNESS
         private const val PROPERTY_EXERCISE_ROUTE_ENABLED = "exercise_routes_enable"
+        private const val PROPERTY_EXERCISE_ROUTE_READ_ALL_ENABLED =
+            "exercise_routes_read_all_enable"
         private const val PROPERTY_SESSIONS_TYPE_ENABLED = "session_types_enable"
         private const val PROPERTY_ENTRY_POINTS_ENABLED = "entry_points_enable"
         private const val PROPERTY_AGGREGATION_SOURCE_CONTROL_ENABLED =
-                "aggregation_source_controls_enable"
+            "aggregation_source_controls_enable"
         private const val PROPERTY_NEW_INFORMATION_ARCHITECTURE_ENABLED =
-                "new_information_architecture_enable"
+            "new_information_architecture_enable"
     }
 
     private val lock = Any()
@@ -45,13 +55,19 @@
         DeviceConfig.getBoolean(
             HEALTH_FITNESS_FLAGS_NAMESPACE, PROPERTY_EXERCISE_ROUTE_ENABLED, true)
 
+    private var isExerciseRouteReadAllEnabled =
+        DeviceConfig.getBoolean(
+            HEALTH_FITNESS_FLAGS_NAMESPACE, PROPERTY_EXERCISE_ROUTE_READ_ALL_ENABLED, true)
+
     private var isEntryPointsEnabled =
         DeviceConfig.getBoolean(HEALTH_FITNESS_FLAGS_NAMESPACE, PROPERTY_ENTRY_POINTS_ENABLED, true)
 
     private var isNewAppPriorityEnabled =
-            DeviceConfig.getBoolean(HEALTH_FITNESS_FLAGS_NAMESPACE, PROPERTY_AGGREGATION_SOURCE_CONTROL_ENABLED, true)
+        DeviceConfig.getBoolean(
+            HEALTH_FITNESS_FLAGS_NAMESPACE, PROPERTY_AGGREGATION_SOURCE_CONTROL_ENABLED, false)
     private var isNewInformationArchitectureEnabled =
-            DeviceConfig.getBoolean(HEALTH_FITNESS_FLAGS_NAMESPACE, PROPERTY_NEW_INFORMATION_ARCHITECTURE_ENABLED, false)
+        DeviceConfig.getBoolean(
+            HEALTH_FITNESS_FLAGS_NAMESPACE, PROPERTY_NEW_INFORMATION_ARCHITECTURE_ENABLED, false)
 
     override fun isNewAppPriorityEnabled(): Boolean {
         synchronized(lock) {
@@ -77,12 +93,24 @@
         }
     }
 
+    override fun isExerciseRouteReadAllEnabled(): Boolean {
+        synchronized(lock) {
+            return isExerciseRouteReadAllEnabled
+        }
+    }
+
     override fun isEntryPointsEnabled(): Boolean {
         synchronized(lock) {
             return isEntryPointsEnabled
         }
     }
 
+    override fun isBackgroundReadEnabled(): Boolean {
+        synchronized(lock) {
+            return false
+        }
+    }
+
     override fun onPropertiesChanged(properties: DeviceConfig.Properties) {
         synchronized(lock) {
             if (!properties.namespace.equals(HEALTH_FITNESS_FLAGS_NAMESPACE)) {
@@ -101,10 +129,11 @@
                             properties.getBoolean(PROPERTY_ENTRY_POINTS_ENABLED, true)
                     PROPERTY_AGGREGATION_SOURCE_CONTROL_ENABLED ->
                         isNewAppPriorityEnabled =
-                                properties.getBoolean(PROPERTY_AGGREGATION_SOURCE_CONTROL_ENABLED, true)
+                            properties.getBoolean(PROPERTY_AGGREGATION_SOURCE_CONTROL_ENABLED, true)
                     PROPERTY_NEW_INFORMATION_ARCHITECTURE_ENABLED ->
                         isNewInformationArchitectureEnabled =
-                                properties.getBoolean(PROPERTY_NEW_INFORMATION_ARCHITECTURE_ENABLED, false)
+                            properties.getBoolean(
+                                PROPERTY_NEW_INFORMATION_ARCHITECTURE_ENABLED, false)
                 }
             }
         }
diff --git a/apk/src/com/android/healthconnect/controller/utils/LocalDateTimeFormatter.kt b/apk/src/com/android/healthconnect/controller/utils/LocalDateTimeFormatter.kt
index 4ce0f31..bf26b81 100644
--- a/apk/src/com/android/healthconnect/controller/utils/LocalDateTimeFormatter.kt
+++ b/apk/src/com/android/healthconnect/controller/utils/LocalDateTimeFormatter.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
@@ -15,16 +17,62 @@
 
 import android.content.Context
 import android.text.format.DateFormat.*
+import android.text.format.DateUtils
 import com.android.healthconnect.controller.R
 import dagger.hilt.android.qualifiers.ApplicationContext
 import java.time.Instant
+import java.time.ZoneId
+import java.time.format.DateTimeFormatter
+import java.util.Locale
 import javax.inject.Inject
 
 /** Formatter for printing time and time ranges. */
 class LocalDateTimeFormatter @Inject constructor(@ApplicationContext private val context: Context) {
 
+    companion object {
+        // Example: "Sun, Aug 20, 2023"
+        private const val WEEKDAY_DATE_FORMAT_FLAGS_WITH_YEAR: Int =
+            DateUtils.FORMAT_SHOW_WEEKDAY or
+                DateUtils.FORMAT_SHOW_DATE or
+                DateUtils.FORMAT_ABBREV_ALL
+
+        // Example: "Sun, Aug 20"
+        private const val WEEKDAY_DATE_FORMAT_FLAGS_WITHOUT_YEAR: Int =
+            WEEKDAY_DATE_FORMAT_FLAGS_WITH_YEAR or DateUtils.FORMAT_NO_YEAR
+
+        // Example: "Aug 20, 2023"
+        private const val DATE_FORMAT_FLAGS_WITH_YEAR: Int =
+            DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_ABBREV_ALL
+
+        // Example: "Aug 20"
+        private const val DATE_FORMAT_FLAGS_WITHOUT_YEAR: Int =
+            DATE_FORMAT_FLAGS_WITH_YEAR or DateUtils.FORMAT_NO_YEAR
+
+        // Example: "August 2023".
+        private const val MONTH_FORMAT_FLAGS_WITH_YEAR: Int =
+            DateUtils.FORMAT_SHOW_DATE or
+                DateUtils.FORMAT_SHOW_YEAR or
+                DateUtils.FORMAT_NO_MONTH_DAY
+
+        // Example: "August".
+        private const val MONTH_FORMAT_FLAGS_WITHOUT_YEAR: Int =
+            DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_NO_YEAR or DateUtils.FORMAT_NO_MONTH_DAY
+    }
+
     private val timeFormat by lazy { getTimeFormat(context) }
     private val longDateFormat by lazy { getLongDateFormat(context) }
+    private val shortDateFormat by lazy {
+        val systemFormat = getBestDateTimePattern(Locale.getDefault(), "dMMMM")
+        DateTimeFormatter.ofPattern(systemFormat, Locale.getDefault())
+    }
+    private val monthAndYearFormat by lazy {
+        val systemFormat = getBestDateTimePattern(Locale.getDefault(), "MMMMYYYY")
+        DateTimeFormatter.ofPattern(systemFormat, Locale.getDefault())
+    }
+    private val monthFormat by lazy {
+        val systemFormat = getBestDateTimePattern(Locale.getDefault(), "MMMM")
+        DateTimeFormatter.ofPattern(systemFormat, Locale.getDefault())
+    }
 
     /** Returns localized time. */
     fun formatTime(instant: Instant): String {
@@ -36,6 +84,11 @@
         return longDateFormat.format(instant.toEpochMilli())
     }
 
+    /** Returns localized short versions of date, such as "15 August" */
+    fun formatShortDate(instant: Instant): String {
+        return instant.atZone(ZoneId.systemDefault()).format(shortDateFormat)
+    }
+
     /** Returns localized time range. */
     fun formatTimeRange(start: Instant, end: Instant): String {
         return context.getString(R.string.time_range, formatTime(start), formatTime(end))
@@ -45,4 +98,46 @@
     fun formatTimeRangeA11y(start: Instant, end: Instant): String {
         return context.getString(R.string.time_range_long, formatTime(start), formatTime(end))
     }
+
+    /** Formats date with weekday and year (e.g. "Sun, Aug 20, 2023"). */
+    fun formatWeekdayDateWithYear(time: Instant): String {
+        return DateUtils.formatDateTime(
+            context,
+            time.toEpochMilli(),
+            WEEKDAY_DATE_FORMAT_FLAGS_WITH_YEAR or DateUtils.FORMAT_ABBREV_ALL)
+    }
+
+    /** Formats date with weekday (e.g. "Sun, Aug 20"). */
+    fun formatWeekdayDateWithoutYear(time: Instant): String {
+        return DateUtils.formatDateTime(
+            context,
+            time.toEpochMilli(),
+            WEEKDAY_DATE_FORMAT_FLAGS_WITHOUT_YEAR or DateUtils.FORMAT_ABBREV_ALL)
+    }
+
+    /** Formats date range with year(e.g. "Aug 21 - 27, 2023", "Aug 28 - Sept 3, 2023"). */
+    fun formatDateRangeWithYear(startTime: Instant, endTime: Instant): String {
+        return DateUtils.formatDateRange(
+            context, startTime.toEpochMilli(), endTime.toEpochMilli(), DATE_FORMAT_FLAGS_WITH_YEAR)
+    }
+
+    /** Formats date range (e.g. "Aug 21 - 27", "Aug 28 - Sept 3"). */
+    fun formatDateRangeWithoutYear(startTime: Instant, endTime: Instant): String {
+        return DateUtils.formatDateRange(
+            context,
+            startTime.toEpochMilli(),
+            endTime.toEpochMilli(),
+            DATE_FORMAT_FLAGS_WITHOUT_YEAR)
+    }
+
+    /** Formats month and year (e.g. "August 2023"). */
+    fun formatMonthWithYear(time: Instant): String {
+        return DateUtils.formatDateTime(context, time.toEpochMilli(), MONTH_FORMAT_FLAGS_WITH_YEAR)
+    }
+
+    /** Formats month (e.g. "August"). */
+    fun formatMonthWithoutYear(time: Instant): String {
+        return DateUtils.formatDateTime(
+            context, time.toEpochMilli(), MONTH_FORMAT_FLAGS_WITHOUT_YEAR)
+    }
 }
diff --git a/apk/src/com/android/healthconnect/controller/utils/SystemTimeSource.kt b/apk/src/com/android/healthconnect/controller/utils/SystemTimeSource.kt
index a7940fd..ba8c02e 100644
--- a/apk/src/com/android/healthconnect/controller/utils/SystemTimeSource.kt
+++ b/apk/src/com/android/healthconnect/controller/utils/SystemTimeSource.kt
@@ -13,9 +13,15 @@
  */
 package com.android.healthconnect.controller.utils
 
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
 import java.time.Instant
 import java.time.LocalDateTime
 import java.time.ZoneId
+import javax.inject.Inject
+import javax.inject.Singleton
 
 /** Time source that uses the system time. */
 object SystemTimeSource : TimeSource {
@@ -33,3 +39,13 @@
             .toLocalDateTime()
     }
 }
+
+@Module
+@InstallIn(SingletonComponent::class)
+class SystemTimeSourceModule {
+    @Provides
+    @Singleton
+    fun providesSystemTimeSourceModule(): TimeSource {
+        return SystemTimeSource
+    }
+}
\ No newline at end of file
diff --git a/apk/src/com/android/healthconnect/controller/utils/TimeExtensions.kt b/apk/src/com/android/healthconnect/controller/utils/TimeExtensions.kt
index 0138f04..0a42689 100644
--- a/apk/src/com/android/healthconnect/controller/utils/TimeExtensions.kt
+++ b/apk/src/com/android/healthconnect/controller/utils/TimeExtensions.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
@@ -43,3 +45,35 @@
 fun Instant.toLocalTime(): LocalTime {
     return atZone(ZoneId.systemDefault()).toLocalTime()
 }
+
+fun Instant.isOnSameDay(other: Instant): Boolean {
+    val localDate1 = this.toLocalDate()
+    val localDate2 = other.toLocalDate()
+    return localDate1 == localDate2
+}
+
+fun Instant.isOnDayBefore(other: Instant): Boolean {
+    val localDate1 = this.toLocalDate()
+    val localDate2 = other.toLocalDate()
+    return localDate1 == localDate2.minusDays(1)
+}
+
+fun Instant.isOnDayAfter(other: Instant): Boolean {
+    val localDate1 = this.toLocalDate()
+    val localDate2 = other.toLocalDate()
+    return localDate1 == localDate2.plusDays(1)
+}
+
+fun Instant.atStartOfDay(): Instant {
+    return this.toLocalDate().atStartOfDay(ZoneId.systemDefault()).toInstant()
+}
+
+fun Instant.isAtLeastOneDayAfter(other: Instant): Boolean {
+    val localDate1 = this.toLocalDate()
+    val localDate2 = other.toLocalDate()
+    return localDate1.isAfter(localDate2.plusDays(1)) || localDate1 == localDate2.plusDays(1)
+}
+
+fun LocalDate.toInstantAtStartOfDay(): Instant {
+    return this.atStartOfDay(ZoneId.systemDefault()).toInstant()
+}
diff --git a/apk/tests/TestApp/AndroidManifest.xml b/apk/tests/TestApp/AndroidManifest.xml
index 19f12be..adbd724 100644
--- a/apk/tests/TestApp/AndroidManifest.xml
+++ b/apk/tests/TestApp/AndroidManifest.xml
@@ -24,7 +24,9 @@
     <uses-permission android:name="android.permission.health.WRITE_EXERCISE"/>
     <uses-permission android:name="android.permission.health.READ_SLEEP"/>
     <uses-permission android:name="android.permission.health.WRITE_SLEEP"/>
+    <uses-permission android:name="android.permission.health.READ_EXERCISE_ROUTES_ALL"/>
     <uses-permission android:name="android.permission.health.WRITE_EXERCISE_ROUTE"/>
+    <uses-permission android:name="android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND"/>
     <uses-permission android:name="android.permission.MANAGE_HEALTH_DATA"/>
         <uses-permission android:name="android.permission-group.HEALTH"/>
 
diff --git a/apk/tests/TestApp2/AndroidManifest.xml b/apk/tests/TestApp2/AndroidManifest.xml
index e4ce51b..9811624 100644
--- a/apk/tests/TestApp2/AndroidManifest.xml
+++ b/apk/tests/TestApp2/AndroidManifest.xml
@@ -39,5 +39,16 @@
                 <category android:name="android.intent.category.HEALTH_PERMISSIONS" />
             </intent-filter>
         </activity>
+        <activity-alias
+            android:name="ViewPermissionsActivity"
+            android:exported="true"
+            android:permission="android.permission.START_VIEW_PERMISSION_USAGE"
+            android:targetActivity="android.healthconnect.controller.test.app2.MainActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
+                <category android:name="android.intent.category.HEALTH_PERMISSIONS" />
+            </intent-filter>
+        </activity-alias>
+
     </application>
 </manifest>
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/MainActivityTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/MainActivityTest.kt
new file mode 100644
index 0000000..2e5c434
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/MainActivityTest.kt
@@ -0,0 +1,106 @@
+package com.android.healthconnect.controller.tests
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import androidx.lifecycle.MutableLiveData
+import androidx.test.core.app.ActivityScenario.launchActivityForResult
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.scrollTo
+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.MainActivity
+import com.android.healthconnect.controller.migration.MigrationViewModel
+import com.android.healthconnect.controller.migration.MigrationViewModel.MigrationFragmentState.WithData
+import com.android.healthconnect.controller.migration.api.MigrationState
+import com.android.healthconnect.controller.tests.utils.showOnboarding
+import com.android.healthconnect.controller.tests.utils.whenever
+import dagger.hilt.android.testing.BindValue
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+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 MainActivityTest {
+
+    @get:Rule val hiltRule = HiltAndroidRule(this)
+
+    @BindValue val viewModel: MigrationViewModel = Mockito.mock(MigrationViewModel::class.java)
+
+    private lateinit var context: Context
+
+    @Before
+    fun setup() {
+        hiltRule.inject()
+        context = InstrumentationRegistry.getInstrumentation().context
+
+        showOnboarding(context, show = false)
+    }
+
+    @Test
+    fun homeSettingsIntent_onboardingDone_launchesMainActivity() = runTest {
+        whenever(viewModel.getCurrentMigrationUiState()).then { MigrationState.COMPLETE_IDLE }
+        whenever(viewModel.migrationState).then {
+            MutableLiveData(WithData(MigrationState.COMPLETE_IDLE))
+        }
+
+        val startActivityIntent =
+            Intent.makeMainActivity(ComponentName(context, MainActivity::class.java))
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+
+        launchActivityForResult<MainActivity>(startActivityIntent)
+
+        onView(withText("Recent access")).check(matches(isDisplayed()))
+        onView(withText("Permissions and data")).check(matches(isDisplayed()))
+    }
+
+    @Test
+    fun homeSettingsIntent_onboardingNotDone_redirectToOnboarding() = runTest {
+        showOnboarding(context, true)
+        whenever(viewModel.getCurrentMigrationUiState()).then { MigrationState.COMPLETE_IDLE }
+        whenever(viewModel.migrationState).then {
+            MutableLiveData(WithData(MigrationState.COMPLETE_IDLE))
+        }
+
+        val startActivityIntent =
+            Intent.makeMainActivity(ComponentName(context, MainActivity::class.java))
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+
+        launchActivityForResult<MainActivity>(startActivityIntent)
+
+        onView(withText("Share data with your apps"))
+            .perform(scrollTo())
+            .check(matches(isDisplayed()))
+    }
+
+    @Test
+    fun homeSettingsIntent_migrationInProgress_redirectToMigrationScreen() = runTest {
+        showOnboarding(context, false)
+        whenever(viewModel.getCurrentMigrationUiState()).then { MigrationState.IN_PROGRESS }
+        whenever(viewModel.migrationState).then {
+            MutableLiveData(WithData(MigrationState.IN_PROGRESS))
+        }
+
+        val startActivityIntent =
+            Intent.makeMainActivity(ComponentName(context, MainActivity::class.java))
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+
+        launchActivityForResult<MainActivity>(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/autodelete/AutoDeleteRangeTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/autodelete/AutoDeleteRangeTest.kt
index 39d4617..3fb2f2f 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/autodelete/AutoDeleteRangeTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/autodelete/AutoDeleteRangeTest.kt
@@ -19,26 +19,15 @@
 import com.android.healthconnect.controller.autodelete.autoDeleteRangeEnd
 import com.android.healthconnect.controller.autodelete.autoDeleteRangeStart
 import com.android.healthconnect.controller.autodelete.fromNumberOfMonths
-import com.android.healthconnect.controller.tests.utils.NOW
+import com.android.healthconnect.controller.tests.utils.TestTimeSource
 import com.android.healthconnect.controller.utils.TimeSource
 import com.google.common.truth.Truth.assertThat
-import java.time.Instant
-import java.time.LocalDateTime
-import java.time.ZoneId
-import java.time.ZoneOffset
 import org.junit.Assert.assertThrows
 import org.junit.Test
+import java.time.Instant
 
 class AutoDeleteRangeTest {
-    private val timeSource: TimeSource =
-        object : TimeSource {
-            override fun currentTimeMillis(): Long = NOW.toEpochMilli()
-            override fun deviceZoneOffset(): ZoneId = ZoneOffset.UTC
-            override fun currentLocalDateTime(): LocalDateTime =
-                Instant.ofEpochMilli(currentTimeMillis())
-                    .atZone(deviceZoneOffset())
-                    .toLocalDateTime()
-        }
+    private val timeSource: TimeSource = TestTimeSource
 
     @Test
     fun numberOfMonths_rangeNever_returnCorrectValue() {
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/categories/HealthDataCategoryTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/categories/HealthDataCategoryTest.kt
deleted file mode 100644
index e69de29..0000000
--- a/apk/tests/src/com/android/healthconnect/controller/tests/categories/HealthDataCategoryTest.kt
+++ /dev/null
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/data/access/AccessFragmentTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/data/access/AccessFragmentTest.kt
new file mode 100644
index 0000000..a491a6f
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/data/access/AccessFragmentTest.kt
@@ -0,0 +1,183 @@
+/*
+ * 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.access
+
+import android.os.Bundle
+import androidx.lifecycle.MutableLiveData
+import androidx.test.espresso.Espresso.onView
+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.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.data.access.AccessViewModel
+import com.android.healthconnect.controller.data.access.AccessViewModel.AccessScreenState
+import com.android.healthconnect.controller.data.access.AccessViewModel.AccessScreenState.WithData
+import com.android.healthconnect.controller.data.access.AppAccessState
+import com.android.healthconnect.controller.dataaccess.HealthDataAccessFragment
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType
+import com.android.healthconnect.controller.permissiontypes.HealthPermissionTypesFragment.Companion.PERMISSION_TYPE_KEY
+import com.android.healthconnect.controller.shared.app.AppMetadata
+import com.android.healthconnect.controller.tests.utils.launchFragment
+import com.android.healthconnect.controller.tests.utils.whenever
+import dagger.hilt.android.testing.BindValue
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.hamcrest.Matchers.not
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito
+
+@HiltAndroidTest
+class AccessFragmentTest {
+
+    @get:Rule val hiltRule = HiltAndroidRule(this)
+
+    @BindValue val viewModel: AccessViewModel = Mockito.mock(AccessViewModel::class.java)
+
+    @Before
+    fun setup() {
+        hiltRule.inject()
+    }
+
+    @Test
+    fun dataAccessFragment_noSections_noneDisplayed() {
+        whenever(viewModel.appMetadataMap).then {
+            MutableLiveData<AccessViewModel.AccessScreenState>(WithData(emptyMap()))
+        }
+        launchFragment<HealthDataAccessFragment>(distanceBundle())
+
+        onView(withText("Can read distance")).check(doesNotExist())
+        onView(withText("Can write distance")).check(doesNotExist())
+        onView(withText("Inactive apps")).check(doesNotExist())
+        onView(
+                withText(
+                    "These apps can no longer write distance, but still have data stored in Health\u00A0Connect"))
+            .check(doesNotExist())
+        onView(withText("Manage data")).check(matches(isDisplayed()))
+        onView(withText("See all entries")).perform(scrollTo()).check(matches(isDisplayed()))
+        onView(withText("Delete this data")).perform(scrollTo()).check(matches(isDisplayed()))
+    }
+
+    @Test
+    fun dataAccessFragment_readSection_isDisplayed() {
+        val map =
+            mapOf(
+                AppAccessState.Read to listOf(AppMetadata("package1", "appName1", null)),
+                AppAccessState.Write to emptyList(),
+                AppAccessState.Inactive to emptyList())
+        whenever(viewModel.appMetadataMap).then {
+            MutableLiveData<AccessScreenState>(WithData(map))
+        }
+        launchFragment<HealthDataAccessFragment>(distanceBundle())
+
+        onView(withText("Can read distance")).check(matches(isDisplayed()))
+        onView(withText("Can write distance")).check(doesNotExist())
+        onView(withText("Inactive apps")).check(doesNotExist())
+        onView(
+                withText(
+                    "These apps can no longer write distance, but still have data stored in Health\u00A0Connect"))
+            .check(doesNotExist())
+        onView(withText("Manage data")).check(matches(isDisplayed()))
+        onView(withText("See all entries")).perform(scrollTo()).check(matches(isDisplayed()))
+        onView(withText("Delete this data")).perform(scrollTo()).check(matches(isDisplayed()))
+    }
+
+    @Test
+    fun dataAccessFragment_readAndWriteSections_isDisplayed() {
+        val map =
+            mapOf(
+                AppAccessState.Read to listOf(AppMetadata("package1", "appName1", null)),
+                AppAccessState.Write to listOf(AppMetadata("package1", "appName1", null)),
+                AppAccessState.Inactive to emptyList())
+        whenever(viewModel.appMetadataMap).then {
+            MutableLiveData<AccessScreenState>(WithData(map))
+        }
+        launchFragment<HealthDataAccessFragment>(distanceBundle())
+
+        onView(withText("Can read distance")).check(matches(isDisplayed()))
+        onView(withText("Can write distance")).check(matches(isDisplayed()))
+        onView(withText("Inactive apps")).check(doesNotExist())
+        onView(
+                withText(
+                    "These apps can no longer write distance, but still have data stored in Health\u00A0Connect"))
+            .check(doesNotExist())
+        onView(withText("Manage data")).check(matches(isDisplayed()))
+        onView(withText("See all entries")).perform(scrollTo()).check(matches(isDisplayed()))
+        onView(withText("Delete this data")).perform(scrollTo()).check(matches(isDisplayed()))
+    }
+
+    @Test
+    fun dataAccessFragment_inactiveSection_isDisplayed() {
+        val map =
+            mapOf(
+                AppAccessState.Read to emptyList(),
+                AppAccessState.Write to emptyList(),
+                AppAccessState.Inactive to listOf(AppMetadata("package1", "appName1", null)))
+        whenever(viewModel.appMetadataMap).then {
+            MutableLiveData<AccessViewModel.AccessScreenState>(WithData(map))
+        }
+        launchFragment<HealthDataAccessFragment>(distanceBundle())
+
+        onView(withText("Can read distance")).check(doesNotExist())
+        onView(withText("Can write distance")).check(doesNotExist())
+        onView(withText("Inactive apps")).check(matches(isDisplayed()))
+        onView(
+                withText(
+                    "These apps can no longer write distance, but still have data stored in Health\u00A0Connect"))
+            .check(matches(isDisplayed()))
+        onView(withText("Manage data")).check(matches(isDisplayed()))
+        onView(withText("See all entries")).perform(scrollTo()).check(matches(isDisplayed()))
+        onView(withText("Delete this data")).perform(scrollTo()).check(matches(isDisplayed()))
+    }
+
+    @Test
+    fun dataAccessFragment_loadingState_showsLoading() {
+        whenever(viewModel.appMetadataMap).then {
+            MutableLiveData<AccessScreenState>(AccessScreenState.Loading)
+        }
+        launchFragment<HealthDataAccessFragment>(distanceBundle())
+        onView(withId(R.id.progress_indicator)).check(matches(isDisplayed()))
+    }
+
+    @Test
+    fun dataAccessFragment_withData_hidesLoading() {
+        whenever(viewModel.appMetadataMap).then {
+            MutableLiveData<AccessScreenState>(WithData(emptyMap()))
+        }
+        launchFragment<HealthDataAccessFragment>(distanceBundle())
+        onView(withId(R.id.progress_indicator)).check(matches(not(isDisplayed())))
+    }
+
+    @Test
+    fun dataAccessFragment_withError_showError() {
+        whenever(viewModel.appMetadataMap).then {
+            MutableLiveData<AccessScreenState>(AccessScreenState.Error)
+        }
+        launchFragment<HealthDataAccessFragment>(distanceBundle())
+        onView(withId(R.id.progress_indicator)).check(matches(not(isDisplayed())))
+        onView(withId(R.id.error_view)).check(matches(isDisplayed()))
+    }
+
+    private fun distanceBundle(): Bundle {
+        val bundle = Bundle()
+        bundle.putSerializable(PERMISSION_TYPE_KEY, HealthPermissionType.DISTANCE)
+        return bundle
+    }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/data/alldata/AllDataFragmentTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/data/alldata/AllDataFragmentTest.kt
new file mode 100644
index 0000000..f0f5946
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/data/alldata/AllDataFragmentTest.kt
@@ -0,0 +1,96 @@
+/*
+ * 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.alldata
+
+import android.health.connect.HealthDataCategory
+import androidx.lifecycle.MutableLiveData
+import androidx.test.espresso.Espresso.onView
+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.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import com.android.healthconnect.controller.data.alldata.AllDataFragment
+import com.android.healthconnect.controller.data.alldata.AllDataViewModel
+import com.android.healthconnect.controller.data.appdata.PermissionTypesPerCategory
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType
+import com.android.healthconnect.controller.tests.utils.launchFragment
+import com.android.healthconnect.controller.tests.utils.whenever
+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
+
+@HiltAndroidTest
+class AllDataFragmentTest {
+
+    @get:Rule val hiltRule = HiltAndroidRule(this)
+    @BindValue val allDataViewModel: AllDataViewModel = Mockito.mock(AllDataViewModel::class.java)
+
+    @Before
+    fun setup() {
+        hiltRule.inject()
+    }
+
+    @Test
+    fun allDataFragment_noData_noDataMessageDisplayed() {
+        whenever(allDataViewModel.allData).then {
+            MutableLiveData(AllDataViewModel.AllDataState.WithData(listOf()))
+        }
+        launchFragment<AllDataFragment>()
+
+        onView(withText("No data")).check(matches(isDisplayed()))
+        onView(withText("Data from apps with access to Health Connect will show here"))
+            .check(matches(isDisplayed()))
+    }
+
+    @Test
+    fun allDataFragment_dataPresent_populatedDataTypesDisplayed() {
+        whenever(allDataViewModel.allData).then {
+            MutableLiveData(
+                AllDataViewModel.AllDataState.WithData(
+                    listOf(
+                        PermissionTypesPerCategory(
+                            HealthDataCategory.ACTIVITY,
+                            listOf(
+                                HealthPermissionType.DISTANCE,
+                                HealthPermissionType.EXERCISE,
+                                HealthPermissionType.EXERCISE_ROUTE,
+                                HealthPermissionType.STEPS)),
+                        PermissionTypesPerCategory(
+                            HealthDataCategory.CYCLE_TRACKING,
+                            listOf(
+                                HealthPermissionType.MENSTRUATION,
+                                HealthPermissionType.SEXUAL_ACTIVITY)))))
+        }
+        launchFragment<AllDataFragment>()
+
+        onView(withText("Activity")).check(matches(isDisplayed()))
+        onView(withText("Distance")).check(matches(isDisplayed()))
+        onView(withText("Exercise")).check(matches(isDisplayed()))
+        onView(withText("Steps")).check(matches(isDisplayed()))
+
+        onView(withText("Cycle tracking")).perform(scrollTo()).check(matches(isDisplayed()))
+        onView(withText("Menstruation")).perform(scrollTo()).check(matches(isDisplayed()))
+        onView(withText("Sexual activity")).perform(scrollTo()).check(matches(isDisplayed()))
+
+        onView(withText("Body measurements")).check(doesNotExist())
+        onView(withText("No data")).check(doesNotExist())
+    }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/data/alldata/AllDataViewModelTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/data/alldata/AllDataViewModelTest.kt
new file mode 100644
index 0000000..0ef0a01
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/data/alldata/AllDataViewModelTest.kt
@@ -0,0 +1,166 @@
+/*
+ * 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.alldata
+
+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.alldata.AllDataViewModel
+import com.android.healthconnect.controller.data.appdata.AppDataUseCase
+import com.android.healthconnect.controller.data.appdata.PermissionTypesPerCategory
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType
+import com.android.healthconnect.controller.tests.utils.InstantTaskExecutorRule
+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.TestObserver
+import com.android.healthconnect.controller.tests.utils.getDataOrigin
+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 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.Matchers.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.MockitoAnnotations
+import org.mockito.invocation.InvocationOnMock
+import java.util.Locale
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@HiltAndroidTest
+class AllDataViewModelTest {
+
+    @get:Rule val hiltRule = HiltAndroidRule(this)
+
+    @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule()
+    private val testDispatcher = TestCoroutineDispatcher()
+
+    var manager: HealthConnectManager = mock(HealthConnectManager::class.java)
+
+    private lateinit var viewModel: AllDataViewModel
+    private lateinit var context: Context
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        context = InstrumentationRegistry.getInstrumentation().context
+        context.setLocale(Locale.US)
+        hiltRule.inject()
+        Dispatchers.setMain(testDispatcher)
+        viewModel = AllDataViewModel(AppDataUseCase(manager, Dispatchers.Main))
+    }
+
+    @After
+    fun tearDown() {
+        Dispatchers.resetMain()
+        testDispatcher.cleanupTestCoroutines()
+    }
+
+    @Test
+    fun loadAllData_noData_returnsEmptyList() = runTest {
+        doAnswer(prepareAnswer(mapOf())).`when`(manager).queryAllRecordTypesInfo(any(), any())
+
+        val testObserver = TestObserver<AllDataViewModel.AllDataState>()
+        viewModel.allData.observeForever(testObserver)
+        viewModel.loadAllData()
+        advanceUntilIdle()
+
+        val expected =
+            listOf(
+                PermissionTypesPerCategory(HealthDataCategory.ACTIVITY, listOf()),
+                PermissionTypesPerCategory(HealthDataCategory.BODY_MEASUREMENTS, listOf()),
+                PermissionTypesPerCategory(HealthDataCategory.CYCLE_TRACKING, listOf()),
+                PermissionTypesPerCategory(HealthDataCategory.NUTRITION, listOf()),
+                PermissionTypesPerCategory(HealthDataCategory.SLEEP, listOf()),
+                PermissionTypesPerCategory(HealthDataCategory.VITALS, listOf()))
+        assertThat(testObserver.getLastValue())
+            .isEqualTo(AllDataViewModel.AllDataState.WithData(expected))
+    }
+
+    @Test
+    fun loadAllData_hasData_returnsDataWrittenByAllApps() = 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)))))
+        doAnswer(prepareAnswer(recordTypeInfoMap))
+            .`when`(manager)
+            .queryAllRecordTypesInfo(any(), any())
+
+        val testObserver = TestObserver<AllDataViewModel.AllDataState>()
+        viewModel.allData.observeForever(testObserver)
+        viewModel.loadAllData()
+        advanceUntilIdle()
+
+        val expected =
+            listOf(
+                PermissionTypesPerCategory(
+                    HealthDataCategory.ACTIVITY, listOf(HealthPermissionType.STEPS)),
+                PermissionTypesPerCategory(
+                    HealthDataCategory.BODY_MEASUREMENTS, listOf(HealthPermissionType.WEIGHT)),
+                PermissionTypesPerCategory(HealthDataCategory.CYCLE_TRACKING, listOf()),
+                PermissionTypesPerCategory(HealthDataCategory.NUTRITION, listOf()),
+                PermissionTypesPerCategory(HealthDataCategory.SLEEP, listOf()),
+                PermissionTypesPerCategory(
+                    HealthDataCategory.VITALS, listOf(HealthPermissionType.HEART_RATE)))
+        assertThat(testObserver.getLastValue())
+            .isEqualTo(AllDataViewModel.AllDataState.WithData(expected))
+    }
+
+    private fun prepareAnswer(
+        recordTypeInfoMap: Map<Class<out Record>, RecordTypeInfoResponse>
+    ): (InvocationOnMock) -> Map<Class<out Record>, RecordTypeInfoResponse> {
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.arguments[1] as OutcomeReceiver<Any?, *>
+            receiver.onResult(recordTypeInfoMap)
+            recordTypeInfoMap
+        }
+        return answer
+    }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/data/appdata/AppDataFragmentTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/data/appdata/AppDataFragmentTest.kt
new file mode 100644
index 0000000..5cdf831
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/data/appdata/AppDataFragmentTest.kt
@@ -0,0 +1,117 @@
+/*
+ * 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.appdata
+
+import android.content.Intent
+import android.health.connect.HealthDataCategory
+import androidx.core.os.bundleOf
+import androidx.lifecycle.MutableLiveData
+import androidx.test.espresso.Espresso.onView
+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.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.healthconnect.controller.R
+import com.android.healthconnect.controller.data.appdata.AppDataFragment
+import com.android.healthconnect.controller.data.appdata.AppDataViewModel
+import com.android.healthconnect.controller.data.appdata.PermissionTypesPerCategory
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType
+import com.android.healthconnect.controller.permissions.shared.Constants
+import com.android.healthconnect.controller.shared.app.AppMetadata
+import com.android.healthconnect.controller.tests.utils.TEST_APP_NAME
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME
+import com.android.healthconnect.controller.tests.utils.launchFragment
+import com.android.healthconnect.controller.tests.utils.whenever
+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
+
+@HiltAndroidTest
+class AppDataFragmentTest {
+
+    @get:Rule val hiltRule = HiltAndroidRule(this)
+    @BindValue val appDataViewModel: AppDataViewModel = Mockito.mock(AppDataViewModel::class.java)
+
+    @Before
+    fun setup() {
+        hiltRule.inject()
+        val context = InstrumentationRegistry.getInstrumentation().context
+        whenever(appDataViewModel.appInfo).then {
+            MutableLiveData(
+                AppMetadata(
+                    TEST_APP_PACKAGE_NAME,
+                    TEST_APP_NAME,
+                    context.getDrawable(R.drawable.health_connect_logo)))
+        }
+    }
+
+    @Test
+    fun allDataFragment_noData_noDataMessageDisplayed() {
+        whenever(appDataViewModel.appData).then {
+            MutableLiveData(AppDataViewModel.AppDataState.WithData(listOf()))
+        }
+        launchFragment<AppDataFragment>(
+            bundleOf(
+                Intent.EXTRA_PACKAGE_NAME to TEST_APP_PACKAGE_NAME,
+                Constants.EXTRA_APP_NAME to TEST_APP_NAME))
+
+        onView(withText("No data")).check(matches(isDisplayed()))
+        onView(withText("Data from apps with access to Health Connect will show here"))
+            .check(matches(isDisplayed()))
+    }
+
+    @Test
+    fun appDataFragment_dataPresent_populatedDataTypesDisplayed() {
+        whenever(appDataViewModel.appData).then {
+            MutableLiveData(
+                AppDataViewModel.AppDataState.WithData(
+                    listOf(
+                        PermissionTypesPerCategory(
+                            HealthDataCategory.ACTIVITY,
+                            listOf(
+                                HealthPermissionType.DISTANCE,
+                                HealthPermissionType.EXERCISE,
+                                HealthPermissionType.EXERCISE_ROUTE,
+                                HealthPermissionType.STEPS)),
+                        PermissionTypesPerCategory(
+                            HealthDataCategory.CYCLE_TRACKING,
+                            listOf(
+                                HealthPermissionType.MENSTRUATION,
+                                HealthPermissionType.SEXUAL_ACTIVITY)))))
+        }
+        launchFragment<AppDataFragment>(
+            bundleOf(
+                Intent.EXTRA_PACKAGE_NAME to TEST_APP_PACKAGE_NAME,
+                Constants.EXTRA_APP_NAME to TEST_APP_NAME))
+
+        onView(withText("Activity")).check(matches(isDisplayed()))
+        onView(withText("Distance")).check(matches(isDisplayed()))
+        onView(withText("Exercise")).check(matches(isDisplayed()))
+        onView(withText("Steps")).check(matches(isDisplayed()))
+
+        onView(withText("Cycle tracking")).perform(scrollTo()).check(matches(isDisplayed()))
+        onView(withText("Menstruation")).perform(scrollTo()).check(matches(isDisplayed()))
+        onView(withText("Sexual activity")).perform(scrollTo()).check(matches(isDisplayed()))
+
+        onView(withText("Body measurements")).check(doesNotExist())
+    }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/data/appdata/AppDataViewModelTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/data/appdata/AppDataViewModelTest.kt
new file mode 100644
index 0000000..204c4fd
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/data/appdata/AppDataViewModelTest.kt
@@ -0,0 +1,169 @@
+/*
+ * 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.appdata
+
+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.appdata.AppDataUseCase
+import com.android.healthconnect.controller.data.appdata.AppDataViewModel
+import com.android.healthconnect.controller.data.appdata.PermissionTypesPerCategory
+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_PACKAGE_NAME
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME_2
+import com.android.healthconnect.controller.tests.utils.TestObserver
+import com.android.healthconnect.controller.tests.utils.getDataOrigin
+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 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.Matchers.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.MockitoAnnotations
+import org.mockito.invocation.InvocationOnMock
+import java.util.Locale
+import javax.inject.Inject
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@HiltAndroidTest
+class AppDataViewModelTest {
+
+    @get:Rule val hiltRule = HiltAndroidRule(this)
+
+    @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule()
+    private val testDispatcher = TestCoroutineDispatcher()
+
+    @Inject lateinit var appInfoReader: AppInfoReader
+
+    var manager: HealthConnectManager = mock(HealthConnectManager::class.java)
+
+    private lateinit var viewModel: AppDataViewModel
+    private lateinit var context: Context
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        context = InstrumentationRegistry.getInstrumentation().context
+        context.setLocale(Locale.US)
+        hiltRule.inject()
+        Dispatchers.setMain(testDispatcher)
+        viewModel = AppDataViewModel(appInfoReader, AppDataUseCase(manager, Dispatchers.Main))
+    }
+
+    @After
+    fun tearDown() {
+        Dispatchers.resetMain()
+        testDispatcher.cleanupTestCoroutines()
+    }
+
+    @Test
+    fun loadAppData_noData_returnsEmptyList() = runTest {
+        doAnswer(prepareAnswer(mapOf())).`when`(manager).queryAllRecordTypesInfo(any(), any())
+
+        val testObserver = TestObserver<AppDataViewModel.AppDataState>()
+        viewModel.appData.observeForever(testObserver)
+        viewModel.loadAppData(TEST_APP_PACKAGE_NAME)
+        advanceUntilIdle()
+
+        val expected =
+            listOf(
+                PermissionTypesPerCategory(HealthDataCategory.ACTIVITY, listOf()),
+                PermissionTypesPerCategory(HealthDataCategory.BODY_MEASUREMENTS, listOf()),
+                PermissionTypesPerCategory(HealthDataCategory.CYCLE_TRACKING, listOf()),
+                PermissionTypesPerCategory(HealthDataCategory.NUTRITION, listOf()),
+                PermissionTypesPerCategory(HealthDataCategory.SLEEP, listOf()),
+                PermissionTypesPerCategory(HealthDataCategory.VITALS, listOf()))
+        assertThat(testObserver.getLastValue())
+            .isEqualTo(AppDataViewModel.AppDataState.WithData(expected))
+    }
+
+    @Test
+    fun loadAppData_hasData_returnsDataWrittenByGivenApp() = 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)))))
+        doAnswer(prepareAnswer(recordTypeInfoMap))
+            .`when`(manager)
+            .queryAllRecordTypesInfo(any(), any())
+
+        val testObserver = TestObserver<AppDataViewModel.AppDataState>()
+        viewModel.appData.observeForever(testObserver)
+        viewModel.loadAppData(TEST_APP_PACKAGE_NAME)
+        advanceUntilIdle()
+
+        val expected =
+            listOf(
+                PermissionTypesPerCategory(
+                    HealthDataCategory.ACTIVITY, listOf(HealthPermissionType.STEPS)),
+                PermissionTypesPerCategory(HealthDataCategory.BODY_MEASUREMENTS, listOf()),
+                PermissionTypesPerCategory(HealthDataCategory.CYCLE_TRACKING, listOf()),
+                PermissionTypesPerCategory(HealthDataCategory.NUTRITION, listOf()),
+                PermissionTypesPerCategory(HealthDataCategory.SLEEP, listOf()),
+                PermissionTypesPerCategory(
+                    HealthDataCategory.VITALS, listOf(HealthPermissionType.HEART_RATE)))
+        assertThat(testObserver.getLastValue())
+            .isEqualTo(AppDataViewModel.AppDataState.WithData(expected))
+    }
+
+    private fun prepareAnswer(
+        recordTypeInfoMap: Map<Class<out Record>, RecordTypeInfoResponse>
+    ): (InvocationOnMock) -> Map<Class<out Record>, RecordTypeInfoResponse> {
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.arguments[1] as OutcomeReceiver<Any?, *>
+            receiver.onResult(recordTypeInfoMap)
+            recordTypeInfoMap
+        }
+        return answer
+    }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/data/entries/AllEntriesFragmentTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/data/entries/AllEntriesFragmentTest.kt
new file mode 100644
index 0000000..3b30364
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/data/entries/AllEntriesFragmentTest.kt
@@ -0,0 +1,160 @@
+/**
+ * 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
+
+import android.content.Context
+import androidx.core.os.bundleOf
+import androidx.lifecycle.MutableLiveData
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+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.data.entries.AllEntriesFragment
+import com.android.healthconnect.controller.data.entries.EntriesViewModel
+import com.android.healthconnect.controller.data.entries.EntriesViewModel.EntriesFragmentState.Empty
+import com.android.healthconnect.controller.data.entries.EntriesViewModel.EntriesFragmentState.Loading
+import com.android.healthconnect.controller.data.entries.EntriesViewModel.EntriesFragmentState.LoadingFailed
+import com.android.healthconnect.controller.data.entries.EntriesViewModel.EntriesFragmentState.With
+import com.android.healthconnect.controller.data.entries.FormattedEntry.FormattedDataEntry
+import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType.STEPS
+import com.android.healthconnect.controller.permissiontypes.HealthPermissionTypesFragment.Companion.PERMISSION_TYPE_KEY
+import com.android.healthconnect.controller.shared.DataType
+import com.android.healthconnect.controller.shared.app.AppMetadata
+import com.android.healthconnect.controller.tests.utils.TEST_APP_NAME
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME
+import com.android.healthconnect.controller.tests.utils.launchFragment
+import com.android.healthconnect.controller.tests.utils.setLocale
+import com.android.healthconnect.controller.tests.utils.withIndex
+import dagger.hilt.android.testing.BindValue
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import java.time.ZoneId
+import java.util.Locale
+import java.util.TimeZone
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito
+
+@HiltAndroidTest
+class AllEntriesFragmentTest {
+
+    @get:Rule val hiltRule = HiltAndroidRule(this)
+
+    @BindValue val viewModel: EntriesViewModel = Mockito.mock(EntriesViewModel::class.java)
+
+    private lateinit var context: Context
+
+    @Before
+    fun setup() {
+        hiltRule.inject()
+        context = InstrumentationRegistry.getInstrumentation().context
+        context.setLocale(Locale.UK)
+        TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("UTC")))
+
+        Mockito.`when`(viewModel.currentSelectedDate).thenReturn(MutableLiveData())
+        Mockito.`when`(viewModel.period)
+            .thenReturn(MutableLiveData(DateNavigationPeriod.PERIOD_DAY))
+        Mockito.`when`(viewModel.appInfo)
+            .thenReturn(
+                MutableLiveData(
+                    AppMetadata(
+                        TEST_APP_PACKAGE_NAME,
+                        TEST_APP_NAME,
+                        context.getDrawable(R.drawable.health_connect_logo))))
+    }
+
+    @Test
+    fun appEntriesInit_showsDateNavigationPreference() {
+        Mockito.`when`(viewModel.entries).thenReturn(MutableLiveData(With(emptyList())))
+
+        launchFragment<AllEntriesFragment>(bundleOf(PERMISSION_TYPE_KEY to STEPS))
+
+        onView(withId(R.id.date_picker_spinner)).check(matches(isDisplayed()))
+    }
+
+    @Test
+    fun allEntriesInit_noData_showsNoData() {
+        Mockito.`when`(viewModel.entries).thenReturn(MutableLiveData(Empty))
+
+        launchFragment<AllEntriesFragment>(bundleOf(PERMISSION_TYPE_KEY to STEPS))
+
+        onView(withId(R.id.no_data_view)).check(matches(isDisplayed()))
+    }
+
+    @Test
+    fun appEntriesInit_error_showsErrorView() {
+        Mockito.`when`(viewModel.entries).thenReturn(MutableLiveData(LoadingFailed))
+
+        launchFragment<AllEntriesFragment>(bundleOf(PERMISSION_TYPE_KEY to STEPS))
+
+        onView(withId(R.id.error_view)).check(matches(isDisplayed()))
+    }
+
+    @Test
+    fun appEntriesInit_loading_showsLoading() {
+        Mockito.`when`(viewModel.entries).thenReturn(MutableLiveData(Loading))
+
+        launchFragment<AllEntriesFragment>(bundleOf(PERMISSION_TYPE_KEY to STEPS))
+
+        onView(withId(R.id.loading)).check(matches(isDisplayed()))
+    }
+
+    @Test
+    fun appEntriesInit_withData_showsListOfEntries() {
+        Mockito.`when`(viewModel.entries).thenReturn(MutableLiveData(With(FORMATTED_STEPS_LIST)))
+
+        launchFragment<AllEntriesFragment>(bundleOf(PERMISSION_TYPE_KEY to STEPS))
+
+        onView(withText("7:06 - 7:06")).check(matches(isDisplayed()))
+        onView(withText("12 steps")).check(matches(isDisplayed()))
+        onView(withText("8:06 - 8:06")).check(matches(isDisplayed()))
+        onView(withText("15 steps")).check(matches(isDisplayed()))
+    }
+
+    @Test
+    fun appEntries_withData_notShowingDeleteAction() {
+        Mockito.`when`(viewModel.entries).thenReturn(MutableLiveData(With(FORMATTED_STEPS_LIST)))
+
+        launchFragment<AllEntriesFragment>(bundleOf(PERMISSION_TYPE_KEY to STEPS))
+
+        onView(withIndex(withId(R.id.item_data_entry_delete), 0))
+            .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
+    }
+}
+
+private val FORMATTED_STEPS_LIST =
+    listOf(
+        FormattedDataEntry(
+            uuid = "test_id",
+            header = "7:06 - 7:06",
+            headerA11y = "from 7:06 to 7:06",
+            title = "12 steps",
+            titleA11y = "12 steps",
+            dataType = DataType.STEPS),
+        FormattedDataEntry(
+            uuid = "test_id",
+            header = "8:06 - 8:06",
+            headerA11y = "from 8:06 to 8:06",
+            title = "15 steps",
+            titleA11y = "15 steps",
+            dataType = DataType.STEPS))
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/data/entries/AppEntriesFragmentTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/data/entries/AppEntriesFragmentTest.kt
new file mode 100644
index 0000000..7565941
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/data/entries/AppEntriesFragmentTest.kt
@@ -0,0 +1,160 @@
+/**
+ * Copyright (C) 2022 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
+
+import android.content.Context
+import androidx.core.os.bundleOf
+import androidx.lifecycle.MutableLiveData
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+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.data.entries.AppEntriesFragment
+import com.android.healthconnect.controller.data.entries.EntriesViewModel
+import com.android.healthconnect.controller.data.entries.EntriesViewModel.EntriesFragmentState.Empty
+import com.android.healthconnect.controller.data.entries.EntriesViewModel.EntriesFragmentState.Loading
+import com.android.healthconnect.controller.data.entries.EntriesViewModel.EntriesFragmentState.LoadingFailed
+import com.android.healthconnect.controller.data.entries.EntriesViewModel.EntriesFragmentState.With
+import com.android.healthconnect.controller.data.entries.FormattedEntry.FormattedDataEntry
+import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType.STEPS
+import com.android.healthconnect.controller.permissiontypes.HealthPermissionTypesFragment.Companion.PERMISSION_TYPE_KEY
+import com.android.healthconnect.controller.shared.DataType
+import com.android.healthconnect.controller.shared.app.AppMetadata
+import com.android.healthconnect.controller.tests.utils.TEST_APP_NAME
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME
+import com.android.healthconnect.controller.tests.utils.launchFragment
+import com.android.healthconnect.controller.tests.utils.setLocale
+import com.android.healthconnect.controller.tests.utils.withIndex
+import dagger.hilt.android.testing.BindValue
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import java.time.ZoneId
+import java.util.Locale
+import java.util.TimeZone
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito
+
+@HiltAndroidTest
+class AppEntriesFragmentTest {
+
+    @get:Rule val hiltRule = HiltAndroidRule(this)
+
+    @BindValue val viewModel: EntriesViewModel = Mockito.mock(EntriesViewModel::class.java)
+
+    private lateinit var context: Context
+
+    @Before
+    fun setup() {
+        hiltRule.inject()
+        context = InstrumentationRegistry.getInstrumentation().context
+        context.setLocale(Locale.UK)
+        TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("UTC")))
+
+        Mockito.`when`(viewModel.currentSelectedDate).thenReturn(MutableLiveData())
+        Mockito.`when`(viewModel.period)
+            .thenReturn(MutableLiveData(DateNavigationPeriod.PERIOD_DAY))
+        Mockito.`when`(viewModel.appInfo)
+            .thenReturn(
+                MutableLiveData(
+                    AppMetadata(
+                        TEST_APP_PACKAGE_NAME,
+                        TEST_APP_NAME,
+                        context.getDrawable(R.drawable.health_connect_logo))))
+    }
+
+    @Test
+    fun appEntriesInit_showsDateNavigationPreference() {
+        Mockito.`when`(viewModel.entries).thenReturn(MutableLiveData(With(emptyList())))
+
+        launchFragment<AppEntriesFragment>(bundleOf(PERMISSION_TYPE_KEY to STEPS))
+
+        onView(withId(R.id.date_picker_spinner)).check(matches(isDisplayed()))
+    }
+
+    @Test
+    fun appEntriesInit_noData_showsNoData() {
+        Mockito.`when`(viewModel.entries).thenReturn(MutableLiveData(Empty))
+
+        launchFragment<AppEntriesFragment>(bundleOf(PERMISSION_TYPE_KEY to STEPS))
+
+        onView(withId(R.id.no_data_view)).check(matches(isDisplayed()))
+    }
+
+    @Test
+    fun appEntriesInit_error_showsNoData() {
+        Mockito.`when`(viewModel.entries).thenReturn(MutableLiveData(LoadingFailed))
+
+        launchFragment<AppEntriesFragment>(bundleOf(PERMISSION_TYPE_KEY to STEPS))
+
+        onView(withId(R.id.error_view)).check(matches(isDisplayed()))
+    }
+
+    @Test
+    fun appEntriesInit_loading_showsLoading() {
+        Mockito.`when`(viewModel.entries).thenReturn(MutableLiveData(Loading))
+
+        launchFragment<AppEntriesFragment>(bundleOf(PERMISSION_TYPE_KEY to STEPS))
+
+        onView(withId(R.id.loading)).check(matches(isDisplayed()))
+    }
+
+    @Test
+    fun appEntriesInit_withData_showsListOfEntries() {
+        Mockito.`when`(viewModel.entries).thenReturn(MutableLiveData(With(FORMATTED_STEPS_LIST)))
+
+        launchFragment<AppEntriesFragment>(bundleOf(PERMISSION_TYPE_KEY to STEPS))
+
+        onView(withText("7:06 - 7:06")).check(matches(isDisplayed()))
+        onView(withText("12 steps")).check(matches(isDisplayed()))
+        onView(withText("8:06 - 8:06")).check(matches(isDisplayed()))
+        onView(withText("15 steps")).check(matches(isDisplayed()))
+    }
+
+    @Test
+    fun appEntries_withData_notShowingDeleteAction() {
+        Mockito.`when`(viewModel.entries).thenReturn(MutableLiveData(With(FORMATTED_STEPS_LIST)))
+
+        launchFragment<AppEntriesFragment>(bundleOf(PERMISSION_TYPE_KEY to STEPS))
+
+        onView(withIndex(withId(R.id.item_data_entry_delete), 0))
+            .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
+    }
+}
+
+private val FORMATTED_STEPS_LIST =
+    listOf(
+        FormattedDataEntry(
+            uuid = "test_id",
+            header = "7:06 - 7:06",
+            headerA11y = "from 7:06 to 7:06",
+            title = "12 steps",
+            titleA11y = "12 steps",
+            dataType = DataType.STEPS),
+        FormattedDataEntry(
+            uuid = "test_id",
+            header = "8:06 - 8:06",
+            headerA11y = "from 8:06 to 8:06",
+            title = "15 steps",
+            titleA11y = "15 steps",
+            dataType = DataType.STEPS))
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/data/entries/EntriesViewModelTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/data/entries/EntriesViewModelTest.kt
new file mode 100644
index 0000000..8863668
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/data/entries/EntriesViewModelTest.kt
@@ -0,0 +1,169 @@
+/*
+ * 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
+
+import com.android.healthconnect.controller.data.entries.EntriesViewModel
+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.shared.DataType
+import com.android.healthconnect.controller.shared.app.AppInfoReader
+import com.android.healthconnect.controller.tests.utils.InstantTaskExecutorRule
+import com.android.healthconnect.controller.tests.utils.TestObserver
+import com.android.healthconnect.controller.tests.utils.TestTimeSource
+import com.android.healthconnect.controller.tests.utils.di.FakeLoadDataAggregationsUseCase
+import com.android.healthconnect.controller.tests.utils.di.FakeLoadDataEntriesUseCase
+import com.android.healthconnect.controller.tests.utils.di.FakeLoadMenstruationDataUseCase
+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.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 java.time.Instant
+import javax.inject.Inject
+
+@ExperimentalCoroutinesApi
+@HiltAndroidTest
+class EntriesViewModelTest {
+
+    companion object {
+        private fun formattedAggregation(aggregation: String) =
+            FormattedEntry.FormattedAggregation(
+                aggregation = aggregation,
+                aggregationA11y = aggregation,
+                contributingApps = "Test App")
+
+        private val FORMATTED_STEPS =
+            FormattedEntry.FormattedDataEntry(
+                uuid = "test_id",
+                header = "7:06 - 7:06",
+                headerA11y = "from 7:06 to 7:06",
+                title = "12 steps",
+                titleA11y = "12 steps",
+                dataType = DataType.STEPS)
+        private val FORMATTED_STEPS_2 =
+            FormattedEntry.FormattedDataEntry(
+                uuid = "test_id",
+                header = "8:06 - 8:06",
+                headerA11y = "from 8:06 to 8:06",
+                title = "15 steps",
+                titleA11y = "15 steps",
+                dataType = DataType.STEPS)
+        private val FORMATTED_MENSTRUATION_PERIOD =
+            FormattedEntry.FormattedDataEntry(
+                uuid = "test_id",
+                header = "8:06 - 8:06",
+                headerA11y = "from 8:06 to 8:06",
+                title = "15 steps",
+                titleA11y = "15 steps",
+                dataType = DataType.MENSTRUATION_PERIOD)
+    }
+
+    @get:Rule val hiltRule = HiltAndroidRule(this)
+
+    @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule()
+    private val testDispatcher = TestCoroutineDispatcher()
+
+    @Inject lateinit var appInfoReader: AppInfoReader
+    private val timeSource = TestTimeSource
+    private val fakeLoadDataEntriesUseCase = FakeLoadDataEntriesUseCase()
+    private val fakeLoadMenstruationDataUseCase = FakeLoadMenstruationDataUseCase()
+    private val fakeLoadDataAggregationsUseCase = FakeLoadDataAggregationsUseCase()
+
+    private lateinit var viewModel: EntriesViewModel
+
+    @Before
+    fun setup() {
+        hiltRule.inject()
+        Dispatchers.setMain(testDispatcher)
+        viewModel =
+            EntriesViewModel(
+                appInfoReader,
+                fakeLoadDataEntriesUseCase,
+                fakeLoadMenstruationDataUseCase,
+                fakeLoadDataAggregationsUseCase)
+    }
+
+    @After
+    fun tearDown() {
+        Dispatchers.resetMain()
+        testDispatcher.cleanupTestCoroutines()
+    }
+
+    @Test
+    fun loadDataEntries_hasStepsData_returnsFragmentStateWitAggregationAndSteps() = runTest {
+        fakeLoadDataEntriesUseCase.updateList(listOf(FORMATTED_STEPS))
+        fakeLoadDataAggregationsUseCase.updateAggregation(formattedAggregation("12 steps"))
+        val testObserver = TestObserver<EntriesViewModel.EntriesFragmentState>()
+        viewModel.entries.observeForever(testObserver)
+        viewModel.loadEntries(
+            HealthPermissionType.STEPS,
+            Instant.ofEpochMilli(timeSource.currentTimeMillis()),
+            DateNavigationPeriod.PERIOD_WEEK)
+        advanceUntilIdle()
+
+        val actual = testObserver.getLastValue()
+        val expected =
+            EntriesViewModel.EntriesFragmentState.With(
+                listOf(formattedAggregation("12 steps"), FORMATTED_STEPS))
+        assertThat(actual).isEqualTo(expected)
+    }
+
+    @Test
+    fun loadDataEntries_hasMultipleSteps_returnsFragmentStateWitAggregationAndSteps() = runTest {
+        fakeLoadDataEntriesUseCase.updateList(listOf(FORMATTED_STEPS, FORMATTED_STEPS_2))
+        fakeLoadDataAggregationsUseCase.updateAggregation(formattedAggregation("27 steps"))
+        val testObserver = TestObserver<EntriesViewModel.EntriesFragmentState>()
+        viewModel.entries.observeForever(testObserver)
+        viewModel.loadEntries(
+            HealthPermissionType.STEPS,
+            Instant.ofEpochMilli(timeSource.currentTimeMillis()),
+            DateNavigationPeriod.PERIOD_WEEK)
+        advanceUntilIdle()
+
+        val actual = testObserver.getLastValue()
+        val expected =
+            EntriesViewModel.EntriesFragmentState.With(
+                listOf(formattedAggregation("27 steps"), FORMATTED_STEPS, FORMATTED_STEPS_2))
+        assertThat(actual).isEqualTo(expected)
+    }
+
+    @Test
+    fun loadDataEntries_hasMenstruationData_returnsFragmentStateWithData() = runTest {
+        fakeLoadMenstruationDataUseCase.updateList(listOf(FORMATTED_MENSTRUATION_PERIOD))
+        val testObserver = TestObserver<EntriesViewModel.EntriesFragmentState>()
+        viewModel.entries.observeForever(testObserver)
+        viewModel.loadEntries(
+            HealthPermissionType.MENSTRUATION,
+            Instant.ofEpochMilli(timeSource.currentTimeMillis()),
+            DateNavigationPeriod.PERIOD_WEEK)
+        advanceUntilIdle()
+
+        val actual = testObserver.getLastValue()
+        val expected =
+            EntriesViewModel.EntriesFragmentState.With(listOf(FORMATTED_MENSTRUATION_PERIOD))
+        assertThat(actual).isEqualTo(expected)
+    }
+}
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
new file mode 100644
index 0000000..e57d019
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/data/entries/api/LoadDataAggregationsUseCaseTest.kt
@@ -0,0 +1,246 @@
+package com.android.healthconnect.controller.tests.data.entries.api
+
+import android.content.Context
+import android.health.connect.AggregateRecordsResponse
+import android.health.connect.AggregateResult
+import android.health.connect.HealthConnectManager
+import android.health.connect.datatypes.AggregationType
+import android.health.connect.datatypes.units.Energy
+import android.health.connect.datatypes.units.Length
+import android.os.OutcomeReceiver
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.healthconnect.controller.data.entries.FormattedEntry
+import com.android.healthconnect.controller.data.entries.api.LoadAggregationInput
+import com.android.healthconnect.controller.data.entries.api.LoadDataAggregationsUseCase
+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.DistanceFormatter
+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.permissions.data.HealthPermissionType
+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_NAME
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME
+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.Duration
+import java.time.Instant
+import java.util.Locale
+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 LoadDataAggregationsUseCaseTest {
+
+    @get:Rule val hiltRule = HiltAndroidRule(this)
+
+    private lateinit var context: Context
+    private val healthConnectManager: HealthConnectManager =
+        Mockito.mock(HealthConnectManager::class.java)
+    private lateinit var loadDataAggregationsUseCase: LoadDataAggregationsUseCase
+
+    @Inject lateinit var loadEntriesHelper: LoadEntriesHelper
+    @Inject lateinit var stepsFormatter: StepsFormatter
+    @Inject lateinit var totalCaloriesBurnedFormatter: TotalCaloriesBurnedFormatter
+    @Inject lateinit var distanceFormatter: DistanceFormatter
+    @Inject lateinit var sleepSessionFormatter: SleepSessionFormatter
+    @Inject lateinit var appInfoReader: AppInfoReader
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        context = InstrumentationRegistry.getInstrumentation().context
+        context.setLocale(Locale.US)
+        hiltRule.inject()
+        loadDataAggregationsUseCase =
+            LoadDataAggregationsUseCase(
+                loadEntriesHelper,
+                stepsFormatter,
+                totalCaloriesBurnedFormatter,
+                distanceFormatter,
+                sleepSessionFormatter,
+                healthConnectManager,
+                appInfoReader,
+                Dispatchers.Main)
+    }
+
+    @Test
+    fun loadDataAggregationsUseCase_withPeriodAggregationForSteps_returnsFormattedStepsAggregation() =
+        runTest {
+            Mockito.doAnswer(prepareStepsAggregationAnswer())
+                .`when`(healthConnectManager)
+                .aggregate<Long>(
+                    ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any())
+
+            val input =
+                LoadAggregationInput.PeriodAggregation(
+                    HealthPermissionType.STEPS,
+                    TEST_APP_PACKAGE_NAME,
+                    Instant.now(),
+                    DateNavigationPeriod.PERIOD_DAY,
+                    true)
+
+            val result = loadDataAggregationsUseCase.invoke(input)
+            val expected =
+                FormattedEntry.FormattedAggregation("100 steps", "100 steps", TEST_APP_NAME)
+            assertThat(result is UseCaseResults.Success).isTrue()
+            assertThat((result as UseCaseResults.Success).data).isEqualTo(expected)
+        }
+
+    @Test
+    fun loadDataAggregationsUseCase_withPeriodAggregationForDistance_returnsFormattedDistanceAggregation() =
+        runTest {
+            Mockito.doAnswer(prepareDistanceAggregationAnswer())
+                .`when`(healthConnectManager)
+                .aggregate<Length>(
+                    ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any())
+
+            val input =
+                LoadAggregationInput.PeriodAggregation(
+                    HealthPermissionType.DISTANCE,
+                    TEST_APP_PACKAGE_NAME,
+                    Instant.now(),
+                    DateNavigationPeriod.PERIOD_DAY,
+                    true)
+
+            val result = loadDataAggregationsUseCase.invoke(input)
+            val expected = FormattedEntry.FormattedAggregation("1 km", "1 kilometer", TEST_APP_NAME)
+            assertThat(result is UseCaseResults.Success).isTrue()
+            assertThat((result as UseCaseResults.Success).data).isEqualTo(expected)
+        }
+
+    @Test
+    fun loadDataAggregationsUseCase_withPeriodAggregationForCalories_returnsFormattedCaloriesAggregation() =
+        runTest {
+            Mockito.doAnswer(prepareCaloriesAggregationAnswer())
+                .`when`(healthConnectManager)
+                .aggregate<Energy>(
+                    ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any())
+
+            val input =
+                LoadAggregationInput.PeriodAggregation(
+                    HealthPermissionType.TOTAL_CALORIES_BURNED,
+                    TEST_APP_PACKAGE_NAME,
+                    Instant.now(),
+                    DateNavigationPeriod.PERIOD_DAY,
+                    true)
+
+            val result = loadDataAggregationsUseCase.invoke(input)
+            val expected =
+                FormattedEntry.FormattedAggregation("1,500 Cal", "1,500 calories", TEST_APP_NAME)
+            assertThat(result is UseCaseResults.Success).isTrue()
+            assertThat((result as UseCaseResults.Success).data).isEqualTo(expected)
+        }
+
+    @Test
+    fun loadDataAggregationsUseCase_withCustomAggregationForSleep_returnsFormattedSleepAggregation() =
+        runTest {
+            Mockito.doAnswer(prepareSleepAggregationAnswer())
+                .`when`(healthConnectManager)
+                .aggregate<Long>(
+                    ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any())
+
+            val input =
+                LoadAggregationInput.CustomAggregation(
+                    HealthPermissionType.SLEEP,
+                    TEST_APP_PACKAGE_NAME,
+                    Instant.now(),
+                    Instant.now(),
+                    true)
+
+            val result = loadDataAggregationsUseCase.invoke(input)
+            val expected =
+                FormattedEntry.FormattedAggregation("11h 5m", "11 hours 5 minutes", TEST_APP_NAME)
+            assertThat(result is UseCaseResults.Success).isTrue()
+            assertThat((result as UseCaseResults.Success).data).isEqualTo(expected)
+        }
+
+    private fun prepareStepsAggregationAnswer():
+        (InvocationOnMock) -> AggregateRecordsResponse<Long> {
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.arguments[2] as OutcomeReceiver<AggregateRecordsResponse<Long>, *>
+            receiver.onResult(getStepsAggregationResponse())
+            getStepsAggregationResponse()
+        }
+        return answer
+    }
+
+    private fun prepareDistanceAggregationAnswer():
+        (InvocationOnMock) -> AggregateRecordsResponse<Length> {
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.arguments[2] as OutcomeReceiver<AggregateRecordsResponse<Length>, *>
+            receiver.onResult(getDistanceAggregationResponse())
+            getDistanceAggregationResponse()
+        }
+        return answer
+    }
+
+    private fun prepareCaloriesAggregationAnswer():
+        (InvocationOnMock) -> AggregateRecordsResponse<Energy> {
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.arguments[2] as OutcomeReceiver<AggregateRecordsResponse<Energy>, *>
+            receiver.onResult(getCaloriesAggregationResponse())
+            getCaloriesAggregationResponse()
+        }
+        return answer
+    }
+
+    private fun prepareSleepAggregationAnswer():
+        (InvocationOnMock) -> AggregateRecordsResponse<Long> {
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.arguments[2] as OutcomeReceiver<AggregateRecordsResponse<Long>, *>
+            receiver.onResult(getSleepAggregationResponse())
+            getSleepAggregationResponse()
+        }
+        return answer
+    }
+
+    private fun getStepsAggregationResponse(): AggregateRecordsResponse<Long> {
+        val aggregationResult = AggregateResult<Long>(100)
+        aggregationResult.setDataOrigins(listOf(TEST_APP_PACKAGE_NAME))
+        return AggregateRecordsResponse<Long>(
+            mapOf(
+                AggregationType.AggregationTypeIdentifier.STEPS_RECORD_COUNT_TOTAL to
+                    aggregationResult))
+    }
+
+    private fun getDistanceAggregationResponse(): AggregateRecordsResponse<Length> {
+        val aggregationResult = AggregateResult(Length.fromMeters(1000.0))
+        aggregationResult.setDataOrigins(listOf(TEST_APP_PACKAGE_NAME))
+        return AggregateRecordsResponse<Length>(
+            mapOf(
+                AggregationType.AggregationTypeIdentifier.DISTANCE_RECORD_DISTANCE_TOTAL to
+                    aggregationResult))
+    }
+
+    private fun getCaloriesAggregationResponse(): AggregateRecordsResponse<Energy> {
+        val aggregationResult = AggregateResult(Energy.fromCalories(1500000.0))
+        aggregationResult.setDataOrigins(listOf(TEST_APP_PACKAGE_NAME))
+        return AggregateRecordsResponse<Energy>(
+            mapOf(
+                AggregationType.AggregationTypeIdentifier
+                    .TOTAL_CALORIES_BURNED_RECORD_ENERGY_TOTAL to aggregationResult))
+    }
+
+    private fun getSleepAggregationResponse(): AggregateRecordsResponse<Long> {
+        val aggregationResult =
+            AggregateResult(Duration.ofHours(11).plus(Duration.ofMinutes(5)).toMillis())
+        aggregationResult.setDataOrigins(listOf(TEST_APP_PACKAGE_NAME))
+        return AggregateRecordsResponse<Long>(
+            mapOf(
+                AggregationType.AggregationTypeIdentifier.SLEEP_SESSION_DURATION_TOTAL to
+                    aggregationResult))
+    }
+}
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/LoadSleepDataUseCaseTest.kt
new file mode 100644
index 0000000..1cf0f10
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/data/entries/api/LoadSleepDataUseCaseTest.kt
@@ -0,0 +1,404 @@
+package com.android.healthconnect.controller.tests.data.entries.api
+
+import android.content.Context
+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.utils.atStartOfDay
+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.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.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 {
+
+    @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 loadEntriesHelper: LoadEntriesHelper
+
+    @Captor
+    lateinit var requestCaptor: ArgumentCaptor<ReadRecordsRequestUsingFilters<SleepSessionRecord>>
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        context = InstrumentationRegistry.getInstrumentation().context
+        context.setLocale(Locale.US)
+        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())
+
+            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 loadSleepDataUseCase_withinWeek_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_WEEK,
+                    showDataOrigin = true,
+                    permissionType = HealthPermissionType.SLEEP)
+
+            val expectedTimeRangeFilter =
+                loadEntriesHelper.getTimeFilter(startTime, DateNavigationPeriod.PERIOD_WEEK, true)
+
+            Mockito.doAnswer(prepareWeekSleepAnswer())
+                .`when`(healthConnectManager)
+                .readRecords(
+                    ArgumentMatchers.any(ReadRecordsRequestUsingFilters::class.java),
+                    ArgumentMatchers.any(),
+                    ArgumentMatchers.any())
+
+            val actual = loadSleepDataUseCase.invoke(input)
+            val expected =
+                listOf(
+                    SleepSessionRecord.Builder(
+                            getMetaData(),
+                            Instant.parse("2023-06-14T22:30:00Z"),
+                            Instant.parse("2023-06-15T07:45:00Z"))
+                        .build(),
+                    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(),
+                    SleepSessionRecord.Builder(
+                            getMetaData(),
+                            Instant.parse("2023-06-11T22:30:00Z"),
+                            Instant.parse("2023-06-13T07: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 loadSleepDataUseCase_withinMonth_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_MONTH,
+                    showDataOrigin = true,
+                    permissionType = HealthPermissionType.SLEEP)
+
+            val expectedTimeRangeFilter =
+                loadEntriesHelper.getTimeFilter(startTime, DateNavigationPeriod.PERIOD_MONTH, true)
+
+            Mockito.doAnswer(prepareMonthSleepAnswer())
+                .`when`(healthConnectManager)
+                .readRecords(
+                    ArgumentMatchers.any(ReadRecordsRequestUsingFilters::class.java),
+                    ArgumentMatchers.any(),
+                    ArgumentMatchers.any())
+
+            val actual = loadSleepDataUseCase.invoke(input)
+            val expected =
+                listOf(
+                    SleepSessionRecord.Builder(
+                            getMetaData(),
+                            Instant.parse("2023-07-09T22:30:00Z"),
+                            Instant.parse("2023-07-13T07:45:00Z"))
+                        .build(),
+                    SleepSessionRecord.Builder(
+                            getMetaData(),
+                            Instant.parse("2023-06-14T22:30:00Z"),
+                            Instant.parse("2023-06-15T07:45:00Z"))
+                        .build(),
+                    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(),
+                    SleepSessionRecord.Builder(
+                            getMetaData(),
+                            Instant.parse("2023-06-11T22:30:00Z"),
+                            Instant.parse("2023-06-13T07: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)
+        }
+
+    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)
+        }
+    }
+
+    private fun prepareDaySleepAnswer():
+        (InvocationOnMock) -> ReadRecordsResponse<SleepSessionRecord> {
+        val answer = { args: InvocationOnMock ->
+            val receiver =
+                args.arguments[2] as OutcomeReceiver<ReadRecordsResponse<SleepSessionRecord>, *>
+            receiver.onResult(getDaySleepRecords())
+            getDaySleepRecords()
+        }
+        return answer
+    }
+
+    private fun prepareWeekSleepAnswer():
+        (InvocationOnMock) -> ReadRecordsResponse<SleepSessionRecord> {
+        val answer = { args: InvocationOnMock ->
+            val receiver =
+                args.arguments[2] as OutcomeReceiver<ReadRecordsResponse<SleepSessionRecord>, *>
+            receiver.onResult(getWeekSleepRecords())
+            getWeekSleepRecords()
+        }
+        return answer
+    }
+
+    private fun prepareMonthSleepAnswer():
+        (InvocationOnMock) -> ReadRecordsResponse<SleepSessionRecord> {
+        val answer = { args: InvocationOnMock ->
+            val receiver =
+                args.arguments[2] as OutcomeReceiver<ReadRecordsResponse<SleepSessionRecord>, *>
+            receiver.onResult(getMonthSleepRecords())
+            getMonthSleepRecords()
+        }
+        return answer
+    }
+
+    private fun getDaySleepRecords(): ReadRecordsResponse<SleepSessionRecord> {
+        return ReadRecordsResponse<SleepSessionRecord>(
+            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()),
+            -1)
+    }
+
+    private fun getWeekSleepRecords(): ReadRecordsResponse<SleepSessionRecord> {
+        return ReadRecordsResponse<SleepSessionRecord>(
+            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(),
+                SleepSessionRecord.Builder(
+                        getMetaData(),
+                        Instant.parse("2023-06-11T22:30:00Z"),
+                        Instant.parse("2023-06-13T07:45:00Z"))
+                    .build(),
+                SleepSessionRecord.Builder(
+                        getMetaData(),
+                        Instant.parse("2023-06-14T22:30:00Z"),
+                        Instant.parse("2023-06-15T07:45:00Z"))
+                    .build()),
+            -1)
+    }
+
+    private fun getMonthSleepRecords(): ReadRecordsResponse<SleepSessionRecord> {
+        return ReadRecordsResponse<SleepSessionRecord>(
+            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(),
+                SleepSessionRecord.Builder(
+                        getMetaData(),
+                        Instant.parse("2023-06-11T22:30:00Z"),
+                        Instant.parse("2023-06-13T07:45:00Z"))
+                    .build(),
+                SleepSessionRecord.Builder(
+                        getMetaData(),
+                        Instant.parse("2023-06-14T22:30:00Z"),
+                        Instant.parse("2023-06-15T07:45:00Z"))
+                    .build(),
+                SleepSessionRecord.Builder(
+                        getMetaData(),
+                        Instant.parse("2023-07-09T22:30:00Z"),
+                        Instant.parse("2023-07-13T07:45:00Z"))
+                    .build()),
+            -1)
+    }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/data/entries/datenavigation/DateNavigationViewTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/data/entries/datenavigation/DateNavigationViewTest.kt
new file mode 100644
index 0000000..0421613
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/data/entries/datenavigation/DateNavigationViewTest.kt
@@ -0,0 +1,263 @@
+/**
+ * Copyright (C) 2022 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.datenavigation
+
+import android.content.Context
+import android.view.View
+import android.widget.ImageButton
+import android.widget.Spinner
+import android.widget.TextView
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.healthconnect.controller.R
+import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod
+import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationView
+import com.android.healthconnect.controller.tests.utils.MIDNIGHT
+import com.android.healthconnect.controller.tests.utils.NOW
+import com.android.healthconnect.controller.tests.utils.TestTimeSource
+import com.android.healthconnect.controller.tests.utils.setLocale
+import com.android.healthconnect.controller.utils.TimeSource
+import com.google.common.truth.Truth.assertThat
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import java.time.Duration
+import java.util.Locale
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito
+
+@HiltAndroidTest
+class DateNavigationViewTest {
+    @get:Rule val hiltRule = HiltAndroidRule(this)
+
+    private lateinit var dateNavigationView: DateNavigationView
+    private lateinit var previousDayButton: ImageButton
+    private lateinit var nextDayButton: ImageButton
+    private lateinit var datePickerSpinner: Spinner
+
+    private lateinit var context: Context
+    private val dateChangedListener =
+        Mockito.mock(DateNavigationView.OnDateChangedListener::class.java)
+    private val timeSource: TimeSource = TestTimeSource
+
+    @Before
+    fun setup() {
+        hiltRule.inject()
+        context = InstrumentationRegistry.getInstrumentation().context
+        context.setLocale(Locale.US)
+
+        dateNavigationView =
+            DateNavigationView(context = context, attrs = null, timeSource = timeSource)
+        datePickerSpinner = dateNavigationView.findViewById(R.id.date_picker_spinner) as Spinner
+        previousDayButton = dateNavigationView.findViewById(R.id.navigation_previous_day)
+        nextDayButton = dateNavigationView.findViewById(R.id.navigation_next_day)
+    }
+
+    @Test
+    fun initDateNavigationPreference_titleSetToToday() {
+        assertThat(datePickerSpinner.visibility).isEqualTo(View.VISIBLE)
+
+        assertSpinnerView("Today")
+    }
+
+    @Test
+    fun setPeriodToDay_yesterday_showsYesterday() {
+        dateNavigationView.setDate(NOW.minus(Duration.ofDays(1)))
+
+        assertSpinnerView("Yesterday")
+    }
+
+    @Test
+    fun setPeriodToDay_twoDaysAgo_showsDate() {
+        dateNavigationView.setDate(NOW.minus(Duration.ofDays(2)))
+
+        assertSpinnerView("Tue, Oct 18")
+    }
+
+    @Test
+    fun setPeriodToDay_thisTimeLastYear_showsYear() {
+        dateNavigationView.setDate(NOW.minus(Duration.ofDays(365)))
+
+        assertSpinnerView("Wed, Oct 20, 2021")
+    }
+
+    @Test
+    fun setPeriodToDay_lastYear_showsYear() {
+        dateNavigationView.setDate(NOW.minus(Duration.ofDays(367)))
+
+        assertSpinnerView("Mon, Oct 18, 2021")
+    }
+
+    @Test
+    fun setPeriodToWeek_navigateToMonday_showsThisWeek() {
+        dateNavigationView.setDate(NOW.minus(Duration.ofDays(3)))
+        dateNavigationView.setPeriod(DateNavigationPeriod.PERIOD_WEEK)
+
+        assertSpinnerView("This week")
+    }
+
+    @Test
+    fun setPeriodToWeek_navigateToSunday_showsLastWeek() {
+        dateNavigationView.setDate(NOW.minus(Duration.ofDays(4)))
+        dateNavigationView.setPeriod(DateNavigationPeriod.PERIOD_WEEK)
+
+        assertSpinnerView("Last week")
+    }
+
+    @Test
+    fun setPeriodToWeek_twoWeeksAgo_showsTwoWeeksAgo() {
+        dateNavigationView.setDate(NOW.minus(Duration.ofDays(14)))
+        dateNavigationView.setPeriod(DateNavigationPeriod.PERIOD_WEEK)
+
+        assertSpinnerView("Oct 3 – 9")
+    }
+
+    @Test
+    fun setPeriodToWeek_thisTimeLastYear_showsYear() {
+        dateNavigationView.setDate(NOW.minus(Duration.ofDays(365)))
+        dateNavigationView.setPeriod(DateNavigationPeriod.PERIOD_WEEK)
+
+        assertSpinnerView("Oct 18 – 24, 2021")
+    }
+
+    @Test
+    fun setPeriodToWeek_lastYear_showsYear() {
+        dateNavigationView.setDate(NOW.minus(Duration.ofDays(379)))
+        dateNavigationView.setPeriod(DateNavigationPeriod.PERIOD_WEEK)
+
+        assertSpinnerView("Oct 4 – 10, 2021")
+    }
+
+    @Test
+    fun setPeriodToWeek_spansAcrossMonths_showsBothMonths() {
+        dateNavigationView.setDate(NOW.minus(Duration.ofDays(21)))
+        dateNavigationView.setPeriod(DateNavigationPeriod.PERIOD_WEEK)
+
+        assertSpinnerView("Sep 26 – Oct 2")
+    }
+
+    @Test
+    fun setPeriodToMonth_thisMonth_showsThisMonth() {
+        dateNavigationView.setPeriod(DateNavigationPeriod.PERIOD_MONTH)
+
+        assertSpinnerView("This month")
+    }
+
+    @Test
+    fun setPeriodToMonth_navigateToFirstDayOfMonth_showsThisMonth() {
+        dateNavigationView.setDate(NOW.minus(Duration.ofDays(19)))
+        dateNavigationView.setPeriod(DateNavigationPeriod.PERIOD_MONTH)
+
+        assertSpinnerView("This month")
+    }
+
+    @Test
+    fun setPeriodToMonth_navigateToLastDayOfPreviousMonth_showsLastMonth() {
+        dateNavigationView.setDate(NOW.minus(Duration.ofDays(21)))
+        dateNavigationView.setPeriod(DateNavigationPeriod.PERIOD_MONTH)
+
+        assertSpinnerView("Last month")
+    }
+
+    @Test
+    fun setPeriodToMonth_twoMonthsAgo_showsTwoMonthsAgo() {
+        dateNavigationView.setDate(NOW.minus(Duration.ofDays(60)))
+        dateNavigationView.setPeriod(DateNavigationPeriod.PERIOD_MONTH)
+
+        assertSpinnerView("August")
+    }
+
+    @Test
+    fun setPeriodToMonth_thisTimeLastYear_showsYear() {
+        dateNavigationView.setDate(NOW.minus(Duration.ofDays(365)))
+        dateNavigationView.setPeriod(DateNavigationPeriod.PERIOD_MONTH)
+
+        assertSpinnerView("October 2021")
+    }
+
+    @Test
+    fun setPeriodToMonth_lastYear_showsYear() {
+        dateNavigationView.setDate(NOW.minus(Duration.ofDays(425)))
+        dateNavigationView.setPeriod(DateNavigationPeriod.PERIOD_MONTH)
+
+        assertSpinnerView("August 2021")
+    }
+
+    @Test
+    fun initDateNavigationPreference_nextNavigationDisabled() {
+        assertThat(nextDayButton.visibility).isEqualTo(View.VISIBLE)
+        assertThat(nextDayButton.isEnabled).isEqualTo(false)
+    }
+
+    @Test
+    fun initDateNavigationPreference_prevNavigationEnabled() {
+        assertThat(previousDayButton.visibility).isEqualTo(View.VISIBLE)
+        assertThat(previousDayButton.isEnabled).isEqualTo(true)
+    }
+
+    @Test
+    fun setDate_withValidFutureDates_nextButtonIsEnabled() {
+        dateNavigationView.setDate(NOW.minus(Duration.ofDays(1)))
+
+        assertThat(nextDayButton.isEnabled).isEqualTo(true)
+    }
+
+    @Test
+    fun onDateChanged_setDate_listenerIsCalled() {
+        dateNavigationView.setPeriod(DateNavigationPeriod.PERIOD_DAY)
+        dateNavigationView.setDateChangedListener(dateChangedListener)
+
+        val newDate = NOW.minus(Duration.ofDays(1))
+        dateNavigationView.setDate(newDate)
+
+        val expectedDate = MIDNIGHT.minus(Duration.ofDays(1))
+        Mockito.verify(dateChangedListener)
+            .onDateChanged(expectedDate, DateNavigationPeriod.PERIOD_DAY)
+    }
+
+    @Test
+    fun onDateChanged_setPeriod_listenerIsCalled() {
+        dateNavigationView.setDate(NOW)
+        dateNavigationView.setDateChangedListener(dateChangedListener)
+
+        dateNavigationView.setPeriod(DateNavigationPeriod.PERIOD_WEEK)
+
+        // Expected date is the beginning of the week.
+        val expectedDate = MIDNIGHT.minus(Duration.ofDays(3))
+        Mockito.verify(dateChangedListener)
+            .onDateChanged(expectedDate, DateNavigationPeriod.PERIOD_WEEK)
+    }
+
+    @Test
+    fun checkDropDowns_dayWeekMonthShown() {
+        assertSpinnerDropDownView("Day", position = 0)
+        assertSpinnerDropDownView("Week", position = 1)
+        assertSpinnerDropDownView("Month", position = 2)
+    }
+
+    private fun assertSpinnerView(expected: String) {
+        val textView: TextView =
+            datePickerSpinner.adapter.getView(
+                datePickerSpinner.selectedItemPosition, null, datePickerSpinner) as TextView
+        assertThat(textView.text).isEqualTo(expected)
+    }
+
+    private fun assertSpinnerDropDownView(expected: String, position: Int) {
+        val textView: TextView =
+            datePickerSpinner.adapter.getDropDownView(position, null, datePickerSpinner) as TextView
+        assertThat(textView.text).isEqualTo(expected)
+    }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/data/entriesandaccess/EntriesAndAccessFragmentTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/data/entriesandaccess/EntriesAndAccessFragmentTest.kt
new file mode 100644
index 0000000..a42926f
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/data/entriesandaccess/EntriesAndAccessFragmentTest.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.entriesandaccess
+
+import androidx.core.os.bundleOf
+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.data.entriesandaccess.EntriesAndAccessFragment
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType.STEPS
+import com.android.healthconnect.controller.permissiontypes.HealthPermissionTypesFragment.Companion.PERMISSION_TYPE_KEY
+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 EntriesAndAccessFragmentTest {
+    @get:Rule val hiltRule = HiltAndroidRule(this)
+
+    @Before
+    fun setup() {
+        hiltRule.inject()
+    }
+
+    @Test
+    fun entriesAndAccessInit_showsTabs() {
+        launchFragment<EntriesAndAccessFragment>(bundleOf(PERMISSION_TYPE_KEY to STEPS))
+
+        onView(withText("Entries")).check(matches(isDisplayed()))
+        onView(withText("Access")).check(matches(isDisplayed()))
+    }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/dataaccess/HealthDataAccessFragmentTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/dataaccess/HealthDataAccessFragmentTest.kt
index fcddf2c..f56eebc 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/dataaccess/HealthDataAccessFragmentTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/dataaccess/HealthDataAccessFragmentTest.kt
@@ -25,11 +25,11 @@
 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.dataaccess.DataAccessAppState
+import com.android.healthconnect.controller.data.access.AccessViewModel
+import com.android.healthconnect.controller.data.access.AccessViewModel.AccessScreenState
+import com.android.healthconnect.controller.data.access.AccessViewModel.AccessScreenState.WithData
+import com.android.healthconnect.controller.data.access.AppAccessState
 import com.android.healthconnect.controller.dataaccess.HealthDataAccessFragment
-import com.android.healthconnect.controller.dataaccess.HealthDataAccessViewModel
-import com.android.healthconnect.controller.dataaccess.HealthDataAccessViewModel.DataAccessScreenState
-import com.android.healthconnect.controller.dataaccess.HealthDataAccessViewModel.DataAccessScreenState.WithData
 import com.android.healthconnect.controller.permissions.data.HealthPermissionType
 import com.android.healthconnect.controller.permissiontypes.HealthPermissionTypesFragment.Companion.PERMISSION_TYPE_KEY
 import com.android.healthconnect.controller.shared.app.AppMetadata
@@ -49,8 +49,7 @@
 
     @get:Rule val hiltRule = HiltAndroidRule(this)
 
-    @BindValue
-    val viewModel: HealthDataAccessViewModel = Mockito.mock(HealthDataAccessViewModel::class.java)
+    @BindValue val viewModel: AccessViewModel = Mockito.mock(AccessViewModel::class.java)
 
     @Before
     fun setup() {
@@ -60,7 +59,7 @@
     @Test
     fun dataAccessFragment_noSections_noneDisplayed() {
         whenever(viewModel.appMetadataMap).then {
-            MutableLiveData<DataAccessScreenState>(WithData(emptyMap()))
+            MutableLiveData<AccessScreenState>(WithData(emptyMap()))
         }
         launchFragment<HealthDataAccessFragment>(distanceBundle())
 
@@ -80,11 +79,11 @@
     fun dataAccessFragment_readSection_isDisplayed() {
         val map =
             mapOf(
-                DataAccessAppState.Read to listOf(AppMetadata("package1", "appName1", null)),
-                DataAccessAppState.Write to emptyList(),
-                DataAccessAppState.Inactive to emptyList())
+                AppAccessState.Read to listOf(AppMetadata("package1", "appName1", null)),
+                AppAccessState.Write to emptyList(),
+                AppAccessState.Inactive to emptyList())
         whenever(viewModel.appMetadataMap).then {
-            MutableLiveData<DataAccessScreenState>(WithData(map))
+            MutableLiveData<AccessScreenState>(WithData(map))
         }
         launchFragment<HealthDataAccessFragment>(distanceBundle())
 
@@ -104,11 +103,11 @@
     fun dataAccessFragment_readAndWriteSections_isDisplayed() {
         val map =
             mapOf(
-                DataAccessAppState.Read to listOf(AppMetadata("package1", "appName1", null)),
-                DataAccessAppState.Write to listOf(AppMetadata("package1", "appName1", null)),
-                DataAccessAppState.Inactive to emptyList())
+                AppAccessState.Read to listOf(AppMetadata("package1", "appName1", null)),
+                AppAccessState.Write to listOf(AppMetadata("package1", "appName1", null)),
+                AppAccessState.Inactive to emptyList())
         whenever(viewModel.appMetadataMap).then {
-            MutableLiveData<DataAccessScreenState>(WithData(map))
+            MutableLiveData<AccessScreenState>(WithData(map))
         }
         launchFragment<HealthDataAccessFragment>(distanceBundle())
 
@@ -128,11 +127,11 @@
     fun dataAccessFragment_inactiveSection_isDisplayed() {
         val map =
             mapOf(
-                DataAccessAppState.Read to emptyList(),
-                DataAccessAppState.Write to emptyList(),
-                DataAccessAppState.Inactive to listOf(AppMetadata("package1", "appName1", null)))
+                AppAccessState.Read to emptyList(),
+                AppAccessState.Write to emptyList(),
+                AppAccessState.Inactive to listOf(AppMetadata("package1", "appName1", null)))
         whenever(viewModel.appMetadataMap).then {
-            MutableLiveData<DataAccessScreenState>(WithData(map))
+            MutableLiveData<AccessScreenState>(WithData(map))
         }
         launchFragment<HealthDataAccessFragment>(distanceBundle())
 
@@ -151,7 +150,7 @@
     @Test
     fun dataAccessFragment_loadingState_showsLoading() {
         whenever(viewModel.appMetadataMap).then {
-            MutableLiveData<DataAccessScreenState>(DataAccessScreenState.Loading)
+            MutableLiveData<AccessScreenState>(AccessScreenState.Loading)
         }
         launchFragment<HealthDataAccessFragment>(distanceBundle())
         onView(withId(R.id.progress_indicator)).check(matches(isDisplayed()))
@@ -160,7 +159,7 @@
     @Test
     fun dataAccessFragment_withData_hidesLoading() {
         whenever(viewModel.appMetadataMap).then {
-            MutableLiveData<DataAccessScreenState>(WithData(emptyMap()))
+            MutableLiveData<AccessScreenState>(WithData(emptyMap()))
         }
         launchFragment<HealthDataAccessFragment>(distanceBundle())
         onView(withId(R.id.progress_indicator)).check(matches(not(isDisplayed())))
@@ -169,7 +168,7 @@
     @Test
     fun dataAccessFragment_withError_showError() {
         whenever(viewModel.appMetadataMap).then {
-            MutableLiveData<DataAccessScreenState>(DataAccessScreenState.Error)
+            MutableLiveData<AccessScreenState>(AccessScreenState.Error)
         }
         launchFragment<HealthDataAccessFragment>(distanceBundle())
         onView(withId(R.id.progress_indicator)).check(matches(not(isDisplayed())))
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/DataEntriesFragmentTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/DataEntriesFragmentTest.kt
index 1aa015c..9e9da5d 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/DataEntriesFragmentTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/DataEntriesFragmentTest.kt
@@ -25,13 +25,13 @@
 import androidx.test.espresso.matcher.ViewMatchers.withText
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.healthconnect.controller.R
+import com.android.healthconnect.controller.data.entries.FormattedEntry.FormattedDataEntry
 import com.android.healthconnect.controller.dataentries.DataEntriesFragment
 import com.android.healthconnect.controller.dataentries.DataEntriesFragmentViewModel
 import com.android.healthconnect.controller.dataentries.DataEntriesFragmentViewModel.DataEntriesFragmentState.Empty
 import com.android.healthconnect.controller.dataentries.DataEntriesFragmentViewModel.DataEntriesFragmentState.Loading
 import com.android.healthconnect.controller.dataentries.DataEntriesFragmentViewModel.DataEntriesFragmentState.LoadingFailed
 import com.android.healthconnect.controller.dataentries.DataEntriesFragmentViewModel.DataEntriesFragmentState.WithData
-import com.android.healthconnect.controller.dataentries.FormattedEntry.FormattedDataEntry
 import com.android.healthconnect.controller.permissions.data.HealthPermissionType.STEPS
 import com.android.healthconnect.controller.permissiontypes.HealthPermissionTypesFragment.Companion.PERMISSION_TYPE_KEY
 import com.android.healthconnect.controller.shared.DataType
@@ -41,13 +41,13 @@
 import dagger.hilt.android.testing.BindValue
 import dagger.hilt.android.testing.HiltAndroidRule
 import dagger.hilt.android.testing.HiltAndroidTest
+import java.time.ZoneId
+import java.util.Locale
+import java.util.TimeZone
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.mockito.Mockito
-import java.time.ZoneId
-import java.util.Locale
-import java.util.TimeZone
 
 @HiltAndroidTest
 class DataEntriesFragmentTest {
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/CyclingPedalingCadenceFormatterTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/CyclingPedalingCadenceFormatterTest.kt
index c78e2a2..b9c00d5 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/CyclingPedalingCadenceFormatterTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/CyclingPedalingCadenceFormatterTest.kt
@@ -19,7 +19,7 @@
 import android.health.connect.datatypes.CyclingPedalingCadenceRecord
 import android.health.connect.datatypes.CyclingPedalingCadenceRecord.CyclingPedalingCadenceRecordSample
 import androidx.test.platform.app.InstrumentationRegistry
-import com.android.healthconnect.controller.dataentries.FormattedEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry
 import com.android.healthconnect.controller.dataentries.formatters.CyclingPedalingCadenceFormatter
 import com.android.healthconnect.controller.dataentries.units.UnitPreferences
 import com.android.healthconnect.controller.tests.utils.NOW
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/ExerciseSessionFormatterTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/ExerciseSessionFormatterTest.kt
index 1f0fc00..23b4f68 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/ExerciseSessionFormatterTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/ExerciseSessionFormatterTest.kt
@@ -27,7 +27,7 @@
 import android.health.connect.datatypes.ExerciseSessionType.EXERCISE_SESSION_TYPE_OTHER_WORKOUT
 import android.health.connect.datatypes.units.Length
 import androidx.test.platform.app.InstrumentationRegistry
-import com.android.healthconnect.controller.dataentries.FormattedEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry
 import com.android.healthconnect.controller.dataentries.formatters.ExerciseSessionFormatter
 import com.android.healthconnect.controller.tests.utils.NOW
 import com.android.healthconnect.controller.tests.utils.getMetaData
@@ -62,17 +62,13 @@
 
     @Test
     fun formatValue() = runBlocking {
-        assertThat(
-                formatter.formatValue(
-                    getRecord(type = EXERCISE_SESSION_TYPE_BIKING)))
+        assertThat(formatter.formatValue(getRecord(type = EXERCISE_SESSION_TYPE_BIKING)))
             .isEqualTo("16 m, Cycling")
     }
 
     @Test
     fun formatA11yValue() = runBlocking {
-        assertThat(
-                formatter.formatA11yValue(
-                    getRecord(type = EXERCISE_SESSION_TYPE_BIKING)))
+        assertThat(formatter.formatA11yValue(getRecord(type = EXERCISE_SESSION_TYPE_BIKING)))
             .isEqualTo("16 minutes, Cycling")
     }
 
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/HealthDataEntryFormatterTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/HealthDataEntryFormatterTest.kt
index 3ffca18..b934bfa 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/HealthDataEntryFormatterTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/HealthDataEntryFormatterTest.kt
@@ -17,8 +17,8 @@
 
 import android.content.Context
 import androidx.test.platform.app.InstrumentationRegistry
-import com.android.healthconnect.controller.dataentries.FormattedEntry.FormattedDataEntry
-import com.android.healthconnect.controller.dataentries.FormattedEntry.SeriesDataEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry.FormattedDataEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry.SeriesDataEntry
 import com.android.healthconnect.controller.dataentries.formatters.shared.HealthDataEntryFormatter
 import com.android.healthconnect.controller.shared.DataType
 import com.android.healthconnect.controller.tests.utils.getBasalMetabolicRateRecord
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/HeartRateFormatterTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/HeartRateFormatterTest.kt
index 6dd9249..9b94265 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/HeartRateFormatterTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/HeartRateFormatterTest.kt
@@ -18,7 +18,7 @@
 import android.content.Context
 import android.health.connect.datatypes.HeartRateRecord
 import androidx.test.platform.app.InstrumentationRegistry
-import com.android.healthconnect.controller.dataentries.FormattedEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry
 import com.android.healthconnect.controller.dataentries.formatters.HeartRateFormatter
 import com.android.healthconnect.controller.dataentries.units.UnitPreferences
 import com.android.healthconnect.controller.tests.utils.getHeartRateRecord
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/MenstruationPeriodFormatterTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/MenstruationPeriodFormatterTest.kt
index ae58413..0cdc5ee 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/MenstruationPeriodFormatterTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/MenstruationPeriodFormatterTest.kt
@@ -21,7 +21,7 @@
 import android.content.Context
 import android.health.connect.datatypes.MenstruationPeriodRecord
 import androidx.test.platform.app.InstrumentationRegistry
-import com.android.healthconnect.controller.dataentries.FormattedEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry
 import com.android.healthconnect.controller.dataentries.formatters.MenstruationPeriodFormatter
 import com.android.healthconnect.controller.dataentries.units.UnitPreferences
 import com.android.healthconnect.controller.shared.DataType
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/PowerFormatterTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/PowerFormatterTest.kt
index 13b92c3..7783906 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/PowerFormatterTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/PowerFormatterTest.kt
@@ -19,7 +19,7 @@
 import android.health.connect.datatypes.PowerRecord
 import android.health.connect.datatypes.units.Power
 import androidx.test.platform.app.InstrumentationRegistry
-import com.android.healthconnect.controller.dataentries.FormattedEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry
 import com.android.healthconnect.controller.dataentries.formatters.PowerFormatter
 import com.android.healthconnect.controller.dataentries.units.UnitPreferences
 import com.android.healthconnect.controller.tests.utils.NOW
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/SleepSessionFormatterTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/SleepSessionFormatterTest.kt
index e0f0d3e..7e49a1a 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/SleepSessionFormatterTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/SleepSessionFormatterTest.kt
@@ -27,7 +27,7 @@
 import android.health.connect.datatypes.SleepSessionRecord.StageType.STAGE_TYPE_SLEEPING_LIGHT
 import android.health.connect.datatypes.SleepSessionRecord.StageType.STAGE_TYPE_SLEEPING_REM
 import androidx.test.platform.app.InstrumentationRegistry
-import com.android.healthconnect.controller.dataentries.FormattedEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry
 import com.android.healthconnect.controller.dataentries.formatters.SleepSessionFormatter
 import com.android.healthconnect.controller.dataentries.units.UnitPreferences
 import com.android.healthconnect.controller.tests.utils.NOW
@@ -36,6 +36,8 @@
 import com.google.common.truth.Truth.assertThat
 import dagger.hilt.android.testing.HiltAndroidRule
 import dagger.hilt.android.testing.HiltAndroidTest
+import java.time.Duration
+import java.time.Instant
 import java.time.ZoneId
 import java.time.temporal.ChronoUnit
 import java.util.Locale
@@ -163,6 +165,24 @@
         assertThat(stage.title).isEqualTo("1h out of bed")
     }
 
+    @Test
+    fun formatUnit_showsAmountOfSleep() {
+        val startTime = Instant.parse("2023-02-11T21:20:00Z")
+        val endTime = Instant.parse("2023-02-12T08:14:00Z")
+        val totalSleepTime = Duration.between(startTime, endTime).toMillis()
+
+        assertThat(formatter.formatUnit(totalSleepTime)).isEqualTo("10h 54m")
+    }
+
+    @Test
+    fun formatA11yUnit_showsAmountOfSleep() {
+        val startTime = Instant.parse("2023-02-11T21:20:00Z")
+        val endTime = Instant.parse("2023-02-12T08:14:00Z")
+        val totalSleepTime = Duration.between(startTime, endTime).toMillis()
+
+        assertThat(formatter.formatA11yUnit(totalSleepTime)).isEqualTo("10 hours 54 minutes")
+    }
+
     private fun getRecord(
         title: String = "",
         note: String = "",
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/SpeedFormatterTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/SpeedFormatterTest.kt
index 9e1dff1..8456cd7 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/SpeedFormatterTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/SpeedFormatterTest.kt
@@ -19,7 +19,7 @@
 import android.health.connect.datatypes.SpeedRecord
 import android.health.connect.datatypes.units.Velocity
 import androidx.test.platform.app.InstrumentationRegistry
-import com.android.healthconnect.controller.dataentries.FormattedEntry.FormattedSessionDetail
+import com.android.healthconnect.controller.data.entries.FormattedEntry.FormattedSessionDetail
 import com.android.healthconnect.controller.dataentries.formatters.SpeedFormatter
 import com.android.healthconnect.controller.dataentries.units.DistanceUnit
 import com.android.healthconnect.controller.dataentries.units.DistanceUnit.KILOMETERS
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/StepsCadenceFormatterTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/StepsCadenceFormatterTest.kt
index 0af0a8a..cfe9576 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/StepsCadenceFormatterTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/dataentries/formatters/StepsCadenceFormatterTest.kt
@@ -18,7 +18,7 @@
 import android.content.Context
 import android.health.connect.datatypes.StepsCadenceRecord
 import androidx.test.platform.app.InstrumentationRegistry
-import com.android.healthconnect.controller.dataentries.FormattedEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry
 import com.android.healthconnect.controller.dataentries.formatters.StepsCadenceFormatter
 import com.android.healthconnect.controller.dataentries.units.UnitPreferences
 import com.android.healthconnect.controller.tests.utils.NOW
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/datasources/AddAnAppFragmentTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/datasources/AddAnAppFragmentTest.kt
new file mode 100644
index 0000000..15e0aad
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/datasources/AddAnAppFragmentTest.kt
@@ -0,0 +1,111 @@
+/*
+ * 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.datasources
+
+import android.health.connect.HealthDataCategory
+import androidx.core.os.bundleOf
+import androidx.lifecycle.MutableLiveData
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers
+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.categories.HealthDataCategoriesFragment.Companion.CATEGORY_KEY
+import com.android.healthconnect.controller.datasources.AddAnAppFragment
+import com.android.healthconnect.controller.datasources.DataSourcesViewModel
+import com.android.healthconnect.controller.datasources.DataSourcesViewModel.DataSourcesInfo
+import com.android.healthconnect.controller.datasources.DataSourcesViewModel.PriorityListState
+import com.android.healthconnect.controller.datasources.DataSourcesViewModel.PotentialAppSourcesState
+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_NAME
+import com.android.healthconnect.controller.tests.utils.TEST_APP_NAME_2
+import com.android.healthconnect.controller.tests.utils.TEST_APP_NAME_3
+import com.android.healthconnect.controller.tests.utils.launchFragment
+import com.android.healthconnect.controller.tests.utils.whenever
+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
+
+@HiltAndroidTest
+class AddAnAppFragmentTest {
+    @get:Rule
+    val hiltRule = HiltAndroidRule(this)
+
+    @BindValue
+    val dataSourcesViewModel: DataSourcesViewModel =
+        Mockito.mock(DataSourcesViewModel::class.java)
+
+    @Before
+    fun setup() {
+        hiltRule.inject()
+    }
+
+    @Test
+    fun fragmentIsDisplayed() {
+        whenever(dataSourcesViewModel.dataSourcesInfo).then {
+            MutableLiveData(
+                DataSourcesInfo(
+                    priorityListState = PriorityListState.WithData(true, listOf()),
+                    potentialAppSourcesState = PotentialAppSourcesState.WithData(true, listOf(
+                        TEST_APP,
+                        TEST_APP_2,
+                        TEST_APP_3
+                    ))))
+        }
+
+        launchFragment<AddAnAppFragment>(bundleOf(CATEGORY_KEY to HealthDataCategory.ACTIVITY))
+
+        onView(withText(TEST_APP_NAME)).check(matches(isDisplayed()))
+        onView(withText(TEST_APP_NAME_2)).check(matches(isDisplayed()))
+        onView(withText(TEST_APP_NAME_3)).check(matches(isDisplayed()))
+    }
+
+    @Test
+    fun showsLoading_whenAppSourcesLoading() {
+        whenever(dataSourcesViewModel.dataSourcesInfo).then {
+            MutableLiveData(
+                DataSourcesInfo(
+                    priorityListState = PriorityListState.WithData(true, listOf()),
+                    potentialAppSourcesState = PotentialAppSourcesState.Loading(true)))
+        }
+
+        launchFragment<AddAnAppFragment>(bundleOf(CATEGORY_KEY to HealthDataCategory.ACTIVITY))
+
+        onView(ViewMatchers.withId(R.id.progress_indicator)).check(matches(isDisplayed()))
+    }
+
+    @Test
+    fun showsError_whenAppSourcesError() {
+        whenever(dataSourcesViewModel.dataSourcesInfo).then {
+            MutableLiveData(
+                DataSourcesInfo(
+                    priorityListState = PriorityListState.WithData(true, listOf()),
+                    potentialAppSourcesState = PotentialAppSourcesState.LoadingFailed(true)))
+        }
+
+        launchFragment<AddAnAppFragment>(bundleOf(CATEGORY_KEY to HealthDataCategory.ACTIVITY))
+
+        onView(ViewMatchers.withId(R.id.error_view)).check(matches(isDisplayed()))
+
+    }
+}
\ No newline at end of file
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 2dd6e26..3b5f49e 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
@@ -1,88 +1,602 @@
+/*
+ * 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.datasources
 
+import android.health.connect.HealthDataCategory
 import android.os.Bundle
 import androidx.lifecycle.MutableLiveData
+import androidx.test.espresso.Espresso.onIdle
 import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
 import androidx.test.espresso.assertion.ViewAssertions.matches
 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 androidx.test.platform.app.InstrumentationRegistry
 import com.android.healthconnect.controller.R
+import com.android.healthconnect.controller.data.entries.FormattedEntry
+import com.android.healthconnect.controller.datasources.AggregationCardInfo
 import com.android.healthconnect.controller.datasources.DataSourcesFragment
-import com.android.healthconnect.controller.permissiontypes.HealthPermissionTypesViewModel
-import com.android.healthconnect.controller.shared.app.AppMetadata
+import com.android.healthconnect.controller.datasources.DataSourcesViewModel
+import com.android.healthconnect.controller.datasources.DataSourcesViewModel.AggregationCardsState
+import com.android.healthconnect.controller.datasources.DataSourcesViewModel.DataSourcesAndAggregationsInfo
+import com.android.healthconnect.controller.datasources.DataSourcesViewModel.PotentialAppSourcesState
+import com.android.healthconnect.controller.datasources.DataSourcesViewModel.PriorityListState
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType
+import com.android.healthconnect.controller.shared.app.AppUtils
+import com.android.healthconnect.controller.shared.app.AppUtilsModule
 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_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.atPosition
+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 dagger.hilt.android.testing.BindValue
 import dagger.hilt.android.testing.HiltAndroidRule
 import dagger.hilt.android.testing.HiltAndroidTest
+import dagger.hilt.android.testing.UninstallModules
+import java.time.Instant
+import java.time.ZoneId
+import java.util.Locale
+import java.util.TimeZone
 import org.hamcrest.Matchers.allOf
+import org.hamcrest.Matchers.not
+import org.junit.After
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.mockito.Mockito
 
+@UninstallModules(AppUtilsModule::class)
 @HiltAndroidTest
 class DataSourcesFragmentTest {
 
-    @get:Rule
-    val hiltRule = HiltAndroidRule(this)
+    @get:Rule val hiltRule = HiltAndroidRule(this)
 
     @BindValue
-    val viewModel: HealthPermissionTypesViewModel =
-            Mockito.mock(HealthPermissionTypesViewModel::class.java)
+    val dataSourcesViewModel: DataSourcesViewModel = Mockito.mock(DataSourcesViewModel::class.java)
+    @BindValue val appUtils: AppUtils = FakeAppUtils()
 
     @Before
     fun setup() {
+        val context = InstrumentationRegistry.getInstrumentation().context
+        context.setLocale(Locale.US)
+        TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("UTC")))
         hiltRule.inject()
+        whenever(dataSourcesViewModel.getCurrentSelection()).then { HealthDataCategory.ACTIVITY }
+    }
+
+    @After
+    fun tearDown() {
+        (appUtils as FakeAppUtils).reset()
     }
 
     @Test
-    fun dataSourcesFragment_withTwoSources_isDisplayed() {
-        Mockito.`when`(viewModel.priorityList).then {
-            MutableLiveData<HealthPermissionTypesViewModel.PriorityListState>(
-                    HealthPermissionTypesViewModel.PriorityListState.WithData(
-                            listOf(TEST_APP, TEST_APP_2)))
+    fun twoActivitySources_noDataTotals_isDisplayed() {
+        whenever(dataSourcesViewModel.dataSourcesAndAggregationsInfo).then {
+            MutableLiveData(
+                DataSourcesAndAggregationsInfo(
+                    priorityListState =
+                        PriorityListState.WithData(true, listOf(TEST_APP, TEST_APP_2)),
+                    potentialAppSourcesState = PotentialAppSourcesState.WithData(true, listOf()),
+                    aggregationCardsState = AggregationCardsState.WithData(true, listOf())))
         }
-        Mockito.`when`(viewModel.editedPriorityList).then {
-            MutableLiveData<List<AppMetadata>>(listOf(TEST_APP, TEST_APP_2))
+        whenever(dataSourcesViewModel.getEditedPriorityList()).then { listOf(TEST_APP, TEST_APP_2) }
+        whenever(dataSourcesViewModel.updatedAggregationCardsData).then {
+            MutableLiveData(AggregationCardsState.WithData(true, listOf()))
+        }
+        launchFragment<DataSourcesFragment>(Bundle())
+        onIdle()
+
+        onView(withText("Activity")).check(matches(isDisplayed()))
+        onView(withText("Data totals")).check(doesNotExist())
+        onView(withText("App sources")).check(matches(isDisplayed()))
+        onView(withText("Add an app")).check(doesNotExist())
+        onView(
+                withText(
+                    "Add app sources to the list to see how the data " +
+                        "totals can change. Removing an app from this list will stop it " +
+                        "from contributing to totals, but it will still have write permissions."))
+            .check(matches(isDisplayed()))
+
+        onView(withId(R.id.linear_layout_recycle_view))
+            .check(
+                matches(
+                    atPosition(
+                        0,
+                        allOf(
+                            hasDescendant(withText("1")), hasDescendant(withText(TEST_APP_NAME))))))
+        onView(withId(R.id.linear_layout_recycle_view))
+            .check(
+                matches(
+                    atPosition(
+                        1,
+                        allOf(
+                            hasDescendant(withText("2")),
+                            hasDescendant(withText(TEST_APP_NAME_2))))))
+    }
+
+    @Test
+    fun twoActivitySources_oneDataTotal_withinLastYear_isDisplayed() {
+        whenever(dataSourcesViewModel.dataSourcesAndAggregationsInfo).then {
+            MutableLiveData(
+                DataSourcesAndAggregationsInfo(
+                    priorityListState =
+                        PriorityListState.WithData(true, listOf(TEST_APP, TEST_APP_2)),
+                    potentialAppSourcesState = PotentialAppSourcesState.WithData(true, listOf()),
+                    aggregationCardsState =
+                        AggregationCardsState.WithData(
+                            true,
+                            listOf(
+                                AggregationCardInfo(
+                                    HealthPermissionType.STEPS,
+                                    FormattedEntry.FormattedAggregation(
+                                        "1234 steps", "1234 steps", "TestApp"),
+                                    Instant.parse("2022-10-19T07:06:05.432Z"))))))
+        }
+
+        whenever(dataSourcesViewModel.getEditedPriorityList())
+            .thenReturn(listOf(TEST_APP, TEST_APP_2))
+        whenever(dataSourcesViewModel.updatedAggregationCardsData).then {
+            MutableLiveData(
+                AggregationCardsState.WithData(
+                    true,
+                    listOf(
+                        AggregationCardInfo(
+                            HealthPermissionType.STEPS,
+                            FormattedEntry.FormattedAggregation(
+                                "1234 steps", "1234 steps", "TestApp"),
+                            Instant.parse("2022-10-19T07:06:05.432Z")))))
         }
         launchFragment<DataSourcesFragment>(Bundle())
 
         onView(withText("Activity")).check(matches(isDisplayed()))
+        onView(withText("Data totals")).check(matches(isDisplayed()))
+        onView(withText("1234 steps")).check(matches(isDisplayed()))
+        onView(withText("October 19")).check(matches(isDisplayed()))
         onView(withText("App sources")).check(matches(isDisplayed()))
-        onView(withText("Add app sources to the list to see how the data " +
-                "totals can change. Removing an app from this list will stop it " +
-                "from contributing to totals, but it will still have write permissions."))
-                .check(matches(isDisplayed()))
+        onView(withText("Add an app")).check(doesNotExist())
+        onView(
+                withText(
+                    "Add app sources to the list to see how the data " +
+                        "totals can change. Removing an app from this list will stop it " +
+                        "from contributing to totals, but it will still have write permissions."))
+            .check(matches(isDisplayed()))
 
         onView(withId(R.id.linear_layout_recycle_view))
-                .check(matches(atPosition(0, allOf(hasDescendant(withText("1")),
-                        hasDescendant(withText(TEST_APP_NAME))))))
+            .check(
+                matches(
+                    atPosition(
+                        0,
+                        allOf(
+                            hasDescendant(withText("1")), hasDescendant(withText(TEST_APP_NAME))))))
         onView(withId(R.id.linear_layout_recycle_view))
-                .check(matches(atPosition(1, allOf(hasDescendant(withText("2")),
-                        hasDescendant(withText(TEST_APP_NAME_2))))))
+            .check(
+                matches(
+                    atPosition(
+                        1,
+                        allOf(
+                            hasDescendant(withText("2")),
+                            hasDescendant(withText(TEST_APP_NAME_2))))))
     }
 
     @Test
-    fun dataSourcesFragment_withNoSources_displaysEmptyState() {
-        Mockito.`when`(viewModel.priorityList).then {
-            MutableLiveData<HealthPermissionTypesViewModel.PriorityListState>(
-                    HealthPermissionTypesViewModel.PriorityListState.WithData(listOf()))
+    fun oneActivityDataTotal_olderThanOneYear_displaysYear() {
+        whenever(dataSourcesViewModel.dataSourcesAndAggregationsInfo).then {
+            MutableLiveData(
+                DataSourcesAndAggregationsInfo(
+                    priorityListState =
+                        PriorityListState.WithData(true, listOf(TEST_APP, TEST_APP_2)),
+                    potentialAppSourcesState = PotentialAppSourcesState.WithData(true, listOf()),
+                    aggregationCardsState =
+                        AggregationCardsState.WithData(
+                            true,
+                            listOf(
+                                AggregationCardInfo(
+                                    HealthPermissionType.STEPS,
+                                    FormattedEntry.FormattedAggregation(
+                                        "1234 steps", "1234 steps", "TestApp"),
+                                    Instant.parse("2020-10-19T07:06:05.432Z"))))))
         }
-        Mockito.`when`(viewModel.editedPriorityList).then {
-            MutableLiveData<List<AppMetadata>>(listOf())
+
+        whenever(dataSourcesViewModel.getEditedPriorityList())
+            .thenReturn(listOf(TEST_APP, TEST_APP_2))
+        whenever(dataSourcesViewModel.updatedAggregationCardsData).then {
+            MutableLiveData(
+                AggregationCardsState.WithData(
+                    true,
+                    listOf(
+                        AggregationCardInfo(
+                            HealthPermissionType.STEPS,
+                            FormattedEntry.FormattedAggregation(
+                                "1234 steps", "1234 steps", "TestApp"),
+                            Instant.parse("2020-10-19T07:06:05.432Z")))))
+        }
+        launchFragment<DataSourcesFragment>(Bundle())
+        onView(withText("Data totals")).check(matches(isDisplayed()))
+        onView(withText("1234 steps")).check(matches(isDisplayed()))
+        onView(withText("October 19, 2020")).check(matches(isDisplayed()))
+    }
+
+    @Test
+    fun twoSleepSources_oneDataTotal_withinLastYear_isDisplayed() {
+        whenever(dataSourcesViewModel.getCurrentSelection()).then { HealthDataCategory.SLEEP }
+        whenever(dataSourcesViewModel.dataSourcesAndAggregationsInfo).then {
+            MutableLiveData(
+                DataSourcesAndAggregationsInfo(
+                    priorityListState =
+                        PriorityListState.WithData(true, listOf(TEST_APP, TEST_APP_2)),
+                    potentialAppSourcesState = PotentialAppSourcesState.WithData(true, listOf()),
+                    aggregationCardsState =
+                        AggregationCardsState.WithData(
+                            true,
+                            listOf(
+                                AggregationCardInfo(
+                                    HealthPermissionType.SLEEP,
+                                    FormattedEntry.FormattedAggregation(
+                                        "11h 5m", "11h 5m", "TestApp"),
+                                    Instant.parse("2022-10-18T21:00:00.00Z"),
+                                    Instant.parse("2022-10-19T08:05:00.00Z"))))))
+        }
+
+        whenever(dataSourcesViewModel.getEditedPriorityList())
+            .thenReturn(listOf(TEST_APP, TEST_APP_2))
+        whenever(dataSourcesViewModel.updatedAggregationCardsData).then {
+            MutableLiveData(
+                AggregationCardsState.WithData(
+                    true,
+                    listOf(
+                        AggregationCardInfo(
+                            HealthPermissionType.SLEEP,
+                            FormattedEntry.FormattedAggregation("11h 5m", "11h 5m", "TestApp"),
+                            Instant.parse("2022-10-18T21:00:00.00Z"),
+                            Instant.parse("2022-10-19T08:05:00.00Z")))))
+        }
+
+        launchFragment<DataSourcesFragment>(Bundle())
+
+        onView(withText("Sleep")).check(matches(isDisplayed()))
+        onView(withText("Data totals")).check(matches(isDisplayed()))
+        onView(withText("11h 5m")).check(matches(isDisplayed()))
+        onView(withText("Oct 18 – 19")).check(matches(isDisplayed()))
+        onView(withText("App sources")).check(matches(isDisplayed()))
+        onView(withText("Add an app")).check(doesNotExist())
+        onView(
+                withText(
+                    "Add app sources to the list to see how the data " +
+                        "totals can change. Removing an app from this list will stop it " +
+                        "from contributing to totals, but it will still have write permissions."))
+            .check(matches(isDisplayed()))
+
+        onView(withId(R.id.linear_layout_recycle_view))
+            .check(
+                matches(
+                    atPosition(
+                        0,
+                        allOf(
+                            hasDescendant(withText("1")), hasDescendant(withText(TEST_APP_NAME))))))
+        onView(withId(R.id.linear_layout_recycle_view))
+            .check(
+                matches(
+                    atPosition(
+                        1,
+                        allOf(
+                            hasDescendant(withText("2")),
+                            hasDescendant(withText(TEST_APP_NAME_2))))))
+    }
+
+    @Test
+    fun twoSleepSources_oneDataTotal_olderThanOneYear_isDisplayed() {
+        whenever(dataSourcesViewModel.getCurrentSelection()).then { HealthDataCategory.SLEEP }
+        whenever(dataSourcesViewModel.dataSourcesAndAggregationsInfo).then {
+            MutableLiveData(
+                DataSourcesAndAggregationsInfo(
+                    priorityListState =
+                        PriorityListState.WithData(true, listOf(TEST_APP, TEST_APP_2)),
+                    potentialAppSourcesState = PotentialAppSourcesState.WithData(true, listOf()),
+                    aggregationCardsState =
+                        AggregationCardsState.WithData(
+                            true,
+                            listOf(
+                                AggregationCardInfo(
+                                    HealthPermissionType.SLEEP,
+                                    FormattedEntry.FormattedAggregation(
+                                        "11h 5m", "11h 5m", "TestApp"),
+                                    Instant.parse("2020-10-18T21:00:00.00Z"),
+                                    Instant.parse("2020-10-19T08:05:00.00Z"))))))
+        }
+
+        whenever(dataSourcesViewModel.getEditedPriorityList())
+            .thenReturn(listOf(TEST_APP, TEST_APP_2))
+        whenever(dataSourcesViewModel.updatedAggregationCardsData).then {
+            MutableLiveData(
+                AggregationCardsState.WithData(
+                    true,
+                    listOf(
+                        AggregationCardInfo(
+                            HealthPermissionType.SLEEP,
+                            FormattedEntry.FormattedAggregation("11h 5m", "11h 5m", "TestApp"),
+                            Instant.parse("2020-10-18T21:00:00.00Z"),
+                            Instant.parse("2020-10-19T08:05:00.00Z")))))
+        }
+
+        launchFragment<DataSourcesFragment>(Bundle())
+
+        onView(withText("Sleep")).check(matches(isDisplayed()))
+        onView(withText("Data totals")).check(matches(isDisplayed()))
+        onView(withText("11h 5m")).check(matches(isDisplayed()))
+        onView(withText("Oct 18 – 19, 2020")).check(matches(isDisplayed()))
+        onView(withText("App sources")).check(matches(isDisplayed()))
+        onView(withText("Add an app")).check(doesNotExist())
+        onView(
+                withText(
+                    "Add app sources to the list to see how the data " +
+                        "totals can change. Removing an app from this list will stop it " +
+                        "from contributing to totals, but it will still have write permissions."))
+            .check(matches(isDisplayed()))
+
+        onView(withId(R.id.linear_layout_recycle_view))
+            .check(
+                matches(
+                    atPosition(
+                        0,
+                        allOf(
+                            hasDescendant(withText("1")), hasDescendant(withText(TEST_APP_NAME))))))
+        onView(withId(R.id.linear_layout_recycle_view))
+            .check(
+                matches(
+                    atPosition(
+                        1,
+                        allOf(
+                            hasDescendant(withText("2")),
+                            hasDescendant(withText(TEST_APP_NAME_2))))))
+    }
+
+    @Test
+    fun twoSleepSources_oneDataTotal_startTimeOlderThanOneYear_endTimeWithinLastYear_isDisplayed() {
+        whenever(dataSourcesViewModel.getCurrentSelection()).then { HealthDataCategory.SLEEP }
+        whenever(dataSourcesViewModel.dataSourcesAndAggregationsInfo).then {
+            MutableLiveData(
+                DataSourcesAndAggregationsInfo(
+                    priorityListState =
+                        PriorityListState.WithData(true, listOf(TEST_APP, TEST_APP_2)),
+                    potentialAppSourcesState = PotentialAppSourcesState.WithData(true, listOf()),
+                    aggregationCardsState =
+                        AggregationCardsState.WithData(
+                            true,
+                            listOf(
+                                AggregationCardInfo(
+                                    HealthPermissionType.SLEEP,
+                                    FormattedEntry.FormattedAggregation(
+                                        "11h 5m", "11h 5m", "TestApp"),
+                                    Instant.parse("2020-12-31T21:00:00.00Z"),
+                                    Instant.parse("2021-01-01T08:05:00.00Z"))))))
+        }
+
+        whenever(dataSourcesViewModel.getEditedPriorityList())
+            .thenReturn(listOf(TEST_APP, TEST_APP_2))
+        whenever(dataSourcesViewModel.updatedAggregationCardsData).then {
+            MutableLiveData(
+                AggregationCardsState.WithData(
+                    true,
+                    listOf(
+                        AggregationCardInfo(
+                            HealthPermissionType.SLEEP,
+                            FormattedEntry.FormattedAggregation("11h 5m", "11h 5m", "TestApp"),
+                            Instant.parse("2020-12-31T21:00:00.00Z"),
+                            Instant.parse("2021-01-01T08:05:00.00Z")))))
+        }
+
+        launchFragment<DataSourcesFragment>(Bundle())
+
+        onView(withText("Sleep")).check(matches(isDisplayed()))
+        onView(withText("Data totals")).check(matches(isDisplayed()))
+        onView(withText("11h 5m")).check(matches(isDisplayed()))
+        onView(withText("Dec 31, 2020 – Jan 1, 2021")).check(matches(isDisplayed()))
+        onView(withText("App sources")).check(matches(isDisplayed()))
+        onView(withText("Add an app")).check(doesNotExist())
+        onView(
+                withText(
+                    "Add app sources to the list to see how the data " +
+                        "totals can change. Removing an app from this list will stop it " +
+                        "from contributing to totals, but it will still have write permissions."))
+            .check(matches(isDisplayed()))
+
+        onView(withId(R.id.linear_layout_recycle_view))
+            .check(
+                matches(
+                    atPosition(
+                        0,
+                        allOf(
+                            hasDescendant(withText("1")), hasDescendant(withText(TEST_APP_NAME))))))
+        onView(withId(R.id.linear_layout_recycle_view))
+            .check(
+                matches(
+                    atPosition(
+                        1,
+                        allOf(
+                            hasDescendant(withText("2")),
+                            hasDescendant(withText(TEST_APP_NAME_2))))))
+    }
+
+    @Test
+    fun noSources_displaysEmptyState() {
+        whenever(dataSourcesViewModel.dataSourcesAndAggregationsInfo).then {
+            MutableLiveData(
+                DataSourcesAndAggregationsInfo(
+                    priorityListState = PriorityListState.WithData(true, listOf()),
+                    potentialAppSourcesState = PotentialAppSourcesState.WithData(true, listOf()),
+                    aggregationCardsState = AggregationCardsState.WithData(true, listOf())))
+        }
+        whenever(dataSourcesViewModel.updatedAggregationCardsData).then {
+            MutableLiveData(AggregationCardsState.WithData(true, listOf()))
         }
         launchFragment<DataSourcesFragment>(Bundle())
 
         onView(withText("Activity")).check(matches(isDisplayed()))
         onView(withText("No app sources")).check(matches(isDisplayed()))
-        onView(withText("Once you give app permissions to write activity data, sources will show here."))
-        onView(withText("How sources & prioritization work"))
+        onView(
+                withText(
+                    "Once you give app permissions to write activity data, sources will show here."))
+            .check(matches(isDisplayed()))
+        onView(withText("How sources & prioritization work")).check(matches(isDisplayed()))
     }
-}
\ No newline at end of file
+
+    @Test
+    fun addAnApp_shownWhenPotentialAppsExist() {
+        whenever(dataSourcesViewModel.dataSourcesAndAggregationsInfo).then {
+            MutableLiveData(
+                DataSourcesAndAggregationsInfo(
+                    priorityListState =
+                        PriorityListState.WithData(true, listOf(TEST_APP, TEST_APP_2)),
+                    potentialAppSourcesState =
+                        PotentialAppSourcesState.WithData(true, listOf(TEST_APP_3)),
+                    aggregationCardsState = AggregationCardsState.WithData(true, listOf())))
+        }
+        whenever(dataSourcesViewModel.getEditedPriorityList())
+            .thenReturn(listOf(TEST_APP, TEST_APP_2))
+        whenever(dataSourcesViewModel.updatedAggregationCardsData).then {
+            MutableLiveData(AggregationCardsState.WithData(true, listOf()))
+        }
+        launchFragment<DataSourcesFragment>(Bundle())
+        onIdle()
+
+        onView(withText("Activity")).check(matches(isDisplayed()))
+        onView(withText("Data totals")).check(doesNotExist())
+        onView(withText("App sources")).check(matches(isDisplayed()))
+        onView(withText("Add an app")).check(matches(isDisplayed()))
+        onView(
+                withText(
+                    "Add app sources to the list to see how the data " +
+                        "totals can change. Removing an app from this list will stop it " +
+                        "from contributing to totals, but it will still have write permissions."))
+            .check(matches(isDisplayed()))
+
+        onView(withId(R.id.linear_layout_recycle_view))
+            .check(
+                matches(
+                    atPosition(
+                        0,
+                        allOf(
+                            hasDescendant(withText("1")), hasDescendant(withText(TEST_APP_NAME))))))
+        onView(withId(R.id.linear_layout_recycle_view))
+            .check(
+                matches(
+                    atPosition(
+                        1,
+                        allOf(
+                            hasDescendant(withText("2")),
+                            hasDescendant(withText(TEST_APP_NAME_2))))))
+    }
+
+    @Test
+    fun appOnPriorityList_whenDefaultApp_showsAsDeviceDefault() {
+        whenever(dataSourcesViewModel.dataSourcesAndAggregationsInfo).then {
+            MutableLiveData(
+                DataSourcesAndAggregationsInfo(
+                    priorityListState =
+                        PriorityListState.WithData(true, listOf(TEST_APP, TEST_APP_2)),
+                    potentialAppSourcesState = PotentialAppSourcesState.WithData(true, listOf()),
+                    aggregationCardsState = AggregationCardsState.WithData(true, listOf())))
+        }
+        whenever(dataSourcesViewModel.getEditedPriorityList()).then { listOf(TEST_APP, TEST_APP_2) }
+        whenever(dataSourcesViewModel.updatedAggregationCardsData).then {
+            MutableLiveData(AggregationCardsState.WithData(true, listOf()))
+        }
+        (appUtils as FakeAppUtils).setDefaultApp(TEST_APP_PACKAGE_NAME)
+        launchFragment<DataSourcesFragment>(Bundle())
+        onIdle()
+
+        onView(withText("Activity")).check(matches(isDisplayed()))
+        onView(withText("Data totals")).check(doesNotExist())
+        onView(withText("App sources")).check(matches(isDisplayed()))
+        onView(withText("Add an app")).check(doesNotExist())
+        onView(
+                withText(
+                    "Add app sources to the list to see how the data " +
+                        "totals can change. Removing an app from this list will stop it " +
+                        "from contributing to totals, but it will still have write permissions."))
+            .check(matches(isDisplayed()))
+
+        onView(withId(R.id.linear_layout_recycle_view))
+            .check(
+                matches(
+                    atPosition(
+                        0,
+                        allOf(
+                            hasDescendant(withText("1")),
+                            hasDescendant(withText(TEST_APP_NAME)),
+                            hasDescendant(withText("Device default"))))))
+
+        onView(withId(R.id.linear_layout_recycle_view))
+            .check(
+                matches(
+                    atPosition(
+                        1,
+                        allOf(
+                            hasDescendant(withText("2")),
+                            hasDescendant(withText(TEST_APP_NAME_2)),
+                            hasDescendant(not(withText("Device default")))))))
+    }
+
+    @Test
+    fun atLeastOneSourceLoading_showsLoading() {
+        whenever(dataSourcesViewModel.dataSourcesAndAggregationsInfo).then {
+            MutableLiveData(
+                DataSourcesAndAggregationsInfo(
+                    priorityListState =
+                        PriorityListState.WithData(true, listOf(TEST_APP, TEST_APP_2)),
+                    potentialAppSourcesState = PotentialAppSourcesState.WithData(true, listOf()),
+                    aggregationCardsState = AggregationCardsState.Loading(true)))
+        }
+
+        whenever(dataSourcesViewModel.getEditedPriorityList())
+            .thenReturn(listOf(TEST_APP, TEST_APP_2))
+        whenever(dataSourcesViewModel.updatedAggregationCardsData).then {
+            MutableLiveData(AggregationCardsState.Loading(false))
+        }
+        launchFragment<DataSourcesFragment>(Bundle())
+
+        onView(withId(R.id.progress_indicator)).check(matches(isDisplayed()))
+    }
+
+    @Test
+    fun atLeastOneSourceLoadingFailed_showsError() {
+        whenever(dataSourcesViewModel.dataSourcesAndAggregationsInfo).then {
+            MutableLiveData(
+                DataSourcesAndAggregationsInfo(
+                    priorityListState = PriorityListState.LoadingFailed(true),
+                    potentialAppSourcesState = PotentialAppSourcesState.WithData(true, listOf()),
+                    aggregationCardsState = AggregationCardsState.WithData(true, listOf())))
+        }
+
+        whenever(dataSourcesViewModel.getEditedPriorityList())
+            .thenReturn(listOf(TEST_APP, TEST_APP_2))
+        whenever(dataSourcesViewModel.updatedAggregationCardsData).then {
+            MutableLiveData(AggregationCardsState.WithData(true, listOf()))
+        }
+        launchFragment<DataSourcesFragment>(Bundle())
+        onIdle()
+
+        onView(withId(R.id.error_view)).check(matches(isDisplayed()))
+    }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/datasources/DataSourcesViewModelTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/datasources/DataSourcesViewModelTest.kt
new file mode 100644
index 0000000..e41526f
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/datasources/DataSourcesViewModelTest.kt
@@ -0,0 +1,149 @@
+package com.android.healthconnect.controller.tests.datasources
+
+import android.health.connect.HealthDataCategory
+import com.android.healthconnect.controller.data.entries.FormattedEntry
+import com.android.healthconnect.controller.datasources.AggregationCardInfo
+import com.android.healthconnect.controller.datasources.DataSourcesViewModel
+import com.android.healthconnect.controller.datasources.DataSourcesViewModel.AggregationCardsState
+import com.android.healthconnect.controller.datasources.DataSourcesViewModel.DataSourcesAndAggregationsInfo
+import com.android.healthconnect.controller.datasources.DataSourcesViewModel.PotentialAppSourcesState
+import com.android.healthconnect.controller.datasources.DataSourcesViewModel.PriorityListState
+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.FakeLoadMostRecentAggregationsUseCase
+import com.android.healthconnect.controller.tests.utils.di.FakeLoadPotentialPriorityListUseCase
+import com.android.healthconnect.controller.tests.utils.di.FakeLoadPriorityListUseCase
+import com.android.healthconnect.controller.tests.utils.di.FakeUpdatePriorityListUseCase
+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.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 java.time.Instant
+import javax.inject.Inject
+
+@ExperimentalCoroutinesApi
+@HiltAndroidTest
+class DataSourcesViewModelTest {
+
+    companion object {
+        private fun formattedAggregation(aggregation: String) =
+            FormattedEntry.FormattedAggregation(
+                aggregation = aggregation,
+                aggregationA11y = aggregation,
+                contributingApps = "Test App")
+    }
+
+    private val testDispatcher = TestCoroutineDispatcher()
+
+    @get:Rule
+    val hiltRule = HiltAndroidRule(this)
+
+    @get:Rule
+    val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+    @Inject
+    lateinit var appInfoReader: AppInfoReader
+
+    private lateinit var viewModel: DataSourcesViewModel
+    private val loadMostRecentAggregationsUseCase = FakeLoadMostRecentAggregationsUseCase()
+    private val loadPotentialAppSourcesUseCase = FakeLoadPotentialPriorityListUseCase()
+    private val loadPriorityListUseCase = FakeLoadPriorityListUseCase()
+    private val updatePriorityListUseCase = FakeUpdatePriorityListUseCase()
+
+    @Before
+    fun setup() {
+        hiltRule.inject()
+        Dispatchers.setMain(testDispatcher)
+        viewModel = DataSourcesViewModel(
+            loadMostRecentAggregationsUseCase,
+            loadPotentialAppSourcesUseCase,
+            loadPriorityListUseCase,
+            updatePriorityListUseCase,
+            appInfoReader
+        )
+    }
+
+    @After
+    fun tearDown() {
+        loadMostRecentAggregationsUseCase.reset()
+        loadPotentialAppSourcesUseCase.reset()
+        loadPriorityListUseCase.reset()
+        updatePriorityListUseCase.reset()
+        Dispatchers.resetMain()
+        testDispatcher.cleanupTestCoroutines()
+    }
+
+    @Test
+    fun setCurrentSelection_setsCorrectCategory() = runTest {
+        viewModel.setCurrentSelection(HealthDataCategory.ACTIVITY)
+        assertThat(viewModel.getCurrentSelection()).isEqualTo(HealthDataCategory.ACTIVITY)
+    }
+
+    @Test
+    fun setEditedPriorityList_setsCorrectPriorityList() = runTest {
+        val editedPriorityList = listOf(TEST_APP, TEST_APP_2)
+        viewModel.setEditedPriorityList(editedPriorityList)
+        assertThat(viewModel.getEditedPriorityList()).isEqualTo(editedPriorityList)
+    }
+
+    @Test
+    fun setEditedPotentialAppSources_setsCorrectAppSources() = runTest {
+        val editedAppSources = listOf(TEST_APP_2, TEST_APP_3)
+        viewModel.setEditedPotentialAppSources(editedAppSources)
+        assertThat(viewModel.getEditedPotentialAppSources()).isEqualTo(editedAppSources)
+    }
+
+    @Test
+    fun loadData_withAllData_returnsDataSourcesAndAggregationsInfoWithData() = runTest {
+        val mostRecentAggregations = listOf(
+            AggregationCardInfo(
+                HealthPermissionType.STEPS,
+                formattedAggregation("100 steps"),
+                Instant.now())
+        )
+        val priorityList = listOf(TEST_APP, TEST_APP_2)
+        val potentialAppSources = listOf(TEST_APP_3)
+        loadMostRecentAggregationsUseCase.updateMostRecentAggregations(mostRecentAggregations)
+        loadPriorityListUseCase.updatePriorityList(priorityList)
+        loadPotentialAppSourcesUseCase.updatePotentialPriorityList(potentialAppSources)
+        val testObserver = TestObserver<DataSourcesAndAggregationsInfo>()
+        viewModel.dataSourcesAndAggregationsInfo.observeForever(testObserver)
+        viewModel.loadData(HealthDataCategory.ACTIVITY)
+        advanceUntilIdle()
+
+        val actual = testObserver.getLastValue()
+        val expected =
+            DataSourcesAndAggregationsInfo(
+                priorityListState = PriorityListState.WithData(true, priorityList),
+                potentialAppSourcesState = PotentialAppSourcesState.WithData(true, potentialAppSources),
+                aggregationCardsState = AggregationCardsState.WithData(true, mostRecentAggregations)
+            )
+        assertThat(actual).isEqualTo(expected)
+    }
+
+    @Test
+    fun updatePriorityList_callsUpdatePriorityListUseCase_withCorrectListAndCategory() = runTest {
+        val newPriorityList = listOf(TEST_APP_3.packageName, TEST_APP.packageName)
+        val category = HealthDataCategory.SLEEP
+        viewModel.updatePriorityList(newPriorityList, category)
+        advanceUntilIdle()
+
+        assertThat(updatePriorityListUseCase.category).isEqualTo(category)
+        assertThat(updatePriorityListUseCase.priorityList).isEqualTo(newPriorityList)
+    }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..3f6c58a
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/datasources/api/LoadMostRecentAggregationsUseCaseTest.kt
@@ -0,0 +1,1236 @@
+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.setLocale
+import com.android.healthconnect.controller.utils.atStartOfDay
+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
+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.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+import org.mockito.invocation.InvocationOnMock
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@HiltAndroidTest
+class LoadMostRecentAggregationsUseCaseTest {
+
+    companion object {
+        private fun formattedAggregation(aggregation: String) =
+            FormattedEntry.FormattedAggregation(
+                aggregation = aggregation,
+                aggregationA11y = aggregation,
+                contributingApps = "Test App")
+    }
+
+    @get:Rule val hiltRule = HiltAndroidRule(this)
+
+    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 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
+        TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("UTC")))
+        loadMostRecentAggregationsUseCase =
+            LoadMostRecentAggregationsUseCase(
+                healthConnectManager,
+                loadDataAggregationsUseCase,
+                loadSleepDataUseCase,
+                Dispatchers.Main)
+    }
+
+    @After
+    fun tearDown() {
+        loadDataAggregationsUseCase.reset()
+        loadSleepDataUseCase.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())
+
+        loadDataAggregationsUseCase.updateAggregationResponses(
+            listOf(stepsAggregation, distanceAggregation, caloriesAggregation))
+
+        val result = loadMostRecentAggregationsUseCase.invoke(HealthDataCategory.ACTIVITY)
+        assertThat(result is UseCaseResults.Success).isTrue()
+        assertThat((result as UseCaseResults.Success).data)
+            .isEqualTo(
+                listOf(
+                    AggregationCardInfo(
+                        HealthPermissionType.STEPS, stepsAggregation, STEPS_DATE_3.atStartOfDay()),
+                    AggregationCardInfo(
+                        HealthPermissionType.DISTANCE,
+                        distanceAggregation,
+                        DISTANCE_DATE_3.atStartOfDay()),
+                    AggregationCardInfo(
+                        HealthPermissionType.TOTAL_CALORIES_BURNED,
+                        caloriesAggregation,
+                        CALORIES_DATE_3.atStartOfDay()),
+                ))
+    }
+
+    // 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() =
+        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 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)
+            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(
+                        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
+    // with gaps
+    @Test
+    fun loadMostRecentAggregations_forSleep_atLeastOneSessionStartsYesterdayAndEndsToday_withGaps() =
+        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")
+
+            // 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)
+            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(
+                        AggregationCardInfo(
+                            HealthPermissionType.SLEEP,
+                            expectedSleepAggregation,
+                            SLEEP_SESSION_3_START_DATE.atStartOfDay(),
+                            SLEEP_SESSION_2_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 Day 1
+    // and finishes on unknown date
+    @Test
+    fun loadMostRecentAggregations_forSleep_atLeastOneSessionStartsYesterdayAndEndsToday_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())
+
+            // 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)
+            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(
+                        AggregationCardInfo(
+                            HealthPermissionType.SLEEP,
+                            expectedSleepAggregation,
+                            SLEEP_SESSION_3_START_DATE.atStartOfDay(),
+                            SLEEP_SESSION_1_END_DATE.atStartOfDay())))
+        }
+
+    // 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)
+        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 unknown date
+    // and finishes within range
+    @Test
+    fun loadMostRecentAggregations_forSleep_atLeastOneSessionFinishesTomorrow_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())
+
+            // 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")
+            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(),
+                            maxEndTime.atStartOfDay())))
+        }
+
+    // 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() =
+        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")
+
+            // 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")
+            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())))
+        }
+
+    @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))))
+
+        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())))
+    }
+
+    @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())
+
+        Mockito.doAnswer(prepareCaloriesAnswer())
+            .`when`(healthConnectManager)
+            .queryActivityDates(eq(caloriesRecordTypes), any(), any())
+
+        Mockito.doAnswer(prepareEmptyAnswer())
+            .`when`(healthConnectManager)
+            .queryActivityDates(eq(sleepRecordTypes), any(), any())
+
+        loadDataAggregationsUseCase.updateAggregationResponses(
+            listOf(stepsAggregation, distanceAggregation, caloriesAggregation))
+
+        val result = loadMostRecentAggregationsUseCase.invoke(HealthDataCategory.ACTIVITY)
+        assertThat(result is UseCaseResults.Failed).isTrue()
+    }
+
+    @Test
+    fun loadMostRecentAggregations_ifAggregationRequestFails_returnsEmptyList() = 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())
+
+        loadDataAggregationsUseCase.updateErrorResponse()
+
+        val result = loadMostRecentAggregationsUseCase.invoke(HealthDataCategory.ACTIVITY)
+        assertThat(result is UseCaseResults.Success).isTrue()
+        assertThat((result as UseCaseResults.Success).data).isEmpty()
+    }
+
+    @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())
+
+        Mockito.doAnswer(prepareEmptyAnswer())
+            .`when`(healthConnectManager)
+            .queryActivityDates(eq(caloriesRecordTypes), any(), any())
+
+        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
+    }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/datasources/api/LoadPotentialPriorityListUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/datasources/api/LoadPotentialPriorityListUseCaseTest.kt
new file mode 100644
index 0000000..6235fb2
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/datasources/api/LoadPotentialPriorityListUseCaseTest.kt
@@ -0,0 +1,225 @@
+/**
+ * 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.datasources.api
+
+import android.content.Context
+import android.health.connect.HealthConnectManager
+import android.health.connect.HealthDataCategory
+import android.health.connect.RecordTypeInfoResponse
+import android.health.connect.datatypes.DataOrigin
+import android.health.connect.datatypes.DistanceRecord
+import android.health.connect.datatypes.HeartRateRecord
+import android.health.connect.datatypes.Record
+import android.health.connect.datatypes.SleepSessionRecord
+import android.health.connect.datatypes.StepsRecord
+import android.os.OutcomeReceiver
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.healthconnect.controller.datasources.api.LoadPotentialPriorityListUseCase
+import com.android.healthconnect.controller.permissions.api.GetGrantedHealthPermissionsUseCase
+import com.android.healthconnect.controller.permissions.api.HealthPermissionManager
+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.permissiontypes.api.LoadPriorityListUseCase
+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_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.whenever
+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.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Matchers.any
+import org.mockito.Mockito
+import org.mockito.invocation.InvocationOnMock
+
+@ExperimentalCoroutinesApi
+@HiltAndroidTest
+class LoadPotentialPriorityListUseCaseTest {
+    @get:Rule val hiltRule = HiltAndroidRule(this)
+
+    private lateinit var context: Context
+    private lateinit var getGrantedHealthPermissionsUseCase: GetGrantedHealthPermissionsUseCase
+    private lateinit var loadPriorityListUseCase: LoadPriorityListUseCase
+    private lateinit var loadPotentialPriorityListUseCase: LoadPotentialPriorityListUseCase
+    @Inject lateinit var appInfoReader: AppInfoReader
+
+    private val healthPermissionManager: HealthPermissionManager =
+        Mockito.mock(HealthPermissionManager::class.java)
+    private val healthConnectManager: HealthConnectManager =
+        Mockito.mock(HealthConnectManager::class.java)
+    private val healthPermissionReader: HealthPermissionReader =
+        Mockito.mock(HealthPermissionReader::class.java)
+
+    @Before
+    fun setup() {
+        hiltRule.inject()
+        context = InstrumentationRegistry.getInstrumentation().context
+        getGrantedHealthPermissionsUseCase =
+            GetGrantedHealthPermissionsUseCase(healthPermissionManager)
+        loadPriorityListUseCase =
+            LoadPriorityListUseCase(healthConnectManager, appInfoReader, Dispatchers.Main)
+        loadPotentialPriorityListUseCase =
+            LoadPotentialPriorityListUseCase(
+                appInfoReader,
+                healthConnectManager,
+                healthPermissionReader,
+                getGrantedHealthPermissionsUseCase,
+                loadPriorityListUseCase,
+                Dispatchers.Main)
+    }
+
+    @Test
+    fun getAppsWithData_forActivity_returnsAppsForActivity() = runTest {
+        Mockito.doAnswer(prepareQueryAllRecordTypesAnswer())
+            .`when`(healthConnectManager)
+            .queryAllRecordTypesInfo(any(), any())
+
+        val result = loadPotentialPriorityListUseCase.getAppsWithData(HealthDataCategory.ACTIVITY)
+        assertThat(result is UseCaseResults.Success).isTrue()
+        assertThat((result as UseCaseResults.Success).data)
+            .isEqualTo(setOf(TEST_APP_PACKAGE_NAME, TEST_APP_PACKAGE_NAME_2))
+    }
+
+    @Test
+    fun getAppsWithData_forSleep_returnsAppsForSleep() = runTest {
+        Mockito.doAnswer(prepareQueryAllRecordTypesAnswer())
+            .`when`(healthConnectManager)
+            .queryAllRecordTypesInfo(any(), any())
+
+        val result = loadPotentialPriorityListUseCase.getAppsWithData(HealthDataCategory.SLEEP)
+        assertThat(result is UseCaseResults.Success).isTrue()
+        assertThat((result as UseCaseResults.Success).data)
+            .isEqualTo(setOf(TEST_APP_PACKAGE_NAME_2))
+    }
+
+    // TODO (b/299920950) Unignore test when we can use mockito-kotlin
+    @Test
+    @Ignore
+    fun getAppsWithWritePermission_forActivity_returnsAppsForActivity() = runTest {
+        whenever(healthPermissionReader.getAppsWithHealthPermissions())
+            .thenReturn(
+                listOf(TEST_APP_PACKAGE_NAME, TEST_APP_PACKAGE_NAME_2, TEST_APP_PACKAGE_NAME_3))
+
+        whenever(healthPermissionManager.getGrantedHealthPermissions(TEST_APP_PACKAGE_NAME))
+            .thenReturn(
+                listOf(
+                    HealthPermission(HealthPermissionType.DISTANCE, PermissionsAccessType.WRITE)
+                        .toString()))
+
+        whenever(healthPermissionManager.getGrantedHealthPermissions(TEST_APP_PACKAGE_NAME_2))
+            .thenReturn(
+                listOf(
+                    HealthPermission(HealthPermissionType.SLEEP, PermissionsAccessType.WRITE)
+                        .toString()))
+
+        whenever(healthPermissionManager.getGrantedHealthPermissions(TEST_APP_PACKAGE_NAME_3))
+            .thenReturn(
+                listOf(
+                    HealthPermission(HealthPermissionType.HEART_RATE, PermissionsAccessType.READ)
+                        .toString()))
+
+        val result =
+            loadPotentialPriorityListUseCase.getAppsWithWritePermission(HealthDataCategory.ACTIVITY)
+        assertThat(result is UseCaseResults.Success).isTrue()
+        assertThat((result as UseCaseResults.Success).data).isEqualTo(setOf(TEST_APP_PACKAGE_NAME))
+    }
+
+    // TODO (b/299920950) Unignore test when we can use mockito-kotlin
+    @Test
+    @Ignore
+    fun getAppsWithWritePermission_forSleep_returnsAppsForSleep() = runTest {
+        whenever(healthPermissionReader.getAppsWithHealthPermissions())
+            .thenReturn(
+                listOf(TEST_APP_PACKAGE_NAME, TEST_APP_PACKAGE_NAME_2, TEST_APP_PACKAGE_NAME_3))
+        whenever(healthPermissionManager.getGrantedHealthPermissions(TEST_APP_PACKAGE_NAME))
+            .thenReturn(
+                listOf(
+                    HealthPermission(HealthPermissionType.SLEEP, PermissionsAccessType.READ)
+                        .toString()))
+
+        whenever(healthPermissionManager.getGrantedHealthPermissions(TEST_APP_PACKAGE_NAME_2))
+            .thenReturn(
+                listOf(
+                    HealthPermission(HealthPermissionType.SLEEP, PermissionsAccessType.WRITE)
+                        .toString()))
+
+        whenever(healthPermissionManager.getGrantedHealthPermissions(TEST_APP_PACKAGE_NAME_3))
+            .thenReturn(
+                listOf(
+                    HealthPermission(HealthPermissionType.HEART_RATE, PermissionsAccessType.READ)
+                        .toString()))
+
+        val result =
+            loadPotentialPriorityListUseCase.getAppsWithWritePermission(HealthDataCategory.SLEEP)
+        assertThat(result is UseCaseResults.Success).isTrue()
+        assertThat((result as UseCaseResults.Success).data)
+            .isEqualTo(setOf(TEST_APP_PACKAGE_NAME_2))
+    }
+
+    private fun getRecordTypeInfoMap(): Map<Class<out Record>, RecordTypeInfoResponse> {
+        val map = mutableMapOf<Class<out Record>, RecordTypeInfoResponse>()
+        map[StepsRecord::class.java] =
+            RecordTypeInfoResponse(
+                HealthPermissionType.STEPS.category,
+                HealthDataCategory.ACTIVITY,
+                listOf(getDataOriginTestApp()))
+        map[DistanceRecord::class.java] =
+            RecordTypeInfoResponse(
+                HealthPermissionType.DISTANCE.category,
+                HealthDataCategory.ACTIVITY,
+                listOf(getDataOriginTestApp2()))
+        map[HeartRateRecord::class.java] =
+            RecordTypeInfoResponse(
+                HealthPermissionType.HEART_RATE.category,
+                HealthDataCategory.VITALS,
+                listOf(getDataOriginTestApp3()))
+        map[SleepSessionRecord::class.java] =
+            RecordTypeInfoResponse(
+                HealthPermissionType.SLEEP.category,
+                HealthDataCategory.SLEEP,
+                listOf(getDataOriginTestApp2()))
+        return map
+    }
+
+    private fun prepareQueryAllRecordTypesAnswer(): (InvocationOnMock) -> Nothing? {
+        val answer = { args: InvocationOnMock ->
+            val receiver =
+                args.arguments[1]
+                    as OutcomeReceiver<Map<Class<out Record>, RecordTypeInfoResponse>, *>
+            receiver.onResult(getRecordTypeInfoMap())
+            null
+        }
+        return answer
+    }
+
+    private fun getDataOriginTestApp(): DataOrigin =
+        DataOrigin.Builder().setPackageName(TEST_APP_PACKAGE_NAME).build()
+
+    private fun getDataOriginTestApp2(): DataOrigin =
+        DataOrigin.Builder().setPackageName(TEST_APP_PACKAGE_NAME_2).build()
+
+    private fun getDataOriginTestApp3(): DataOrigin =
+        DataOrigin.Builder().setPackageName(TEST_APP_PACKAGE_NAME_3).build()
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/datasources/api/UpdatePriorityListUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/datasources/api/UpdatePriorityListUseCaseTest.kt
new file mode 100644
index 0000000..04b3c69
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/datasources/api/UpdatePriorityListUseCaseTest.kt
@@ -0,0 +1,73 @@
+package com.android.healthconnect.controller.tests.datasources.api
+
+import android.health.connect.HealthConnectManager
+import android.health.connect.HealthDataCategory
+import android.health.connect.UpdateDataOriginPriorityOrderRequest
+import android.health.connect.datatypes.DataOrigin
+import com.android.healthconnect.controller.datasources.api.UpdatePriorityListUseCase
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME_3
+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.ArgumentMatchers.any
+import org.mockito.Captor
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.times
+import org.mockito.invocation.InvocationOnMock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@ExperimentalCoroutinesApi
+@HiltAndroidTest
+class UpdatePriorityListUseCaseTest {
+
+    @get:Rule
+    val hiltRule = HiltAndroidRule(this)
+    private lateinit var useCase: UpdatePriorityListUseCase
+    private val healthConnectManager : HealthConnectManager = mock(HealthConnectManager::class.java)
+
+    @Captor lateinit var requestCaptor : ArgumentCaptor<UpdateDataOriginPriorityOrderRequest>
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        hiltRule.inject()
+        useCase = UpdatePriorityListUseCase(healthConnectManager, Dispatchers.Main)
+    }
+
+    @Test
+    fun invoke_callsHealthConnectManager() = runTest {
+        doAnswer(prepareAnswer())
+            .`when`(healthConnectManager)
+            .updateDataOriginPriorityOrder(any(UpdateDataOriginPriorityOrderRequest::class.java), any(), any())
+
+        val priorityList = listOf(TEST_APP_PACKAGE_NAME, TEST_APP_PACKAGE_NAME_3)
+        useCase.invoke(
+            priorityList = priorityList,
+            category = HealthDataCategory.ACTIVITY
+        )
+        val expectedPriorityList = priorityList
+            .map { packageName -> DataOrigin.Builder().setPackageName(packageName).build() }
+            .toList()
+
+        verify(healthConnectManager, times(1))
+            .updateDataOriginPriorityOrder(requestCaptor.capture(), any(), any())
+        assertThat(requestCaptor.value.dataCategory).isEqualTo(HealthDataCategory.ACTIVITY)
+        assertThat(requestCaptor.value.dataOriginInOrder)
+            .isEqualTo(expectedPriorityList)
+    }
+
+    private fun prepareAnswer(): (InvocationOnMock) -> Nothing? {
+        val answer = { _: InvocationOnMock -> null }
+        return answer
+    }
+
+}
\ No newline at end of file
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 b25c275..ef3c558 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
@@ -49,6 +49,7 @@
 import org.mockito.Mockito
 
 @HiltAndroidTest
+@Deprecated("This won't be used once the NEW_INFORMATION_ARCHITECTURE feature is enabled.")
 class DeletionFragmentTest {
 
     @get:Rule val hiltRule = HiltAndroidRule(this)
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/deletion/api/DeleteAllDataUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/deletion/api/DeleteAllDataUseCaseTest.kt
index 68a400a..455a1b1 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/deletion/api/DeleteAllDataUseCaseTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/deletion/api/DeleteAllDataUseCaseTest.kt
@@ -39,6 +39,7 @@
 import org.mockito.invocation.InvocationOnMock
 
 @HiltAndroidTest
+@Deprecated("This won't be used once the NEW_INFORMATION_ARCHITECTURE feature is enabled.")
 class DeleteAllDataUseCaseTest {
     @get:Rule val hiltRule = HiltAndroidRule(this)
 
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/deletion/api/DeleteAppDataUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/deletion/api/DeleteAppDataUseCaseTest.kt
index 088b96b..674ed9b 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/deletion/api/DeleteAppDataUseCaseTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/deletion/api/DeleteAppDataUseCaseTest.kt
@@ -39,6 +39,7 @@
 import org.mockito.invocation.InvocationOnMock
 
 @HiltAndroidTest
+@Deprecated("This won't be used once the NEW_INFORMATION_ARCHITECTURE feature is enabled.")
 class DeleteAppDataUseCaseTest {
     @get:Rule val hiltRule = HiltAndroidRule(this)
 
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/deletion/api/DeleteCategoryUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/deletion/api/DeleteCategoryUseCaseTest.kt
index 7ad75d3..205cf1b 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/deletion/api/DeleteCategoryUseCaseTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/deletion/api/DeleteCategoryUseCaseTest.kt
@@ -53,6 +53,7 @@
 import org.mockito.invocation.InvocationOnMock
 
 @HiltAndroidTest
+@Deprecated("This won't be used once the NEW_INFORMATION_ARCHITECTURE feature is enabled.")
 class DeleteCategoryUseCaseTest {
     @get:Rule val hiltRule = HiltAndroidRule(this)
 
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/deletion/api/DeleteEntryUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/deletion/api/DeleteEntryUseCaseTest.kt
index b397495..0cc5226 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/deletion/api/DeleteEntryUseCaseTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/deletion/api/DeleteEntryUseCaseTest.kt
@@ -41,6 +41,7 @@
 import org.mockito.invocation.InvocationOnMock
 
 @HiltAndroidTest
+@Deprecated("This won't be used once the NEW_INFORMATION_ARCHITECTURE feature is enabled.")
 class DeleteEntryUseCaseTest {
 
     @get:Rule val hiltRule = HiltAndroidRule(this)
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/deletion/api/DeletePermissionTypeUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/deletion/api/DeletePermissionTypeUseCaseTest.kt
index 5a74218..776c7ef 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/deletion/api/DeletePermissionTypeUseCaseTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/deletion/api/DeletePermissionTypeUseCaseTest.kt
@@ -41,6 +41,7 @@
 import org.mockito.invocation.InvocationOnMock
 
 @HiltAndroidTest
+@Deprecated("This won't be used once the NEW_INFORMATION_ARCHITECTURE feature is enabled.")
 class DeletePermissionTypeUseCaseTest {
 
     @get:Rule val hiltRule = HiltAndroidRule(this)
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/entrydetails/DataEntryDetailsFragmentTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/entrydetails/DataEntryDetailsFragmentTest.kt
index b4e6645..9253e68 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/entrydetails/DataEntryDetailsFragmentTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/entrydetails/DataEntryDetailsFragmentTest.kt
@@ -41,10 +41,10 @@
 import androidx.test.espresso.matcher.ViewMatchers.withText
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.healthconnect.controller.R
-import com.android.healthconnect.controller.dataentries.FormattedEntry
-import com.android.healthconnect.controller.dataentries.FormattedEntry.ExerciseSessionEntry
-import com.android.healthconnect.controller.dataentries.FormattedEntry.SeriesDataEntry
-import com.android.healthconnect.controller.dataentries.FormattedEntry.SleepSessionEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry.ExerciseSessionEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry.SeriesDataEntry
+import com.android.healthconnect.controller.data.entries.FormattedEntry.SleepSessionEntry
 import com.android.healthconnect.controller.entrydetails.DataEntryDetailsFragment
 import com.android.healthconnect.controller.entrydetails.DataEntryDetailsViewModel
 import com.android.healthconnect.controller.entrydetails.DataEntryDetailsViewModel.DateEntryFragmentState.Loading
@@ -91,7 +91,8 @@
         whenever(viewModel.sessionData).thenReturn(MutableLiveData(WithData(emptyList())))
 
         launchFragment<DataEntryDetailsFragment>(
-            DataEntryDetailsFragment.createBundle(permissionType = SLEEP, entryId = "1"))
+            DataEntryDetailsFragment.createBundle(
+                permissionType = SLEEP, entryId = "1", showDataOrigin = true))
 
         onView(withId(R.id.loading)).check(matches(not(isDisplayed())))
     }
@@ -101,7 +102,8 @@
         whenever(viewModel.sessionData).thenReturn(MutableLiveData(LoadingFailed))
 
         launchFragment<DataEntryDetailsFragment>(
-            DataEntryDetailsFragment.createBundle(permissionType = SLEEP, entryId = "1"))
+            DataEntryDetailsFragment.createBundle(
+                permissionType = SLEEP, entryId = "1", showDataOrigin = true))
 
         onView(withId(R.id.error_view)).check(matches(isDisplayed()))
     }
@@ -111,7 +113,8 @@
         whenever(viewModel.sessionData).thenReturn(MutableLiveData(Loading))
 
         launchFragment<DataEntryDetailsFragment>(
-            DataEntryDetailsFragment.createBundle(permissionType = SLEEP, entryId = "1"))
+            DataEntryDetailsFragment.createBundle(
+                permissionType = SLEEP, entryId = "1", showDataOrigin = true))
 
         onView(withId(R.id.loading)).check(matches(isDisplayed()))
     }
@@ -133,7 +136,8 @@
                                 notes = "notes")))))
 
         launchFragment<DataEntryDetailsFragment>(
-            DataEntryDetailsFragment.createBundle(permissionType = SLEEP, entryId = "1"))
+            DataEntryDetailsFragment.createBundle(
+                permissionType = SLEEP, entryId = "1", showDataOrigin = true))
 
         onView(withText("07:06 • TEST_APP_NAME")).check(matches(isDisplayed()))
         onView(withText("12 hour sleeping")).check(matches(isDisplayed()))
@@ -149,7 +153,8 @@
         whenever(viewModel.sessionData).thenReturn(MutableLiveData(WithData(list)))
 
         launchFragment<DataEntryDetailsFragment>(
-            DataEntryDetailsFragment.createBundle(permissionType = SLEEP, entryId = "1"))
+            DataEntryDetailsFragment.createBundle(
+                permissionType = SLEEP, entryId = "1", showDataOrigin = true))
 
         onView(withText("12 hour sleeping")).check(matches(isDisplayed()))
         onView(withText("6 hour light sleeping")).check(matches(isDisplayed()))
@@ -162,7 +167,8 @@
         whenever(viewModel.sessionData).thenReturn(MutableLiveData(WithData(list)))
 
         launchFragment<DataEntryDetailsFragment>(
-            DataEntryDetailsFragment.createBundle(permissionType = HEART_RATE, entryId = "1"))
+            DataEntryDetailsFragment.createBundle(
+                permissionType = HEART_RATE, entryId = "1", showDataOrigin = true))
 
         onView(withText("07:06 - 8:06 • TEST_APP_NAME")).check(matches(isDisplayed()))
         onView(withText("100 bpm")).check(matches(isDisplayed()))
@@ -173,7 +179,8 @@
         val list = buildList { add(getFormattedExerciseSession(showSession = true)) }
         whenever(viewModel.sessionData).thenReturn(MutableLiveData(WithData(list)))
         launchFragment<DataEntryDetailsFragment>(
-            DataEntryDetailsFragment.createBundle(permissionType = EXERCISE, entryId = "1"))
+            DataEntryDetailsFragment.createBundle(
+                permissionType = EXERCISE, entryId = "1", showDataOrigin = true))
 
         onView(withText("12 hour running")).check(matches(isDisplayed()))
         onView(withId(R.id.map_view)).check(matches(isDisplayed()))
@@ -184,7 +191,8 @@
         val list = buildList { add(getFormattedExerciseSession(showSession = false)) }
         whenever(viewModel.sessionData).thenReturn(MutableLiveData(WithData(list)))
         launchFragment<DataEntryDetailsFragment>(
-            DataEntryDetailsFragment.createBundle(permissionType = EXERCISE, entryId = "1"))
+            DataEntryDetailsFragment.createBundle(
+                permissionType = EXERCISE, entryId = "1", showDataOrigin = true))
 
         onView(withText("12 hour running")).check(matches(isDisplayed()))
         onView(withId(R.id.map_view)).check(matches(not(isDisplayed())))
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/permissions/HealthPermissionConstants.java b/apk/tests/src/com/android/healthconnect/controller/tests/permissions/HealthPermissionConstants.java
new file mode 100644
index 0000000..8b69f8e
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/permissions/HealthPermissionConstants.java
@@ -0,0 +1,27 @@
+/*
+ * 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;
+
+/** Temporary copy-pasted permission constants to allow some tests passing. */
+// TODO(b/299897306): Remove when UI implementation for Background Reads is done
+public final class HealthPermissionConstants {
+
+    public static final String READ_HEALTH_DATA_IN_BACKGROUND =
+            "android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND";
+
+    private HealthPermissionConstants() {}
+}
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
new file mode 100644
index 0000000..dfd3790
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/permissions/api/GetHealthPermissionsFlagsUseCaseTest.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.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 org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito
+import org.mockito.Mockito.verify
+
+class GetHealthPermissionsFlagsUseCaseTest {
+    private lateinit var context: Context
+    private lateinit var useCase: GetHealthPermissionsFlagsUseCase
+    private val healthPermissionManager: HealthPermissionManager =
+        Mockito.mock(HealthPermissionManager::class.java)
+
+    @Before
+    fun setup() {
+        context = InstrumentationRegistry.getInstrumentation().context
+        useCase = GetHealthPermissionsFlagsUseCase(healthPermissionManager)
+    }
+
+    @Test
+    fun invoke_callsHealthPermissionManager() {
+        useCase.invoke("TEST_APP", listOf("PERMISSION_1", "PERMISSION_2", "PERMISSION_3"))
+
+        verify(healthPermissionManager)
+            .getHealthPermissionsFlags(
+                "TEST_APP", listOf("PERMISSION_1", "PERMISSION_2", "PERMISSION_3"))
+    }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/permissions/api/MakeHealthPermissionsRequestableUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/permissions/api/MakeHealthPermissionsRequestableUseCaseTest.kt
new file mode 100644
index 0000000..36f4bc8
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/permissions/api/MakeHealthPermissionsRequestableUseCaseTest.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.content.Context
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.healthconnect.controller.permissions.api.HealthPermissionManager
+import com.android.healthconnect.controller.permissions.api.MakeHealthPermissionsRequestableUseCase
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito
+import org.mockito.Mockito.verify
+
+class MakeHealthPermissionsRequestableUseCaseTest {
+    private lateinit var context: Context
+    private lateinit var useCase: MakeHealthPermissionsRequestableUseCase
+    private val healthPermissionManager: HealthPermissionManager =
+        Mockito.mock(HealthPermissionManager::class.java)
+
+    @Before
+    fun setup() {
+        context = InstrumentationRegistry.getInstrumentation().context
+        useCase = MakeHealthPermissionsRequestableUseCase(healthPermissionManager)
+    }
+
+    @Test
+    fun invoke_callsHealthPermissionManager() {
+        useCase.invoke("TEST_APP", listOf("PERMISSION_1", "PERMISSION_2", "PERMISSION_3"))
+
+        verify(healthPermissionManager)
+            .makeHealthPermissionsRequestable(
+                "TEST_APP", listOf("PERMISSION_1", "PERMISSION_2", "PERMISSION_3"))
+    }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/permissions/connectedapps/ConnectedAppFragmentTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/permissions/connectedapps/ConnectedAppFragmentTest.kt
index 7efeb34..2b48702 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/permissions/connectedapps/ConnectedAppFragmentTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/permissions/connectedapps/ConnectedAppFragmentTest.kt
@@ -22,6 +22,7 @@
 import androidx.test.espresso.Espresso.onView
 import androidx.test.espresso.action.ViewActions.click
 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.ViewMatchers.isChecked
 import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
@@ -43,10 +44,12 @@
 import com.android.healthconnect.controller.tests.TestActivity
 import com.android.healthconnect.controller.tests.utils.TEST_APP_NAME
 import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME
+import com.android.healthconnect.controller.tests.utils.di.FakeFeatureUtils
 import com.android.healthconnect.controller.tests.utils.launchFragment
 import com.android.healthconnect.controller.tests.utils.safeEq
 import com.android.healthconnect.controller.tests.utils.setLocale
 import com.android.healthconnect.controller.tests.utils.whenever
+import com.android.healthconnect.controller.utils.FeatureUtils
 import com.android.settingslib.widget.MainSwitchPreference
 import com.google.common.truth.Truth.assertThat
 import dagger.hilt.android.testing.BindValue
@@ -56,6 +59,7 @@
 import java.time.ZoneId
 import java.util.Locale
 import java.util.TimeZone
+import javax.inject.Inject
 import org.hamcrest.Matchers.not
 import org.junit.Before
 import org.junit.Rule
@@ -68,8 +72,8 @@
 class ConnectedAppFragmentTest {
 
     @get:Rule val hiltRule = HiltAndroidRule(this)
-
     @BindValue val viewModel: AppPermissionViewModel = mock(AppPermissionViewModel::class.java)
+    @Inject lateinit var fakeFeatureUtils: FeatureUtils
 
     @Before
     fun setup() {
@@ -77,6 +81,7 @@
         context.setLocale(Locale.US)
         TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("UTC")))
         hiltRule.inject()
+        (fakeFeatureUtils as FakeFeatureUtils).setIsNewInformationArchitectureEnabled(false)
 
         whenever(viewModel.revokeAllPermissionsState).then { MutableLiveData(NotStarted) }
         whenever(viewModel.allAppPermissionsGranted).then { MutableLiveData(false) }
@@ -120,6 +125,7 @@
             assertThat(readCategory?.preferenceCount).isEqualTo(0)
             assertThat(writeCategory?.preferenceCount).isEqualTo(0)
         }
+        onView(withText("See app data")).check(doesNotExist())
     }
 
     @Test
@@ -147,6 +153,8 @@
             assertThat(writeCategory?.preferenceCount).isEqualTo(0)
         }
         onView(withText("Distance")).check(matches(isDisplayed()))
+        onView(withText("See app data")).check(doesNotExist())
+        onView(withText("Delete app data")).check(matches(isDisplayed()))
     }
 
     @Test
@@ -174,6 +182,7 @@
             assertThat(writeCategory?.preferenceCount).isEqualTo(1)
         }
         onView(withText("Exercise")).check(matches(isDisplayed()))
+        onView(withText("See app data")).check(doesNotExist())
     }
 
     @Test
@@ -205,6 +214,7 @@
         }
         onView(withText("Exercise")).check(matches(isDisplayed()))
         onView(withText("Distance")).check(matches(isDisplayed()))
+        onView(withText("See app data")).check(doesNotExist())
     }
 
     @Test
@@ -234,6 +244,7 @@
 
             assertThat(mainSwitchPreference?.isChecked).isTrue()
         }
+        onView(withText("See app data")).check(doesNotExist())
     }
 
     @Test
@@ -276,6 +287,7 @@
         onView(withText("Allow all")).perform(click())
 
         onView(withText("Remove all permissions?")).check(matches(isDisplayed()))
+        onView(withText("See app data")).check(doesNotExist())
     }
 
     @Test
@@ -297,6 +309,7 @@
 
         onView(withText("Exercise")).check(matches(not(isChecked())))
         onView(withText("Distance")).check(matches(not(isChecked())))
+        onView(withText("See app data")).check(doesNotExist())
     }
 
     @Test
@@ -340,4 +353,22 @@
             .check(matches(isDisplayed()))
         onView(withText("Read privacy policy")).perform(scrollTo()).check(matches(isDisplayed()))
     }
+
+    @Test
+    fun seeAppData_isEnabled_buttonDisplayed() {
+        (fakeFeatureUtils as FakeFeatureUtils).setIsNewInformationArchitectureEnabled(true)
+        val writePermission = HealthPermission(EXERCISE, WRITE)
+        val readPermission = HealthPermission(DISTANCE, READ)
+        whenever(viewModel.appPermissions).then {
+            MutableLiveData(listOf(writePermission, readPermission))
+        }
+        whenever(viewModel.grantedPermissions).then {
+            MutableLiveData(setOf(writePermission, readPermission))
+        }
+        whenever(viewModel.allAppPermissionsGranted).then { MutableLiveData(true) }
+        launchFragment<ConnectedAppFragment>(
+            bundleOf(EXTRA_PACKAGE_NAME to TEST_APP_PACKAGE_NAME, EXTRA_APP_NAME to TEST_APP_NAME))
+        onView(withText("See app data")).perform(scrollTo()).check(matches(isDisplayed()))
+        onView(withText("Delete app data")).check(doesNotExist())
+    }
 }
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/permissions/connectedapps/settings/SettingsActivityTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/permissions/connectedapps/settings/SettingsActivityTest.kt
index 7862268..a4c5048 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/permissions/connectedapps/settings/SettingsActivityTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/permissions/connectedapps/settings/SettingsActivityTest.kt
@@ -120,7 +120,7 @@
     }
 
     @Test
-    fun settingsActivityFinishes_ifNoRationaleIntent_andNoPermissions() {
+    fun settingsActivityFinishes_whenNoPackageName_ifNoRationaleIntent_andNoPermissions() {
         val intent = Intent(context, SettingsActivity::class.java)
         val bundle = Bundle()
         bundle.putString(EXTRA_PACKAGE_NAME, TEST_APP_PACKAGE_NAME)
@@ -140,7 +140,7 @@
     }
 
     @Test
-    fun settingsActivity_navigatesToFragment_ifRationaleIntentDeclared() {
+    fun settingsActivity_navigatesToAppPermissionsFragment_ifRationaleIntentDeclared() {
         val intent = Intent(context, SettingsActivity::class.java)
         val bundle = Bundle()
         bundle.putString(EXTRA_PACKAGE_NAME, TEST_APP_PACKAGE_NAME)
@@ -159,7 +159,7 @@
     }
 
     @Test
-    fun settingsActivity_navigatesToFragment_ifNoRationaleIntentDeclared_andGrantedPermission() {
+    fun settingsActivity_navigatesToAppPermissionsFragment_ifNoRationaleIntentDeclared_andGrantedPermission() {
         val intent = Intent(context, SettingsActivity::class.java)
         val bundle = Bundle()
         bundle.putString(EXTRA_PACKAGE_NAME, TEST_APP_PACKAGE_NAME)
@@ -177,4 +177,14 @@
         }
     }
 
+    @Test
+    fun settingsActivity_navigatesToManagePermissionsFragment_ifNoPackageName() {
+        val intent = Intent(context, SettingsActivity::class.java)
+
+        ActivityScenario.launch<SettingsActivity>(intent).use { scenario ->
+            onView(withText("Apps with this permission can read and write your" +
+                    " health and fitness data.")).check(matches(isDisplayed()))
+        }
+    }
+
 }
\ No newline at end of file
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/permissions/data/HealthPermissionStringsTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/permissions/data/HealthPermissionStringsTest.kt
index be6f3c7..5872e40 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/permissions/data/HealthPermissionStringsTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/permissions/data/HealthPermissionStringsTest.kt
@@ -13,23 +13,28 @@
  */
 package com.android.healthconnect.controller.tests.permissions.data
 
-import android.content.Context
-import android.health.connect.HealthConnectManager
-import androidx.test.platform.app.InstrumentationRegistry
 import com.android.healthconnect.controller.permissions.data.HealthPermission
 import com.android.healthconnect.controller.permissions.data.HealthPermissionStrings
 import com.android.healthconnect.controller.permissions.data.HealthPermissionType
+import com.android.healthconnect.controller.shared.HealthPermissionReader
 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 HealthPermissionStringsTest {
 
-    private lateinit var context: Context
+    @get:Rule val hiltRule = HiltAndroidRule(this)
+
+    @Inject lateinit var healthPermissionReader: HealthPermissionReader
 
     @Before
     fun setup() {
-        context = InstrumentationRegistry.getInstrumentation().context
+        hiltRule.inject()
     }
 
     @Test
@@ -41,7 +46,7 @@
 
     @Test
     fun allHealthPermissionsHaveStrings() {
-        val allPermissions = HealthConnectManager.getHealthPermissions(context)
+        val allPermissions = healthPermissionReader.getHealthPermissions()
         for (permission in allPermissions) {
             val type = HealthPermission.fromPermissionString(permission).healthPermissionType
             assertThat(HealthPermissionStrings.fromPermissionType(type)).isNotNull()
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/permissions/data/HealthPermissionTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/permissions/data/HealthPermissionTest.kt
index 14ac4f3..39dc7ed 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/permissions/data/HealthPermissionTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/permissions/data/HealthPermissionTest.kt
@@ -15,26 +15,31 @@
  */
 package com.android.healthconnect.controller.tests.permissions.data
 
-import android.content.Context
-import android.health.connect.HealthConnectManager
-import androidx.test.platform.app.InstrumentationRegistry
 import com.android.healthconnect.controller.permissions.data.HealthPermission
 import com.android.healthconnect.controller.permissions.data.HealthPermission.Companion.fromPermissionString
 import com.android.healthconnect.controller.permissions.data.HealthPermissionType.ACTIVE_CALORIES_BURNED
 import com.android.healthconnect.controller.permissions.data.HealthPermissionType.BLOOD_GLUCOSE
 import com.android.healthconnect.controller.permissions.data.PermissionsAccessType
+import com.android.healthconnect.controller.shared.HealthPermissionReader
 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.Assert.assertThrows
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 
+@HiltAndroidTest
 class HealthPermissionTest {
 
-    private lateinit var context: Context
+    @get:Rule val hiltRule = HiltAndroidRule(this)
+
+    @Inject lateinit var healthPermissionReader: HealthPermissionReader
 
     @Before
     fun setup() {
-        context = InstrumentationRegistry.getInstrumentation().context
+        hiltRule.inject()
     }
 
     @Test
@@ -51,8 +56,8 @@
 
     @Test
     fun fromPermissionString_canParseAllHealthPermissions() {
-        val allPermissions = HealthConnectManager.getHealthPermissions(context)
-        allPermissions.forEach { permissionString ->
+        val allPermissions = healthPermissionReader.getHealthPermissions()
+        for (permissionString in allPermissions) {
             assertThat(fromPermissionString(permissionString).toString())
                 .isEqualTo(permissionString)
         }
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/permissions/request/RequestPermissionViewModelTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/permissions/request/RequestPermissionViewModelTest.kt
index 922b21d..c9e3944 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/permissions/request/RequestPermissionViewModelTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/permissions/request/RequestPermissionViewModelTest.kt
@@ -22,28 +22,36 @@
 import com.android.healthconnect.controller.permissions.api.GrantHealthPermissionUseCase
 import com.android.healthconnect.controller.permissions.api.HealthPermissionManager
 import com.android.healthconnect.controller.permissions.api.RevokeHealthPermissionUseCase
+import com.android.healthconnect.controller.permissions.data.HealthPermission
 import com.android.healthconnect.controller.permissions.data.HealthPermission.Companion.fromPermissionString
 import com.android.healthconnect.controller.permissions.request.RequestPermissionViewModel
 import com.android.healthconnect.controller.service.HealthPermissionManagerModule
 import com.android.healthconnect.controller.shared.app.AppInfoReader
+import com.android.healthconnect.controller.shared.app.AppMetadata
 import com.android.healthconnect.controller.tests.utils.InstantTaskExecutorRule
 import com.android.healthconnect.controller.tests.utils.TEST_APP_NAME
 import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME
+import com.android.healthconnect.controller.tests.utils.TestObserver
 import com.android.healthconnect.controller.tests.utils.di.FakeHealthPermissionManager
-import com.android.healthconnect.controller.tests.utils.getOrAwaitValue
 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 dagger.hilt.android.testing.UninstallModules
-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 javax.inject.Inject
 
-@ExperimentalCoroutinesApi
+@OptIn(ExperimentalCoroutinesApi::class)
 @UninstallModules(HealthPermissionManagerModule::class)
 @HiltAndroidTest
 class RequestPermissionViewModelTest {
@@ -54,6 +62,7 @@
 
     @get:Rule val hiltRule = HiltAndroidRule(this)
     @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule()
+    private val testDispatcher = TestCoroutineDispatcher()
 
     @BindValue val permissionManager: HealthPermissionManager = FakeHealthPermissionManager()
 
@@ -68,6 +77,7 @@
     fun setup() {
         hiltRule.inject()
         permissionManager.revokeAllHealthPermissions(TEST_APP_PACKAGE_NAME)
+        Dispatchers.setMain(testDispatcher)
         viewModel =
             RequestPermissionViewModel(
                 appInfoReader,
@@ -77,16 +87,27 @@
         viewModel.init(TEST_APP_PACKAGE_NAME, permissions)
     }
 
+    @After
+    fun tearDown() {
+        Dispatchers.resetMain()
+        testDispatcher.cleanupTestCoroutines()
+    }
+
     @Test
     fun init_initAppInfo_initPermissions() = runTest {
-        val appMetaData = viewModel.appMetadata.getOrAwaitValue()
-        assertThat(appMetaData.appName).isEqualTo(TEST_APP_NAME)
-        assertThat(appMetaData.packageName).isEqualTo(TEST_APP_PACKAGE_NAME)
+        val testObserver = TestObserver<AppMetadata>()
+        viewModel.appMetadata.observeForever(testObserver)
+        advanceUntilIdle()
+        assertThat(testObserver.getLastValue()?.appName).isEqualTo(TEST_APP_NAME)
+        assertThat(testObserver.getLastValue()?.packageName).isEqualTo(TEST_APP_PACKAGE_NAME)
     }
 
     @Test
     fun init_initPermissions() = runTest {
-        assertThat(viewModel.permissionsList.getOrAwaitValue())
+        val testObserver = TestObserver<List<HealthPermission>>()
+        viewModel.permissionsList.observeForever(testObserver)
+        advanceUntilIdle()
+        assertThat(testObserver.getLastValue())
             .isEqualTo(
                 listOf(fromPermissionString(READ_STEPS), fromPermissionString(READ_HEART_RATE)))
     }
@@ -108,36 +129,48 @@
     }
 
     @Test
-    fun updatePermission_grant_updatesGrantedPermissions() {
+    fun updatePermission_grant_updatesGrantedPermissions() = runTest {
         val readStepsPermission = fromPermissionString(READ_STEPS)
+        val testObserver = TestObserver<Set<HealthPermission>>()
+        viewModel.grantedPermissions.observeForever(testObserver)
         viewModel.updatePermission(readStepsPermission, grant = true)
+        advanceUntilIdle()
 
-        assertThat(viewModel.grantedPermissions.getOrAwaitValue()).contains(readStepsPermission)
+        assertThat(testObserver.getLastValue()).contains(readStepsPermission)
     }
 
     @Test
-    fun updatePermission_revoke_updatesGrantedPermissions() {
+    fun updatePermission_revoke_updatesGrantedPermissions() = runTest {
         val readStepsPermission = fromPermissionString(READ_STEPS)
+        val testObserver = TestObserver<Set<HealthPermission>>()
+        viewModel.grantedPermissions.observeForever(testObserver)
         viewModel.updatePermission(readStepsPermission, grant = false)
+        advanceUntilIdle()
 
-        assertThat(viewModel.grantedPermissions.getOrAwaitValue())
+        assertThat(testObserver.getLastValue())
             .doesNotContain(readStepsPermission)
     }
 
     @Test
-    fun updatePermissions_grant_updatesGrantedPermissions() {
+    fun updatePermissions_grant_updatesGrantedPermissions() = runTest {
+        val testObserver = TestObserver<Set<HealthPermission>>()
+        viewModel.grantedPermissions.observeForever(testObserver)
         viewModel.updatePermissions(grant = true)
+        advanceUntilIdle()
 
-        assertThat(viewModel.grantedPermissions.getOrAwaitValue())
+        assertThat(testObserver.getLastValue())
             .containsExactly(
                 fromPermissionString(READ_STEPS), fromPermissionString(READ_HEART_RATE))
     }
 
     @Test
-    fun updatePermissions_revoke_updatesGrantedPermissions() {
+    fun updatePermissions_revoke_updatesGrantedPermissions() = runTest {
+        val testObserver = TestObserver<Set<HealthPermission>>()
+        viewModel.grantedPermissions.observeForever(testObserver)
         viewModel.updatePermissions(grant = false)
+        advanceUntilIdle()
 
-        assertThat(viewModel.grantedPermissions.getOrAwaitValue()).isEmpty()
+        assertThat(testObserver.getLastValue()).isEmpty()
     }
 
     @Test
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 8335850..767890e 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
@@ -33,7 +33,9 @@
 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.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
@@ -41,11 +43,14 @@
 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
 
     @BindValue
     val viewModel: HealthPermissionTypesViewModel =
@@ -54,6 +59,7 @@
     @Before
     fun setup() {
         hiltRule.inject()
+        (fakeFeatureUtils as FakeFeatureUtils).setIsNewAppPriorityEnabled(false)
     }
 
     @Test
@@ -308,6 +314,49 @@
         assert(viewModel.selectedAppFilter.value == TEST_APP_3.appName)
     }
 
+    @Test
+    fun permissionTypesFragment_whenNewPriorityEnabled_doesNotShowAppPriority() {
+        (fakeFeatureUtils as FakeFeatureUtils).setIsNewAppPriorityEnabled(true)
+        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<List<AppMetadata>>(listOf(TEST_APP, TEST_APP_2))
+        }
+        Mockito.`when`(viewModel.categoryLabel).then { MutableLiveData("activity") }
+        launchFragment<HealthPermissionTypesFragment>(activityCategoryBundle())
+
+        onView(withText("Active calories burned")).check(doesNotExist())
+        onView(withText("Distance")).check(matches(isDisplayed()))
+        onView(withText("Elevation gained")).check(doesNotExist())
+        onView(withText("Exercise")).check(matches(isDisplayed()))
+        onView(withText("Floors climbed")).check(doesNotExist())
+        onView(withText("Power")).check(doesNotExist())
+        onView(withText("Steps")).check(matches(isDisplayed()))
+        onView(withText("Total calories burned")).check(doesNotExist())
+        onView(withText("VO2 max")).check(doesNotExist())
+        onView(withText("Wheelchair pushes")).check(doesNotExist())
+        onView(withText("Manage data")).check(matches(isDisplayed()))
+        onView(withText("App priority")).check(doesNotExist())
+        onView(withText("Health Connect test app")).check(doesNotExist())
+        onView(withText("Delete activity data")).check(matches(isDisplayed()))
+    }
+
     private fun activityCategoryBundle(): Bundle {
         val bundle = Bundle()
         bundle.putInt(HealthDataCategoriesFragment.CATEGORY_KEY, 1)
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/recentaccess/RecentAccessViewModelTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/recentaccess/RecentAccessViewModelTest.kt
index 4bb9b89..cad5bdb 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/recentaccess/RecentAccessViewModelTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/recentaccess/RecentAccessViewModelTest.kt
@@ -27,8 +27,6 @@
 import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.uppercaseTitle
 import com.android.healthconnect.controller.shared.app.AppInfoReader
 import com.android.healthconnect.controller.shared.dataTypeToCategory
-import com.android.healthconnect.controller.tests.utils.di.FakeHealthPermissionAppsUseCase
-import com.android.healthconnect.controller.tests.utils.di.FakeRecentAccessUseCase
 import com.android.healthconnect.controller.tests.utils.InstantTaskExecutorRule
 import com.android.healthconnect.controller.tests.utils.MIDNIGHT
 import com.android.healthconnect.controller.tests.utils.NOW
@@ -36,27 +34,36 @@
 import com.android.healthconnect.controller.tests.utils.TEST_APP_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.TestObserver
 import com.android.healthconnect.controller.tests.utils.TestTimeSource
-import com.android.healthconnect.controller.tests.utils.getOrAwaitValue
+import com.android.healthconnect.controller.tests.utils.di.FakeHealthPermissionAppsUseCase
+import com.android.healthconnect.controller.tests.utils.di.FakeRecentAccessUseCase
 import com.google.common.truth.Truth.assertThat
 import dagger.hilt.android.testing.HiltAndroidRule
 import dagger.hilt.android.testing.HiltAndroidTest
-import java.time.Duration
-import java.time.Instant
-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 java.time.Duration
+import java.time.Instant
+import javax.inject.Inject
 
-@ExperimentalCoroutinesApi
+@OptIn(ExperimentalCoroutinesApi::class)
 @HiltAndroidTest
 class RecentAccessViewModelTest {
 
     @get:Rule val hiltRule = HiltAndroidRule(this)
 
     @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule()
+    private val testDispatcher = TestCoroutineDispatcher()
 
     @Inject lateinit var appInfoReader: AppInfoReader
 
@@ -68,9 +75,16 @@
     @Before
     fun setup() {
         hiltRule.inject()
+        Dispatchers.setMain(testDispatcher)
         viewModel =
             RecentAccessViewModel(
-                appInfoReader, fakeHealthPermissionAppsUseCase, fakeRecentAccessUseCase)
+                appInfoReader, fakeHealthPermissionAppsUseCase, fakeRecentAccessUseCase, timeSource)
+    }
+
+    @After
+    fun tearDown() {
+        Dispatchers.resetMain()
+        testDispatcher.cleanupTestCoroutines()
     }
 
     @Test
@@ -112,8 +126,12 @@
                 .sortedByDescending { it.accessTime }
 
         fakeRecentAccessUseCase.updateList(accessLogs)
-        viewModel.loadRecentAccessApps(timeSource = timeSource)
-        val actual = viewModel.recentAccessApps.getOrAwaitValue(time = 5, callsCount = 2)
+        val testObserver = TestObserver<RecentAccessState>()
+        viewModel.recentAccessApps.observeForever(testObserver)
+        viewModel.loadRecentAccessApps()
+        advanceUntilIdle()
+
+        val actual = testObserver.getLastValue()
         val expected =
             listOf(
                 RecentAccessEntry(
@@ -191,8 +209,12 @@
                 .sortedByDescending { it.accessTime }
 
         fakeRecentAccessUseCase.updateList(accessLogs)
-        viewModel.loadRecentAccessApps(timeSource = timeSource)
-        val actual = viewModel.recentAccessApps.getOrAwaitValue(time = 5, callsCount = 2)
+        val testObserver = TestObserver<RecentAccessState>()
+        viewModel.recentAccessApps.observeForever(testObserver)
+        viewModel.loadRecentAccessApps()
+        advanceUntilIdle()
+
+        val actual = testObserver.getLastValue()
         val expected =
             listOf(
                 RecentAccessEntry(
@@ -267,8 +289,12 @@
                 .sortedByDescending { it.accessTime }
 
         fakeRecentAccessUseCase.updateList(accessLogs)
-        viewModel.loadRecentAccessApps(timeSource = timeSource)
-        val actual = viewModel.recentAccessApps.getOrAwaitValue(time = 5, callsCount = 2)
+        val testObserver = TestObserver<RecentAccessState>()
+        viewModel.recentAccessApps.observeForever(testObserver)
+        viewModel.loadRecentAccessApps()
+        advanceUntilIdle()
+
+        val actual = testObserver.getLastValue()
         val expected =
             listOf(
                 RecentAccessEntry(
@@ -314,8 +340,12 @@
                 .sortedByDescending { it.accessTime }
 
         fakeRecentAccessUseCase.updateList(accessLogs)
-        viewModel.loadRecentAccessApps(timeSource = timeSource)
-        val actual = viewModel.recentAccessApps.getOrAwaitValue(time = 5, callsCount = 2)
+        val testObserver = TestObserver<RecentAccessState>()
+        viewModel.recentAccessApps.observeForever(testObserver)
+        viewModel.loadRecentAccessApps()
+        advanceUntilIdle()
+
+        val actual = testObserver.getLastValue()
 
         val expected =
             listOf(
@@ -370,8 +400,12 @@
                 .sortedByDescending { it.accessTime }
 
         fakeRecentAccessUseCase.updateList(accessLogs)
-        viewModel.loadRecentAccessApps(timeSource = timeSource)
-        val actual = viewModel.recentAccessApps.getOrAwaitValue(time = 5, callsCount = 2)
+        val testObserver = TestObserver<RecentAccessState>()
+        viewModel.recentAccessApps.observeForever(testObserver)
+        viewModel.loadRecentAccessApps()
+        advanceUntilIdle()
+
+        val actual = testObserver.getLastValue()
         val expected =
             listOf(
                 RecentAccessEntry(
@@ -418,8 +452,12 @@
                 .sortedByDescending { it.accessTime }
 
         fakeRecentAccessUseCase.updateList(accessLogs)
-        viewModel.loadRecentAccessApps(timeSource = timeSource)
-        val actual = viewModel.recentAccessApps.getOrAwaitValue(time = 5, callsCount = 2)
+        val testObserver = TestObserver<RecentAccessState>()
+        viewModel.recentAccessApps.observeForever(testObserver)
+        viewModel.loadRecentAccessApps()
+        advanceUntilIdle()
+
+        val actual = testObserver.getLastValue()
         val expected =
             listOf(
                 RecentAccessEntry(
@@ -505,8 +543,12 @@
                 .sortedByDescending { it.accessTime }
 
         fakeRecentAccessUseCase.updateList(accessLogs)
-        viewModel.loadRecentAccessApps(maxNumEntries = 3, timeSource = timeSource)
-        val actual = viewModel.recentAccessApps.getOrAwaitValue(time = 5, callsCount = 2)
+        val testObserver = TestObserver<RecentAccessState>()
+        viewModel.recentAccessApps.observeForever(testObserver)
+        viewModel.loadRecentAccessApps(maxNumEntries = 3)
+        advanceUntilIdle()
+
+        val actual = testObserver.getLastValue()
         val expected =
             listOf(
                 RecentAccessEntry(
@@ -541,7 +583,7 @@
     }
 
     private fun assertRecentAccessEquality(
-        state: RecentAccessState,
+        state: RecentAccessState?,
         expectedValues: List<RecentAccessEntry>
     ) {
         assertThat(state).isInstanceOf(RecentAccessState.WithData::class.java)
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/route/ExerciseRouteViewModelTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/route/ExerciseRouteViewModelTest.kt
index a28dc7d..ed9481b 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/route/ExerciseRouteViewModelTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/route/ExerciseRouteViewModelTest.kt
@@ -27,23 +27,23 @@
 import com.android.healthconnect.controller.route.ExerciseRouteViewModel
 import com.android.healthconnect.controller.route.LoadExerciseRouteUseCase
 import com.android.healthconnect.controller.shared.app.AppInfoReader
-import com.android.healthconnect.controller.shared.app.AppMetadata
 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_NAME
 import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME
+import com.android.healthconnect.controller.tests.utils.TestObserver
 import com.android.healthconnect.controller.tests.utils.getMetaData
-import com.android.healthconnect.controller.tests.utils.getOrAwaitValue
 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.util.Locale
-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
@@ -52,14 +52,18 @@
 import org.mockito.Mockito.mock
 import org.mockito.MockitoAnnotations
 import org.mockito.invocation.InvocationOnMock
+import java.time.Instant
+import java.util.Locale
+import javax.inject.Inject
 
-@ExperimentalCoroutinesApi
+@OptIn(ExperimentalCoroutinesApi::class)
 @HiltAndroidTest
 class ExerciseRouteViewModelTest {
 
     @get:Rule val hiltRule = HiltAndroidRule(this)
 
     @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule()
+    private val testDispatcher = TestCoroutineDispatcher()
 
     @Inject lateinit var appInfoReader: AppInfoReader
 
@@ -74,20 +78,30 @@
         context = InstrumentationRegistry.getInstrumentation().context
         context.setLocale(Locale.US)
         hiltRule.inject()
+        Dispatchers.setMain(testDispatcher)
         viewModel =
             ExerciseRouteViewModel(
                 LoadExerciseRouteUseCase(manager, Dispatchers.Main), appInfoReader)
     }
 
+    @After
+    fun tearDown() {
+        Dispatchers.resetMain()
+        testDispatcher.cleanupTestCoroutines()
+    }
+
     @Test
     fun loadExerciseRoute_noSession() = runTest {
-        doAnswer(prepareAnswer(listOf<ExerciseSessionRecord>()))
+        doAnswer(prepareAnswer(listOf()))
             .`when`(manager)
             .readRecords(any(ReadRecordsRequestUsingIds::class.java), any(), any())
 
+        val testObserver = TestObserver<ExerciseRouteViewModel.SessionWithAttribution?>()
+        viewModel.exerciseSession.observeForever(testObserver)
         viewModel.getExerciseWithRoute("testId")
+        advanceUntilIdle()
 
-        assertThat(viewModel.exerciseSession.getOrAwaitValue()).isEqualTo(null)
+        assertThat(testObserver.getLastValue()).isEqualTo(null)
     }
 
     @Test
@@ -106,9 +120,12 @@
             .`when`(manager)
             .readRecords(any(ReadRecordsRequestUsingIds::class.java), any(), any())
 
+        val testObserver = TestObserver<ExerciseRouteViewModel.SessionWithAttribution?>()
+        viewModel.exerciseSession.observeForever(testObserver)
         viewModel.getExerciseWithRoute("testId")
+        advanceUntilIdle()
 
-        assertThat(viewModel.exerciseSession.getOrAwaitValue()).isEqualTo(null)
+        assertThat(testObserver.getLastValue()).isEqualTo(null)
     }
 
     @Test
@@ -132,13 +149,16 @@
             .`when`(manager)
             .readRecords(any(ReadRecordsRequestUsingIds::class.java), any(), any())
 
+        val testObserver = TestObserver<ExerciseRouteViewModel.SessionWithAttribution?>()
+        viewModel.exerciseSession.observeForever(testObserver)
         viewModel.getExerciseWithRoute("testId")
+        advanceUntilIdle()
 
-        val result = viewModel.exerciseSession.getOrAwaitValue()
+        val result = testObserver.getLastValue()
 
         assertThat(result?.session as ExerciseSessionRecord).isEqualTo(expectedSession)
-        assertThat(result?.appInfo?.appName).isEqualTo(TEST_APP_NAME)
-        assertThat(result?.appInfo?.packageName).isEqualTo(TEST_APP_PACKAGE_NAME)
+        assertThat(result.appInfo.appName).isEqualTo(TEST_APP_NAME)
+        assertThat(result.appInfo.packageName).isEqualTo(TEST_APP_PACKAGE_NAME)
     }
 
     private fun prepareAnswer(
@@ -151,13 +171,4 @@
         }
         return answer
     }
-
-    private fun returnAppInfo(): (InvocationOnMock) -> AppMetadata {
-        val answer = { args: InvocationOnMock ->
-            val receiver = args.arguments[2] as OutcomeReceiver<Any?, *>
-            receiver.onResult(TEST_APP)
-            TEST_APP
-        }
-        return answer
-    }
 }
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/selectabledeletion/api/DeleteAllDataUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/selectabledeletion/api/DeleteAllDataUseCaseTest.kt
new file mode 100644
index 0000000..e4fe789
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/selectabledeletion/api/DeleteAllDataUseCaseTest.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.selectabledeletion.api
+
+import android.health.connect.DeleteUsingFiltersRequest
+import android.health.connect.HealthConnectManager
+import com.android.healthconnect.controller.selectabledeletion.api.DeleteAllDataUseCase
+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.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.Matchers.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import org.mockito.invocation.InvocationOnMock
+
+@HiltAndroidTest
+class DeleteAllDataUseCaseTest {
+    @get:Rule val hiltRule = HiltAndroidRule(this)
+
+    private lateinit var useCase: DeleteAllDataUseCase
+    var manager: HealthConnectManager = mock(HealthConnectManager::class.java)
+
+    @Captor lateinit var filtersCaptor: ArgumentCaptor<DeleteUsingFiltersRequest>
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        useCase = DeleteAllDataUseCase(manager, Dispatchers.Main)
+    }
+
+    @Test
+    fun invoke_deleteAllData_callsHealthManager() = runTest {
+        doAnswer(prepareAnswer())
+            .`when`(manager)
+            .deleteRecords(any(DeleteUsingFiltersRequest::class.java), any(), any())
+
+        useCase.invoke()
+
+        verify(manager, times(1)).deleteRecords(filtersCaptor.capture(), any(), any())
+        assertThat(filtersCaptor.value.timeRangeFilter).isNull()
+        assertThat(filtersCaptor.value.dataOrigins).isEmpty()
+        assertThat(filtersCaptor.value.recordTypes).isEmpty()
+    }
+
+    private fun prepareAnswer(): (InvocationOnMock) -> Nothing? {
+        val answer = { _: InvocationOnMock -> null }
+        return answer
+    }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/selectabledeletion/api/DeleteAppDataUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/selectabledeletion/api/DeleteAppDataUseCaseTest.kt
new file mode 100644
index 0000000..efeec9b
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/selectabledeletion/api/DeleteAppDataUseCaseTest.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.selectabledeletion.api
+
+import android.health.connect.DeleteUsingFiltersRequest
+import android.health.connect.HealthConnectManager
+import android.health.connect.datatypes.DataOrigin
+import com.android.healthconnect.controller.permissions.api.HealthPermissionManager
+import com.android.healthconnect.controller.permissions.api.RevokeAllHealthPermissionsUseCase
+import com.android.healthconnect.controller.selectabledeletion.DeletionType.DeletionTypeAppData
+import com.android.healthconnect.controller.selectabledeletion.api.DeleteAppDataUseCase
+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.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
+
+@HiltAndroidTest
+class DeleteAppDataUseCaseTest {
+    @get:Rule val hiltRule = HiltAndroidRule(this)
+
+    private lateinit var useCase: DeleteAppDataUseCase
+
+    var dataManager: HealthConnectManager = mock(HealthConnectManager::class.java)
+    var permissionManager: HealthPermissionManager = mock(HealthPermissionManager::class.java)
+
+    @Captor lateinit var filtersCaptor: ArgumentCaptor<DeleteUsingFiltersRequest>
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        val revokePermissionsUseCase = RevokeAllHealthPermissionsUseCase(permissionManager)
+        useCase = DeleteAppDataUseCase(dataManager, revokePermissionsUseCase, Dispatchers.Main)
+    }
+
+    @Test
+    fun invoke_deleteAppData_callsHealthManager() = runTest {
+        doAnswer(prepareAnswer())
+            .`when`(dataManager)
+            .deleteRecords(any(DeleteUsingFiltersRequest::class.java), any(), any())
+
+        val deleteAppData = DeletionTypeAppData(packageName = "package.name", appName = "APP_NAME")
+
+        useCase.invoke(deleteAppData)
+
+        verify(dataManager, times(1)).deleteRecords(filtersCaptor.capture(), any(), any())
+        Truth.assertThat(filtersCaptor.value.timeRangeFilter).isNull()
+        Truth.assertThat(filtersCaptor.value.dataOrigins)
+            .containsExactly(DataOrigin.Builder().setPackageName("package.name").build())
+        Truth.assertThat(filtersCaptor.value.recordTypes).isEmpty()
+    }
+
+    private fun prepareAnswer(): (InvocationOnMock) -> Nothing? {
+        val answer = { _: InvocationOnMock -> null }
+        return answer
+    }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/selectabledeletion/api/DeleteEntriesUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/selectabledeletion/api/DeleteEntriesUseCaseTest.kt
new file mode 100644
index 0000000..679a66c
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/selectabledeletion/api/DeleteEntriesUseCaseTest.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.selectabledeletion.api
+
+import android.health.connect.HealthConnectManager
+import android.health.connect.RecordIdFilter
+import android.health.connect.datatypes.StepsRecord
+import com.android.healthconnect.controller.selectabledeletion.DeletionType.DeletionTypeEntries
+import com.android.healthconnect.controller.selectabledeletion.api.DeleteEntriesUseCase
+import com.android.healthconnect.controller.shared.DataType
+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.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.Matchers.any
+import org.mockito.Matchers.anyListOf
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import org.mockito.invocation.InvocationOnMock
+
+@HiltAndroidTest
+class DeleteEntryUseCaseTest {
+
+    @get:Rule val hiltRule = HiltAndroidRule(this)
+
+    private lateinit var useCase: DeleteEntriesUseCase
+    var manager: HealthConnectManager = mock(HealthConnectManager::class.java)
+
+    @Captor lateinit var listCaptor: ArgumentCaptor<List<RecordIdFilter>>
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        useCase = DeleteEntriesUseCase(manager, Dispatchers.Main)
+    }
+
+    @Test
+    fun invoke_deleteEntries_callsHealthManager() = runTest {
+        doAnswer(prepareAnswer())
+            .`when`(manager)
+            .deleteRecords(anyListOf(RecordIdFilter::class.java), any(), any())
+
+        useCase.invoke(DeletionTypeEntries(listOf("test_id1", "test_id2"), DataType.STEPS))
+
+        verify(manager, times(1)).deleteRecords(listCaptor.capture(), any(), any())
+        assertThat(listCaptor.value[0].id).isEqualTo("test_id1")
+        assertThat(listCaptor.value[0].recordType).isEqualTo(StepsRecord::class.java)
+        assertThat(listCaptor.value[1].id).isEqualTo("test_id2")
+        assertThat(listCaptor.value[1].recordType).isEqualTo(StepsRecord::class.java)
+    }
+
+    private fun prepareAnswer(): (InvocationOnMock) -> Nothing? {
+        val answer = { _: InvocationOnMock -> null }
+        return answer
+    }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/selectabledeletion/api/DeletePermissionTypesFromAppUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/selectabledeletion/api/DeletePermissionTypesFromAppUseCaseTest.kt
new file mode 100644
index 0000000..3da92bd
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/selectabledeletion/api/DeletePermissionTypesFromAppUseCaseTest.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.selectabledeletion.api
+
+import android.health.connect.DeleteUsingFiltersRequest
+import android.health.connect.HealthConnectManager
+import android.health.connect.datatypes.CyclingPedalingCadenceRecord
+import android.health.connect.datatypes.DataOrigin
+import android.health.connect.datatypes.ExerciseSessionRecord
+import android.health.connect.datatypes.HeartRateRecord
+import android.health.connect.datatypes.MenstruationFlowRecord
+import android.health.connect.datatypes.MenstruationPeriodRecord
+import android.health.connect.datatypes.SleepSessionRecord
+import android.health.connect.datatypes.StepsCadenceRecord
+import android.health.connect.datatypes.StepsRecord
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType
+import com.android.healthconnect.controller.selectabledeletion.DeletionType.DeletionTypeHealthPermissionTypesFromApp
+import com.android.healthconnect.controller.selectabledeletion.api.DeletePermissionTypesFromAppUseCase
+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.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.Matchers.any
+import org.mockito.Mockito
+import org.mockito.Mockito.doAnswer
+import org.mockito.MockitoAnnotations
+import org.mockito.invocation.InvocationOnMock
+
+@HiltAndroidTest
+class DeletePermissionTypeUseCaseTest {
+
+    @get:Rule val hiltRule = HiltAndroidRule(this)
+
+    private lateinit var useCase: DeletePermissionTypesFromAppUseCase
+    var manager: HealthConnectManager = Mockito.mock(HealthConnectManager::class.java)
+
+    @Captor lateinit var filtersCaptor: ArgumentCaptor<DeleteUsingFiltersRequest>
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        useCase = DeletePermissionTypesFromAppUseCase(manager, Dispatchers.Main)
+    }
+
+    @Test
+    fun invoke_deletePermissionTypesFromApp_callsHealthManager() = runTest {
+        doAnswer(prepareAnswer())
+            .`when`(manager)
+            .deleteRecords(any(DeleteUsingFiltersRequest::class.java), any(), any())
+
+        val deletePermissionTypes =
+            DeletionTypeHealthPermissionTypesFromApp(
+                listOf(
+                    HealthPermissionType.STEPS,
+                    HealthPermissionType.HEART_RATE,
+                    HealthPermissionType.SLEEP,
+                    HealthPermissionType.EXERCISE,
+                    HealthPermissionType.MENSTRUATION),
+                packageName = "package.name",
+                appName = "APP_NAME")
+
+        useCase.invoke(deletePermissionTypes)
+
+        Mockito.verify(manager, Mockito.times(1))
+            .deleteRecords(filtersCaptor.capture(), any(), any())
+
+        assertThat(filtersCaptor.value.timeRangeFilter).isNull()
+        assertThat(filtersCaptor.value.dataOrigins)
+            .containsExactly(DataOrigin.Builder().setPackageName("package.name").build())
+        assertThat(filtersCaptor.value.recordTypes)
+            .containsExactly(
+                StepsRecord::class.java,
+                StepsCadenceRecord::class.java,
+                HeartRateRecord::class.java,
+                SleepSessionRecord::class.java,
+                ExerciseSessionRecord::class.java,
+                MenstruationFlowRecord::class.java,
+                MenstruationPeriodRecord::class.java,
+                CyclingPedalingCadenceRecord::class.java)
+    }
+
+    private fun prepareAnswer(): (InvocationOnMock) -> Nothing? {
+        val answer = { _: InvocationOnMock -> null }
+        return answer
+    }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/selectabledeletion/api/DeletePermissionTypesUseCaseTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/selectabledeletion/api/DeletePermissionTypesUseCaseTest.kt
new file mode 100644
index 0000000..8a0ae49
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/selectabledeletion/api/DeletePermissionTypesUseCaseTest.kt
@@ -0,0 +1,101 @@
+/*
+ * 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.selectabledeletion.api
+
+import android.health.connect.DeleteUsingFiltersRequest
+import android.health.connect.HealthConnectManager
+import android.health.connect.datatypes.CyclingPedalingCadenceRecord
+import android.health.connect.datatypes.ExerciseSessionRecord
+import android.health.connect.datatypes.HeartRateRecord
+import android.health.connect.datatypes.MenstruationFlowRecord
+import android.health.connect.datatypes.MenstruationPeriodRecord
+import android.health.connect.datatypes.SleepSessionRecord
+import android.health.connect.datatypes.StepsCadenceRecord
+import android.health.connect.datatypes.StepsRecord
+import com.android.healthconnect.controller.permissions.data.HealthPermissionType
+import com.android.healthconnect.controller.selectabledeletion.DeletionType.DeletionTypeHealthPermissionTypes
+import com.android.healthconnect.controller.selectabledeletion.api.DeletePermissionTypesUseCase
+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.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.Matchers.any
+import org.mockito.Mockito
+import org.mockito.Mockito.doAnswer
+import org.mockito.MockitoAnnotations
+import org.mockito.invocation.InvocationOnMock
+
+@HiltAndroidTest
+class DeletePermissionTypesUseCaseTest {
+
+    @get:Rule val hiltRule = HiltAndroidRule(this)
+
+    private lateinit var useCase: DeletePermissionTypesUseCase
+    var manager: HealthConnectManager = Mockito.mock(HealthConnectManager::class.java)
+
+    @Captor lateinit var filtersCaptor: ArgumentCaptor<DeleteUsingFiltersRequest>
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        useCase = DeletePermissionTypesUseCase(manager, Dispatchers.Main)
+    }
+
+    @Test
+    fun invoke_deletePermissionTypes_callsHealthManager() = runTest {
+        doAnswer(prepareAnswer())
+            .`when`(manager)
+            .deleteRecords(any(DeleteUsingFiltersRequest::class.java), any(), any())
+
+        val deletePermissionType =
+            DeletionTypeHealthPermissionTypes(
+                listOf(
+                    HealthPermissionType.STEPS,
+                    HealthPermissionType.HEART_RATE,
+                    HealthPermissionType.SLEEP,
+                    HealthPermissionType.EXERCISE,
+                    HealthPermissionType.MENSTRUATION))
+
+        useCase.invoke(deletePermissionType)
+
+        Mockito.verify(manager, Mockito.times(1))
+            .deleteRecords(filtersCaptor.capture(), any(), any())
+
+        assertThat(filtersCaptor.value.timeRangeFilter).isNull()
+        assertThat(filtersCaptor.value.dataOrigins).isEmpty()
+        assertThat(filtersCaptor.value.recordTypes)
+            .containsExactly(
+                StepsRecord::class.java,
+                StepsCadenceRecord::class.java,
+                HeartRateRecord::class.java,
+                SleepSessionRecord::class.java,
+                ExerciseSessionRecord::class.java,
+                MenstruationFlowRecord::class.java,
+                MenstruationPeriodRecord::class.java,
+                CyclingPedalingCadenceRecord::class.java)
+    }
+
+    private fun prepareAnswer(): (InvocationOnMock) -> Nothing? {
+        val answer = { _: InvocationOnMock -> null }
+        return answer
+    }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/shared/HealthDataCategoryExtensionsTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/shared/HealthDataCategoryExtensionsTest.kt
index bbe9497..973ac8f 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/shared/HealthDataCategoryExtensionsTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/shared/HealthDataCategoryExtensionsTest.kt
@@ -18,15 +18,12 @@
 
 package com.android.healthconnect.controller.tests.shared
 
-import android.content.Context
-import android.health.connect.HealthConnectManager
 import android.health.connect.HealthDataCategory.ACTIVITY
 import android.health.connect.HealthDataCategory.BODY_MEASUREMENTS
 import android.health.connect.HealthDataCategory.CYCLE_TRACKING
 import android.health.connect.HealthDataCategory.NUTRITION
 import android.health.connect.HealthDataCategory.SLEEP
 import android.health.connect.HealthDataCategory.VITALS
-import androidx.test.platform.app.InstrumentationRegistry
 import com.android.healthconnect.controller.R
 import com.android.healthconnect.controller.permissions.data.HealthPermission
 import com.android.healthconnect.controller.permissions.data.HealthPermissionType
@@ -35,24 +32,32 @@
 import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.healthPermissionTypes
 import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.lowercaseTitle
 import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.uppercaseTitle
+import com.android.healthconnect.controller.shared.HealthPermissionReader
 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.Assert.assertThrows
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 
+@HiltAndroidTest
 class HealthDataCategoryExtensionsTest {
 
-    private lateinit var context: Context
+    @get:Rule val hiltRule = HiltAndroidRule(this)
+
+    @Inject lateinit var healthPermissionReader: HealthPermissionReader
 
     @Before
     fun setup() {
-        context = InstrumentationRegistry.getInstrumentation().context
+        hiltRule.inject()
     }
 
     @Test
     fun allHealthPermission_haveParentCategory() {
-        val allPermissions = HealthConnectManager.getHealthPermissions(context)
-        allPermissions.forEach { permissionString ->
+        val allPermissions = healthPermissionReader.getHealthPermissions()
+        for (permissionString in allPermissions) {
             val healthPermission = HealthPermission.fromPermissionString(permissionString)
             assertThat(
                     HEALTH_DATA_CATEGORIES.any {
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/shared/HealthPermissionReaderTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/shared/HealthPermissionReaderTest.kt
index 80eda58..f4b7d25 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/shared/HealthPermissionReaderTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/shared/HealthPermissionReaderTest.kt
@@ -6,41 +6,41 @@
 import com.android.healthconnect.controller.permissions.data.HealthPermission
 import com.android.healthconnect.controller.shared.HealthPermissionReader
 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.UNSUPPORTED_TEST_APP_PACKAGE_NAME
+import com.android.healthconnect.controller.tests.utils.di.FakeFeatureUtils
 import com.android.healthconnect.controller.utils.FeatureUtils
-import com.android.healthconnect.controller.utils.FeaturesModule
 import com.google.common.truth.Truth.assertThat
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.android.testing.UninstallModules
-import dagger.hilt.components.SingletonComponent
+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
 
+@HiltAndroidTest
 @OptIn(ExperimentalCoroutinesApi::class)
 class HealthPermissionReaderTest {
+
+    @get:Rule val hiltRule = HiltAndroidRule(this)
+
+    @Inject lateinit var permissionReader: HealthPermissionReader
+    @Inject lateinit var fakeFeatureUtils: FeatureUtils
     private lateinit var context: Context
 
     @Before
     fun setup() {
+        hiltRule.inject()
         context = InstrumentationRegistry.getInstrumentation().context
     }
 
     @Test
     fun getDeclaredPermissions_hidesSessionTypesIfDisabled() = runTest {
-        val permissionReader = HealthPermissionReader(context, disabledSessionFeature)
+        (fakeFeatureUtils as FakeFeatureUtils).setIsSessionTypesEnabled(false)
 
         assertThat(permissionReader.getDeclaredPermissions(TEST_APP_PACKAGE_NAME))
-            .doesNotContain(
-                listOf(
-                    HealthPermissions.READ_EXERCISE.toHealthPermission(),
-                    HealthPermissions.WRITE_EXERCISE.toHealthPermission(),
-                    HealthPermissions.WRITE_SLEEP.toHealthPermission(),
-                    HealthPermissions.READ_SLEEP.toHealthPermission(),
-                ))
-        assertThat(permissionReader.getDeclaredPermissions(TEST_APP_PACKAGE_NAME))
             .containsExactly(
                 HealthPermissions.WRITE_EXERCISE_ROUTE.toHealthPermission(),
                 HealthPermissions.READ_ACTIVE_CALORIES_BURNED.toHealthPermission(),
@@ -49,10 +49,7 @@
 
     @Test
     fun getDeclaredPermissions_hidesExerciseRouteIfDisabled() = runTest {
-        val permissionReader = HealthPermissionReader(context, disabledRouteFeature)
-
-        assertThat(permissionReader.getDeclaredPermissions(TEST_APP_PACKAGE_NAME))
-            .doesNotContain(listOf(HealthPermissions.WRITE_EXERCISE_ROUTE.toHealthPermission()))
+        (fakeFeatureUtils as FakeFeatureUtils).setIsExerciseRoutesEnabled(false)
 
         assertThat(permissionReader.getDeclaredPermissions(TEST_APP_PACKAGE_NAME))
             .containsExactly(
@@ -65,11 +62,8 @@
     }
 
     @Test
-    fun getDeclaredPermissions_returnsAllPermissions() = runTest {
-        val permissionReader = HealthPermissionReader(context, enabledFeatures)
-
-        assertThat(permissionReader.getDeclaredPermissions(TEST_APP_PACKAGE_NAME))
-            .doesNotContain(listOf(HealthPermissions.WRITE_EXERCISE_ROUTE.toHealthPermission()))
+    fun getDeclaredPermissions_filtersOutBackgroundReadPermission() = runTest {
+        (fakeFeatureUtils as FakeFeatureUtils).setIsBackgroundReadEnabled(true)
 
         assertThat(permissionReader.getDeclaredPermissions(TEST_APP_PACKAGE_NAME))
             .containsExactly(
@@ -82,72 +76,47 @@
                 HealthPermissions.WRITE_ACTIVE_CALORIES_BURNED.toHealthPermission())
     }
 
-    companion object {
-        private val disabledSessionFeature =
-            object : FeatureUtils {
-                override fun isSessionTypesEnabled(): Boolean {
-                    return false
-                }
+    @Test
+    fun getDeclaredPermissions_returnsAllPermissions_exceptReadAllRoutes() = runTest {
+        assertThat(permissionReader.getDeclaredPermissions(TEST_APP_PACKAGE_NAME))
+            .containsExactly(
+                HealthPermissions.WRITE_EXERCISE_ROUTE.toHealthPermission(),
+                HealthPermissions.READ_EXERCISE.toHealthPermission(),
+                HealthPermissions.WRITE_EXERCISE.toHealthPermission(),
+                HealthPermissions.WRITE_SLEEP.toHealthPermission(),
+                HealthPermissions.READ_SLEEP.toHealthPermission(),
+                HealthPermissions.READ_ACTIVE_CALORIES_BURNED.toHealthPermission(),
+                HealthPermissions.WRITE_ACTIVE_CALORIES_BURNED.toHealthPermission())
+    }
 
-                override fun isExerciseRouteEnabled(): Boolean {
-                    return true
-                }
+    @Test
+    fun isRationalIntentDeclared_withIntent_returnsTrue() {
+        assertThat(permissionReader.isRationalIntentDeclared(TEST_APP_PACKAGE_NAME)).isTrue()
+    }
 
-                override fun isEntryPointsEnabled(): Boolean {
-                    return true
-                }
+    @Test
+    fun isRationalIntentDeclared_noIntent_returnsTrue() {
+        assertThat(permissionReader.isRationalIntentDeclared(UNSUPPORTED_TEST_APP_PACKAGE_NAME))
+            .isFalse()
+    }
 
-                override fun isNewAppPriorityEnabled(): Boolean {
-                    return false
-                }
+    @Test
+    fun getAppsWithHealthPermissions_returnsSupportedApps() = runTest {
+        assertThat(permissionReader.getAppsWithHealthPermissions())
+            .containsAtLeast(TEST_APP_PACKAGE_NAME, TEST_APP_PACKAGE_NAME_2)
+    }
 
-                override fun isNewInformationArchitectureEnabled(): Boolean {
-                    return false
-                }
-            }
-        private val disabledRouteFeature =
-            object : FeatureUtils {
-                override fun isSessionTypesEnabled(): Boolean {
-                    return true
-                }
+    @Test
+    fun getAppsWithHealthPermissions_returnsDistinctApps() = runTest {
+        val apps = permissionReader.getAppsWithHealthPermissions()
+        assertThat(apps).isEqualTo(apps.distinct())
+    }
 
-                override fun isExerciseRouteEnabled(): Boolean {
-                    return false
-                }
 
-                override fun isEntryPointsEnabled(): Boolean {
-                    return true
-                }
-
-                override fun isNewAppPriorityEnabled(): Boolean {
-                    return true
-                }
-
-                override fun isNewInformationArchitectureEnabled(): Boolean {
-                    return false
-                }
-            }
-        private val enabledFeatures =
-            object : FeatureUtils {
-                override fun isSessionTypesEnabled(): Boolean {
-                    return true
-                }
-
-                override fun isExerciseRouteEnabled(): Boolean {
-                    return true
-                }
-                override fun isEntryPointsEnabled(): Boolean {
-                    return true
-                }
-
-                override fun isNewAppPriorityEnabled(): Boolean {
-                    return true
-                }
-
-                override fun isNewInformationArchitectureEnabled(): Boolean {
-                    return false
-                }
-            }
+    @Test
+    fun getAppsWithHealthPermissions_doesNotReturnUnsupportedApps() = runTest {
+        assertThat(permissionReader.getAppsWithHealthPermissions())
+            .doesNotContain(UNSUPPORTED_TEST_APP_PACKAGE_NAME)
     }
 
     private fun String.toHealthPermission(): HealthPermission {
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/utils/LiveDataExtensions.kt b/apk/tests/src/com/android/healthconnect/controller/tests/utils/LiveDataExtensions.kt
deleted file mode 100644
index 94453fe..0000000
--- a/apk/tests/src/com/android/healthconnect/controller/tests/utils/LiveDataExtensions.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2022 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.utils
-
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.Observer
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.TimeoutException
-
-fun <T> LiveData<T>.getOrAwaitValue(
-    time: Long = 2,
-    timeUnit: TimeUnit = TimeUnit.SECONDS,
-    callsCount: Int = 1
-): T {
-    var data: T? = null
-    val latch = CountDownLatch(callsCount)
-    val observer = Observer<T> { newData ->
-        data = newData
-        latch.countDown()
-    }
-
-    this.observeForever(observer)
-
-    try {
-        // Don't wait indefinitely if the LiveData is not set.
-        if (!latch.await(time, timeUnit)) {
-            throw TimeoutException("LiveData value was never set.")
-        }
-    } finally {
-        this.removeObserver(observer)
-    }
-
-    @Suppress("UNCHECKED_CAST") return data as T
-}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/utils/LiveDataUtils.kt b/apk/tests/src/com/android/healthconnect/controller/tests/utils/LiveDataUtils.kt
new file mode 100644
index 0000000..2341a73
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/utils/LiveDataUtils.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2022 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.utils
+
+import androidx.lifecycle.Observer
+
+/**
+ * An observer used in ViewModel tests that captures emitted LiveData values
+ * and provides an easy way to access the last posted value.
+ */
+class TestObserver<T> : Observer<T> {
+
+    private val observedValues = mutableListOf<T>()
+
+    override fun onChanged(value: T) {
+        observedValues.add(value)
+    }
+
+    fun getLastValue(): T {
+        return observedValues.last()
+    }
+}
\ No newline at end of file
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/utils/NavigationTestUtils.kt b/apk/tests/src/com/android/healthconnect/controller/tests/utils/NavigationTestUtils.kt
new file mode 100644
index 0000000..f357996
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/utils/NavigationTestUtils.kt
@@ -0,0 +1,12 @@
+package com.android.healthconnect.controller.tests.utils
+
+import android.content.Context
+import com.android.healthconnect.controller.onboarding.OnboardingActivity
+
+fun showOnboarding(context: Context, show: Boolean) {
+    val sharedPreference =
+        context.getSharedPreferences(OnboardingActivity.USER_ACTIVITY_TRACKER, Context.MODE_PRIVATE)
+    val editor = sharedPreference.edit()
+    editor.putBoolean(OnboardingActivity.ONBOARDING_SHOWN_PREF_KEY, !show)
+    editor.apply()
+}
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 ecc29d7..5c9532d 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
@@ -48,9 +48,13 @@
 }
 
 fun getMetaData(): Metadata {
+    return getMetaData(TEST_APP_PACKAGE_NAME)
+}
+
+fun getMetaData(packageName: String): Metadata {
     val device: Device =
         Device.Builder().setManufacturer("google").setModel("Pixel4a").setType(2).build()
-    val dataOrigin = DataOrigin.Builder().setPackageName(TEST_APP_PACKAGE_NAME).build()
+    val dataOrigin = DataOrigin.Builder().setPackageName(packageName).build()
     return Metadata.Builder()
         .setId("test_id")
         .setDevice(device)
@@ -59,10 +63,14 @@
         .build()
 }
 
+fun getDataOrigin(packageName: String): DataOrigin =
+    DataOrigin.Builder().setPackageName(packageName).build()
+
 // region apps
 
 const val TEST_APP_PACKAGE_NAME = "android.healthconnect.controller.test.app"
 const val TEST_APP_PACKAGE_NAME_2 = "android.healthconnect.controller.test.app2"
+const val TEST_APP_PACKAGE_NAME_3 = "package.name.3"
 const val UNSUPPORTED_TEST_APP_PACKAGE_NAME = "android.healthconnect.controller.test.app3"
 const val TEST_APP_NAME = "Health Connect test app"
 const val TEST_APP_NAME_2 = "Health Connect test app 2"
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 aab736b..c5ded85 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
@@ -13,11 +13,17 @@
  */
 package com.android.healthconnect.controller.tests.utils
 
+import com.android.healthconnect.controller.utils.SystemTimeSourceModule
 import com.android.healthconnect.controller.utils.TimeSource
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.components.SingletonComponent
+import dagger.hilt.testing.TestInstallIn
 import java.time.Instant
 import java.time.LocalDateTime
 import java.time.ZoneId
 import java.time.ZoneOffset.UTC
+import javax.inject.Singleton
 
 /** Time source for testing purposes. */
 object TestTimeSource : TimeSource {
@@ -28,3 +34,11 @@
     override fun currentLocalDateTime(): LocalDateTime =
         Instant.ofEpochMilli(currentTimeMillis()).atZone(deviceZoneOffset()).toLocalDateTime()
 }
+
+@Module
+@TestInstallIn(components = [SingletonComponent::class], replaces = [SystemTimeSourceModule::class])
+object TestTimeSourceModule {
+    @Provides
+    @Singleton
+    fun providesTestTimeSource() : TimeSource = TestTimeSource
+}
\ No newline at end of file
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 01fd39e..b64f3ad 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
@@ -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
@@ -13,8 +15,20 @@
  */
 package com.android.healthconnect.controller.tests.utils
 
+import com.android.healthconnect.controller.utils.atStartOfDay
 import com.android.healthconnect.controller.utils.getInstant
+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.toInstant
+import com.android.healthconnect.controller.utils.toInstantAtStartOfDay
+import com.android.healthconnect.controller.utils.toLocalDate
+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.LocalTime
 import java.time.ZoneId
 import java.util.TimeZone
 import org.junit.Test
@@ -27,4 +41,145 @@
 
         assertThat(getInstant(2022, 10, 23).toEpochMilli()).isEqualTo(1666483200000)
     }
+
+    @Test
+    fun longToInstant_returnsCorrectInstant() {
+        TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("UTC")))
+
+        val testLong = 1698317930L
+        assertThat(testLong.toInstant()).isEqualTo(Instant.ofEpochMilli(testLong))
+    }
+
+    @Test
+    fun instantToLocalDate_returnsCorrectLocalDate() {
+        // UTC - 7
+        TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("America/Los_Angeles")))
+
+        val instant = Instant.parse("2023-02-14T05:00:00Z")
+        val expected = LocalDate.of(2023, 2, 13)
+        val actual = instant.toLocalDate()
+        assertThat(actual).isEqualTo(expected)
+    }
+
+    @Test
+    fun instantToLocalTime_returnsCorrectLocalTime() {
+        // UTC + 9
+        TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("Asia/Tokyo")))
+
+        val instant = Instant.parse("2023-02-14T20:00:00Z")
+        val expected = LocalTime.of(5, 0, 0)
+        val actual = instant.toLocalTime()
+        assertThat(actual).isEqualTo(expected)
+    }
+
+    @Test
+    fun instantIsOnSameDay_whenOtherInstantOnSameLocalDate_returnsTrue() {
+        // UTC + 9
+        TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("Asia/Tokyo")))
+
+        val thisInstant = Instant.parse("2023-02-14T20:00:00Z")
+        val otherInstant = Instant.parse("2023-02-15T02:00:00Z")
+        assertThat(thisInstant.isOnSameDay(otherInstant)).isTrue()
+    }
+
+    @Test
+    fun instantIsOnSameDay_whenOtherInstantNotOnSameLocalDate_returnsFalse() {
+        // UTC + 9
+        TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("Asia/Tokyo")))
+
+        val thisInstant = Instant.parse("2023-02-14T10:00:00Z")
+        val otherInstant = Instant.parse("2023-02-14T20:00:00Z")
+        assertThat(thisInstant.isOnSameDay(otherInstant)).isFalse()
+    }
+
+    @Test
+    fun instantIsOnDayBefore_whenOtherInstantIsOnDayAfter_returnsTrue() {
+        // UTC + 2
+        TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("Africa/Cairo")))
+
+        val thisInstant = Instant.parse("2023-02-14T10:00:00Z")
+        val otherInstant = Instant.parse("2023-02-15T20:00:00Z")
+        assertThat(thisInstant.isOnDayBefore(otherInstant)).isTrue()
+    }
+
+    @Test
+    fun instantIsOnDayBefore_whenOtherInstantIsOnSameDay_returnsFalse() {
+        // UTC + 2
+        TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("Africa/Cairo")))
+
+        val thisInstant = Instant.parse("2023-02-14T10:00:00Z")
+        val otherInstant = Instant.parse("2023-02-14T20:00:00Z")
+        assertThat(thisInstant.isOnDayBefore(otherInstant)).isFalse()
+    }
+
+    @Test
+    fun instantIsOnDayAfter_whenOtherInstantIsOnDayBefore_returnsTrue() {
+        // UTC + 5:30
+        TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("Asia/Kolkata")))
+
+        val thisInstant = Instant.parse("2023-02-14T10:00:00Z")
+        val otherInstant = Instant.parse("2023-02-13T12:00:00Z")
+        assertThat(thisInstant.isOnDayAfter(otherInstant)).isTrue()
+    }
+
+    @Test
+    fun instantIsOnDayAfter_whenOtherInstantIsOnDayAfter_returnsFalse() {
+        // UTC + 5:30
+        TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("Asia/Kolkata")))
+
+        val thisInstant = Instant.parse("2023-02-14T10:00:00Z")
+        val otherInstant = Instant.parse("2023-02-15T12:00:00Z")
+        assertThat(thisInstant.isOnDayAfter(otherInstant)).isFalse()
+    }
+
+    @Test
+    fun instantAtStartOfDay_returnsLocalizedStartOfDay() {
+        // UTC + 8
+        TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("Australia/Perth")))
+
+        val testInstant = Instant.parse("2023-02-14T20:00:00Z")
+        val expectedInstant = Instant.parse("2023-02-14T16:00:00Z")
+
+        assertThat(testInstant.atStartOfDay()).isEqualTo(expectedInstant)
+    }
+
+    @Test
+    fun instantIsAtLeastOneDayAfter_whenOtherInstantOneDayBefore_returnsTrue() {
+        // UTC - 7
+        TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("America/Denver")))
+
+        val thisInstant = Instant.parse("2023-02-14T10:00:00Z")
+        val otherInstant = Instant.parse("2023-02-13T12:00:00Z")
+        assertThat(thisInstant.isAtLeastOneDayAfter(otherInstant)).isTrue()
+    }
+
+    @Test
+    fun instantIsAtLeastOneDayAfter_whenOtherInstantTwoDaysBefore_returnsTrue() {
+        // UTC - 7
+        TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("America/Denver")))
+
+        val thisInstant = Instant.parse("2023-02-15T10:00:00Z")
+        val otherInstant = Instant.parse("2023-02-13T12:00:00Z")
+        assertThat(thisInstant.isAtLeastOneDayAfter(otherInstant)).isTrue()
+    }
+
+    @Test
+    fun instantIsAtLeastOneDayAfter_whenOtherInstantAtLeastOnSameDay_returnsFalse() {
+        // UTC - 7
+        TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("America/Denver")))
+
+        val thisInstant = Instant.parse("2023-02-14T03:00:00Z")
+        val otherInstant = Instant.parse("2023-02-13T12:00:00Z")
+        assertThat(thisInstant.isAtLeastOneDayAfter(otherInstant)).isFalse()
+    }
+
+    @Test
+    fun localDateToInstantAtStartOfDay_returnsCorrectInstant() {
+        // UTC - 3
+        TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("America/Sao_Paulo")))
+
+        val testLocalDate = LocalDate.of(2021, 10, 1)
+        val expectedInstant = Instant.parse("2021-10-01T03:00:00Z")
+        assertThat(testLocalDate.toInstantAtStartOfDay()).isEqualTo(expectedInstant)
+    }
 }
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/utils/di/FakeAppUtils.kt b/apk/tests/src/com/android/healthconnect/controller/tests/utils/di/FakeAppUtils.kt
new file mode 100644
index 0000000..3e9c869
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/utils/di/FakeAppUtils.kt
@@ -0,0 +1,21 @@
+package com.android.healthconnect.controller.tests.utils.di
+
+import android.content.Context
+import com.android.healthconnect.controller.shared.app.AppUtils
+
+class FakeAppUtils : AppUtils {
+
+    private var defaultApp = ""
+
+    fun setDefaultApp(packageName: String) {
+        this.defaultApp = packageName
+    }
+
+    override fun isDefaultApp(context: Context, packageName: String): Boolean {
+        return this.defaultApp == packageName
+    }
+
+    fun reset() {
+        this.defaultApp = ""
+    }
+}
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 af07876..a718785 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
@@ -15,13 +15,34 @@
  */
 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.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.ILoadMostRecentAggregationsUseCase
+import com.android.healthconnect.controller.datasources.api.ILoadPotentialPriorityListUseCase
+import com.android.healthconnect.controller.datasources.api.IUpdatePriorityListUseCase
 import com.android.healthconnect.controller.permissions.connectedapps.ILoadHealthPermissionApps
+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.LocalDate
 
 class FakeRecentAccessUseCase : ILoadRecentAccessUseCase {
     private var list: List<AccessLog> = emptyList()
+
     fun updateList(list: List<AccessLog>) {
         this.list = list
     }
@@ -42,3 +63,185 @@
         return list
     }
 }
+
+class FakeLoadDataEntriesUseCase : ILoadDataEntriesUseCase {
+    private var list: List<FormattedEntry> = emptyList()
+
+    fun updateList(list: List<FormattedEntry>) {
+        this.list = list
+    }
+
+    override suspend fun invoke(input: LoadDataEntriesInput): UseCaseResults<List<FormattedEntry>> {
+        return UseCaseResults.Success(list)
+    }
+
+    override suspend fun execute(input: LoadDataEntriesInput): List<FormattedEntry> {
+        return list
+    }
+}
+
+class FakeLoadMenstruationDataUseCase : ILoadMenstruationDataUseCase {
+    private var list: List<FormattedEntry> = emptyList()
+
+    fun updateList(list: List<FormattedEntry>) {
+        this.list = list
+    }
+
+    override suspend fun invoke(
+        input: LoadMenstruationDataInput
+    ): UseCaseResults<List<FormattedEntry>> {
+        return UseCaseResults.Success(list)
+    }
+
+    override suspend fun execute(input: LoadMenstruationDataInput): List<FormattedEntry> {
+        return list
+    }
+}
+
+class FakeLoadDataAggregationsUseCase : ILoadDataAggregationsUseCase {
+    private var aggregation: FormattedEntry.FormattedAggregation =
+        FormattedEntry.FormattedAggregation("100 steps", "100 steps", "Test App")
+
+    private var aggregations: List<FormattedEntry.FormattedAggregation> = listOf(aggregation)
+    private var invocationCount = 0
+    private var shouldReturnFailed = false
+
+    fun updateAggregation(aggregation: FormattedEntry.FormattedAggregation) {
+        this.aggregations = listOf(aggregation)
+    }
+
+    /** Used for subsequent invocations when we need different responses */
+    fun updateAggregationResponses(aggregations: List<FormattedEntry.FormattedAggregation>) {
+        this.aggregations = aggregations
+    }
+
+    fun updateErrorResponse() {
+        this.shouldReturnFailed = true
+    }
+
+    override suspend fun invoke(
+        input: LoadAggregationInput
+    ): UseCaseResults<FormattedEntry.FormattedAggregation> {
+        return if (invocationCount >= this.aggregations.size) {
+            UseCaseResults.Failed(
+                IllegalStateException(
+                    "AggregationResponsesSize = ${this.aggregations.size}, " +
+                        "invocationCount = $invocationCount. Please update aggregation responses before invoking."))
+        } else if (shouldReturnFailed) {
+            UseCaseResults.Failed(IllegalStateException("Custom failure"))
+        } else {
+            val result = UseCaseResults.Success(aggregations[invocationCount])
+            invocationCount += 1
+            result
+        }
+    }
+
+    override suspend fun execute(input: LoadAggregationInput): FormattedEntry.FormattedAggregation {
+        return aggregation
+    }
+
+    fun reset() {
+        this.invocationCount = 0
+        this.aggregations = listOf(aggregation)
+        this.shouldReturnFailed = false
+    }
+}
+
+class FakeLoadMostRecentAggregationsUseCase : ILoadMostRecentAggregationsUseCase {
+
+    private var mostRecentAggregations = listOf<AggregationCardInfo>()
+
+    override suspend fun invoke(
+        healthDataCategory: @HealthDataCategoryInt Int
+    ): UseCaseResults<List<AggregationCardInfo>> {
+        return UseCaseResults.Success(mostRecentAggregations)
+    }
+
+    fun updateMostRecentAggregations(aggregations: List<AggregationCardInfo>) {
+        this.mostRecentAggregations = aggregations
+    }
+
+    fun reset() {
+        this.mostRecentAggregations = listOf()
+    }
+}
+
+class FakeLoadPotentialPriorityListUseCase : ILoadPotentialPriorityListUseCase {
+
+    private var potentialPriorityList = listOf<AppMetadata>()
+
+    override suspend fun invoke(
+        category: @HealthDataCategoryInt Int
+    ): UseCaseResults<List<AppMetadata>> {
+        return UseCaseResults.Success(potentialPriorityList)
+    }
+
+    fun updatePotentialPriorityList(potentialList: List<AppMetadata>) {
+        this.potentialPriorityList = potentialList
+    }
+
+    fun reset() {
+        this.potentialPriorityList = listOf()
+    }
+}
+
+class FakeLoadPriorityListUseCase : ILoadPriorityListUseCase {
+
+    private var priorityList = listOf<AppMetadata>()
+
+    override suspend fun invoke(
+        input: @HealthDataCategoryInt Int
+    ): UseCaseResults<List<AppMetadata>> {
+        return UseCaseResults.Success(priorityList)
+    }
+
+    override suspend fun execute(input: Int): List<AppMetadata> {
+        return priorityList
+    }
+
+    fun updatePriorityList(priorityList: List<AppMetadata>) {
+        this.priorityList = priorityList
+    }
+
+    fun reset() {
+        this.priorityList = listOf()
+    }
+}
+
+class FakeUpdatePriorityListUseCase : IUpdatePriorityListUseCase {
+
+    var priorityList = listOf<String>()
+    var category = HealthDataCategory.UNKNOWN
+
+    override suspend fun invoke(priorityList: List<String>, category: Int) {
+        this.priorityList = priorityList
+        this.category = category
+    }
+
+    fun reset() {
+        this.priorityList = listOf()
+        this.category = HealthDataCategory.UNKNOWN
+    }
+}
+
+class FakeLoadSleepDataUseCase : ILoadSleepDataUseCase {
+
+    private var sleepDataMap: MutableMap<LocalDate, List<Record>> = mutableMapOf()
+
+    fun updateSleepData(date: LocalDate, recordsList: List<Record>) {
+        sleepDataMap[date] = recordsList
+    }
+
+    override suspend fun invoke(input: LoadDataEntriesInput): UseCaseResults<List<Record>> {
+        val result = sleepDataMap.getOrDefault(input.displayedStartTime.toLocalDate(), listOf())
+        return UseCaseResults.Success(result)
+    }
+
+    override suspend fun execute(input: LoadDataEntriesInput): List<Record> {
+        return sleepDataMap.getOrDefault(input.displayedStartTime.toLocalDate(), listOf())
+    }
+
+    fun reset() {
+        this.sleepDataMap = mutableMapOf()
+    }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/utils/di/FakeFeatureUtils.kt b/apk/tests/src/com/android/healthconnect/controller/tests/utils/di/FakeFeatureUtils.kt
index 58c5695..46fa9e4 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/utils/di/FakeFeatureUtils.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/utils/di/FakeFeatureUtils.kt
@@ -12,9 +12,11 @@
 
     private var isSessionTypesEnabled = true
     private var isExerciseRoutesEnabled = true
+    private var isExerciseRoutesReadAllEnabled = true
     private var isEntryPointsEnabled = true
     private var isNewAppPriorityEnabled = false
     private var isNewInformationArchitectureEnabled = false
+    private var isBackgroundReadEnabled = false
 
     fun setIsSessionTypesEnabled(boolean: Boolean) {
         isSessionTypesEnabled = boolean
@@ -24,6 +26,10 @@
         isExerciseRoutesEnabled = boolean
     }
 
+    fun setIsExerciseRoutesReadAllEnabled(boolean: Boolean) {
+        isExerciseRoutesReadAllEnabled = boolean
+    }
+
     fun setIsEntryPointsEnabled(boolean: Boolean) {
         isEntryPointsEnabled = boolean
     }
@@ -36,6 +42,10 @@
         isNewInformationArchitectureEnabled = boolean
     }
 
+    fun setIsBackgroundReadEnabled(isBackgroundReadEnabled: Boolean) {
+        this.isBackgroundReadEnabled = isBackgroundReadEnabled
+    }
+
     override fun isNewAppPriorityEnabled(): Boolean {
         return isNewAppPriorityEnabled
     }
@@ -52,16 +62,21 @@
         return isExerciseRoutesEnabled
     }
 
+    override fun isExerciseRouteReadAllEnabled(): Boolean {
+        return isExerciseRoutesReadAllEnabled
+    }
+
     override fun isEntryPointsEnabled(): Boolean {
         return isEntryPointsEnabled
     }
 
+    override fun isBackgroundReadEnabled(): Boolean {
+        return isBackgroundReadEnabled
+    }
 }
 
 @Module
 @TestInstallIn(components = [SingletonComponent::class], replaces = [FeaturesModule::class])
 object FakeFeaturesUtilsModule {
-    @Provides
-    @Singleton
-    fun providesFeaturesUtils() : FeatureUtils =  FakeFeatureUtils()
+    @Provides @Singleton fun providesFeaturesUtils(): FeatureUtils = FakeFeatureUtils()
 }
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/utils/di/FakeHealthPermissionManager.kt b/apk/tests/src/com/android/healthconnect/controller/tests/utils/di/FakeHealthPermissionManager.kt
index 077f470..821047f 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/utils/di/FakeHealthPermissionManager.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/utils/di/FakeHealthPermissionManager.kt
@@ -32,6 +32,17 @@
         return grantedPermissions.getOrDefault(packageName, emptyList())
     }
 
+    override fun getHealthPermissionsFlags(
+        packageName: String,
+        permissions: List<String>
+    ): Map<String, Int> {
+        TODO("Not yet implemented")
+    }
+
+    override fun makeHealthPermissionsRequestable(packageName: String, permissions: List<String>) {
+        TODO("Not yet implemented")
+    }
+
     override fun grantHealthPermission(packageName: String, permissionName: String) {
         val permissions = grantedPermissions.getOrDefault(packageName, mutableListOf())
         permissions.add(permissionName)
diff --git a/framework/java/android/health/connect/Constants.java b/framework/java/android/health/connect/Constants.java
index b667def..5282dd5 100644
--- a/framework/java/android/health/connect/Constants.java
+++ b/framework/java/android/health/connect/Constants.java
@@ -33,7 +33,7 @@
     public static final double DEFAULT_DOUBLE = Double.MIN_VALUE;
     public static final int DEFAULT_PAGE_SIZE = 1000;
     public static final int MAXIMUM_PAGE_SIZE = 5000;
-
+    public static final int MAXIMUM_ALLOWED_CURSOR_COUNT = 100_000;
     public static final int UPSERT = 0;
     public static final int DELETE = 1;
     public static final int READ = 2;
diff --git a/framework/java/android/health/connect/HealthConnectManager.java b/framework/java/android/health/connect/HealthConnectManager.java
index d7422e2..fcd4e3b 100644
--- a/framework/java/android/health/connect/HealthConnectManager.java
+++ b/framework/java/android/health/connect/HealthConnectManager.java
@@ -88,6 +88,7 @@
 import android.os.OutcomeReceiver;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
+import android.os.UserHandle;
 import android.util.Log;
 
 import java.lang.annotation.Retention;
@@ -250,6 +251,7 @@
     @SystemApi
     public static final String ACTION_HEALTH_CONNECT_MIGRATION_READY =
             "android.health.connect.action.HEALTH_CONNECT_MIGRATION_READY";
+
     /**
      * Unknown download state considered to be the default download state.
      *
@@ -258,6 +260,7 @@
      * @hide
      */
     @SystemApi public static final int DATA_DOWNLOAD_STATE_UNKNOWN = 0;
+
     /**
      * Indicates that the download has started.
      *
@@ -266,6 +269,7 @@
      * @hide
      */
     @SystemApi public static final int DATA_DOWNLOAD_STARTED = 1;
+
     /**
      * Indicates that the download is being retried.
      *
@@ -274,6 +278,7 @@
      * @hide
      */
     @SystemApi public static final int DATA_DOWNLOAD_RETRY = 2;
+
     /**
      * Indicates that the download has failed.
      *
@@ -282,6 +287,7 @@
      * @hide
      */
     @SystemApi public static final int DATA_DOWNLOAD_FAILED = 3;
+
     /**
      * Indicates that the download has completed.
      *
@@ -396,6 +402,58 @@
     }
 
     /**
+     * Returns permission flags for the given package name and Health permissions.
+     *
+     * <p>This is equivalent to calling {@link PackageManager#getPermissionFlags(String, String,
+     * UserHandle)} for each provided permission except it throws an exception for non-Health or
+     * undeclared permissions. Flag masks listed in {@link PackageManager#MASK_PERMISSION_FLAGS_ALL}
+     * can be used to check the flag values.
+     *
+     * <p>Returned flags for invalid, non-Health or undeclared permissions are equal to zero.
+     *
+     * @return a map which contains all requested permissions as keys and corresponding flags as
+     *     values.
+     * @throws IllegalArgumentException if the package doesn't exist, any of the permissions are not
+     *     Health permissions or not declared by the app.
+     * @throws NullPointerException if any of the arguments is {@code null}.
+     * @throws SecurityException if the caller doesn't possess {@code
+     *     android.permission.MANAGE_HEALTH_PERMISSIONS}.
+     * @hide
+     */
+    @RequiresPermission(MANAGE_HEALTH_PERMISSIONS)
+    @UserHandleAware
+    public Map<String, Integer> getHealthPermissionsFlags(
+            @NonNull String packageName, @NonNull List<String> permissions) {
+        try {
+            return mService.getHealthPermissionsFlags(packageName, mContext.getUser(), permissions);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Allow provided permissions to be requested again if they previously were denied multiple
+     * times by the users.
+     *
+     * @throws IllegalArgumentException if the package doesn't exist, any of the permissions are not
+     *     Health permissions or not declared by the app.
+     * @throws NullPointerException if any of the arguments is {@code null}.
+     * @throws SecurityException if the caller doesn't possess {@code
+     *     android.permission.MANAGE_HEALTH_PERMISSIONS}.
+     * @hide
+     */
+    @RequiresPermission(MANAGE_HEALTH_PERMISSIONS)
+    @UserHandleAware
+    public void makeHealthPermissionsRequestable(
+            @NonNull String packageName, @NonNull List<String> permissions) {
+        try {
+            mService.makeHealthPermissionsRequestable(packageName, mContext.getUser(), permissions);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Returns the date from which an app have access to the historical health data. Returns null if
      * the package doesn't have historical access date.
      *
diff --git a/framework/java/android/health/connect/HealthPermissions.java b/framework/java/android/health/connect/HealthPermissions.java
index 3ce7fca..81195ab 100644
--- a/framework/java/android/health/connect/HealthPermissions.java
+++ b/framework/java/android/health/connect/HealthPermissions.java
@@ -56,10 +56,13 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
+import android.content.Context;
+import android.content.pm.PackageInfo;
 import android.health.connect.datatypes.ExerciseRoute;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -128,6 +131,8 @@
      *
      * @hide
      */
+    // TODO (b/299897306): Specify a label for the permission in HealthPermissionsManifest.xml when
+    // exposing this constant. Also search for 299897306 and remove workarounds.
     public static final String READ_HEALTH_DATA_IN_BACKGROUND =
             "android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND";
 
@@ -138,12 +143,14 @@
      */
     public static final String READ_ACTIVE_CALORIES_BURNED =
             "android.permission.health.READ_ACTIVE_CALORIES_BURNED";
+
     /**
      * Allows an application to read the user's distance data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String READ_DISTANCE = "android.permission.health.READ_DISTANCE";
+
     /**
      * Allows an application to read the user's elevation gained data.
      *
@@ -151,6 +158,7 @@
      */
     public static final String READ_ELEVATION_GAINED =
             "android.permission.health.READ_ELEVATION_GAINED";
+
     /**
      * Allows an application to read the user's exercise data.
      *
@@ -171,18 +179,28 @@
             "android.permission.health.READ_EXERCISE_ROUTE";
 
     /**
+     * Allows an application to read any {@link ExerciseRoute}.
+     *
+     * @hide
+     */
+    public static final String READ_EXERCISE_ROUTES_ALL =
+            "android.permission.health.READ_EXERCISE_ROUTES_ALL";
+
+    /**
      * Allows an application to read the user's floors climbed data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String READ_FLOORS_CLIMBED =
             "android.permission.health.READ_FLOORS_CLIMBED";
+
     /**
      * Allows an application to read the user's steps data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String READ_STEPS = "android.permission.health.READ_STEPS";
+
     /**
      * Allows an application to read the user's total calories burned data.
      *
@@ -190,12 +208,14 @@
      */
     public static final String READ_TOTAL_CALORIES_BURNED =
             "android.permission.health.READ_TOTAL_CALORIES_BURNED";
+
     /**
      * Allows an application to read the user's vo2 maximum data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String READ_VO2_MAX = "android.permission.health.READ_VO2_MAX";
+
     /**
      * Allows an application to read the user's wheelchair pushes data.
      *
@@ -203,18 +223,21 @@
      */
     public static final String READ_WHEELCHAIR_PUSHES =
             "android.permission.health.READ_WHEELCHAIR_PUSHES";
+
     /**
      * Allows an application to read the user's power data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String READ_POWER = "android.permission.health.READ_POWER";
+
     /**
      * Allows an application to read the user's speed data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String READ_SPEED = "android.permission.health.READ_SPEED";
+
     /**
      * Allows an application to read the user's basal metabolic rate data.
      *
@@ -222,12 +245,14 @@
      */
     public static final String READ_BASAL_METABOLIC_RATE =
             "android.permission.health.READ_BASAL_METABOLIC_RATE";
+
     /**
      * Allows an application to read the user's body fat data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String READ_BODY_FAT = "android.permission.health.READ_BODY_FAT";
+
     /**
      * Allows an application to read the user's body water mass data.
      *
@@ -235,18 +260,21 @@
      */
     public static final String READ_BODY_WATER_MASS =
             "android.permission.health.READ_BODY_WATER_MASS";
+
     /**
      * Allows an application to read the user's bone mass data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String READ_BONE_MASS = "android.permission.health.READ_BONE_MASS";
+
     /**
      * Allows an application to read the user's height data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String READ_HEIGHT = "android.permission.health.READ_HEIGHT";
+
     /**
      * Allows an application to read the user's lean body mass data.
      *
@@ -254,12 +282,14 @@
      */
     public static final String READ_LEAN_BODY_MASS =
             "android.permission.health.READ_LEAN_BODY_MASS";
+
     /**
      * Allows an application to read the user's weight data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String READ_WEIGHT = "android.permission.health.READ_WEIGHT";
+
     /**
      * Allows an application to read the user's cervical mucus data.
      *
@@ -267,12 +297,14 @@
      */
     public static final String READ_CERVICAL_MUCUS =
             "android.permission.health.READ_CERVICAL_MUCUS";
+
     /**
      * Allows an application to read the user's menstruation data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String READ_MENSTRUATION = "android.permission.health.READ_MENSTRUATION";
+
     /**
      * Allows an application to read the user's intermenstrual bleeding data.
      *
@@ -280,6 +312,7 @@
      */
     public static final String READ_INTERMENSTRUAL_BLEEDING =
             "android.permission.health.READ_INTERMENSTRUAL_BLEEDING";
+
     /**
      * Allows an application to read the user's ovulation test data.
      *
@@ -287,6 +320,7 @@
      */
     public static final String READ_OVULATION_TEST =
             "android.permission.health.READ_OVULATION_TEST";
+
     /**
      * Allows an application to read the user's sexual activity data.
      *
@@ -294,24 +328,28 @@
      */
     public static final String READ_SEXUAL_ACTIVITY =
             "android.permission.health.READ_SEXUAL_ACTIVITY";
+
     /**
      * Allows an application to read the user's hydration data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String READ_HYDRATION = "android.permission.health.READ_HYDRATION";
+
     /**
      * Allows an application to read the user's nutrition data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String READ_NUTRITION = "android.permission.health.READ_NUTRITION";
+
     /**
      * Allows an application to read the user's sleep data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String READ_SLEEP = "android.permission.health.READ_SLEEP";
+
     /**
      * Allows an application to read the user's body temperature data.
      *
@@ -319,12 +357,14 @@
      */
     public static final String READ_BASAL_BODY_TEMPERATURE =
             "android.permission.health.READ_BASAL_BODY_TEMPERATURE";
+
     /**
      * Allows an application to read the user's blood glucose data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String READ_BLOOD_GLUCOSE = "android.permission.health.READ_BLOOD_GLUCOSE";
+
     /**
      * Allows an application to read the user's blood pressure data.
      *
@@ -332,6 +372,7 @@
      */
     public static final String READ_BLOOD_PRESSURE =
             "android.permission.health.READ_BLOOD_PRESSURE";
+
     /**
      * Allows an application to read the user's body temperature data.
      *
@@ -339,12 +380,14 @@
      */
     public static final String READ_BODY_TEMPERATURE =
             "android.permission.health.READ_BODY_TEMPERATURE";
+
     /**
      * Allows an application to read the user's heart rate data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String READ_HEART_RATE = "android.permission.health.READ_HEART_RATE";
+
     /**
      * Allows an application to read the user's heart rate variability data.
      *
@@ -352,6 +395,7 @@
      */
     public static final String READ_HEART_RATE_VARIABILITY =
             "android.permission.health.READ_HEART_RATE_VARIABILITY";
+
     /**
      * Allows an application to read the user's oxygen saturation data.
      *
@@ -359,6 +403,7 @@
      */
     public static final String READ_OXYGEN_SATURATION =
             "android.permission.health.READ_OXYGEN_SATURATION";
+
     /**
      * Allows an application to read the user's respiratory rate data.
      *
@@ -366,6 +411,7 @@
      */
     public static final String READ_RESPIRATORY_RATE =
             "android.permission.health.READ_RESPIRATORY_RATE";
+
     /**
      * Allows an application to read the user's resting heart rate data.
      *
@@ -373,6 +419,7 @@
      */
     public static final String READ_RESTING_HEART_RATE =
             "android.permission.health.READ_RESTING_HEART_RATE";
+
     /**
      * Allows an application to write the user's calories burned data.
      *
@@ -380,12 +427,14 @@
      */
     public static final String WRITE_ACTIVE_CALORIES_BURNED =
             "android.permission.health.WRITE_ACTIVE_CALORIES_BURNED";
+
     /**
      * Allows an application to write the user's distance data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String WRITE_DISTANCE = "android.permission.health.WRITE_DISTANCE";
+
     /**
      * Allows an application to write the user's elevation gained data.
      *
@@ -393,6 +442,7 @@
      */
     public static final String WRITE_ELEVATION_GAINED =
             "android.permission.health.WRITE_ELEVATION_GAINED";
+
     /**
      * Allows an application to write the user's exercise data. Additional permission {@link
      * HealthPermissions#WRITE_EXERCISE_ROUTE} is required to write user's exercise route.
@@ -400,6 +450,7 @@
      * <p>Protection level: dangerous.
      */
     public static final String WRITE_EXERCISE = "android.permission.health.WRITE_EXERCISE";
+
     /**
      * Allows an application to write the user's exercise route.
      *
@@ -407,6 +458,7 @@
      */
     public static final String WRITE_EXERCISE_ROUTE =
             "android.permission.health.WRITE_EXERCISE_ROUTE";
+
     /**
      * Allows an application to write the user's floors climbed data.
      *
@@ -414,12 +466,14 @@
      */
     public static final String WRITE_FLOORS_CLIMBED =
             "android.permission.health.WRITE_FLOORS_CLIMBED";
+
     /**
      * Allows an application to write the user's steps data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String WRITE_STEPS = "android.permission.health.WRITE_STEPS";
+
     /**
      * Allows an application to write the user's total calories burned data.
      *
@@ -427,12 +481,14 @@
      */
     public static final String WRITE_TOTAL_CALORIES_BURNED =
             "android.permission.health.WRITE_TOTAL_CALORIES_BURNED";
+
     /**
      * Allows an application to write the user's vo2 maximum data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String WRITE_VO2_MAX = "android.permission.health.WRITE_VO2_MAX";
+
     /**
      * Allows an application to write the user's wheelchair pushes data.
      *
@@ -440,18 +496,21 @@
      */
     public static final String WRITE_WHEELCHAIR_PUSHES =
             "android.permission.health.WRITE_WHEELCHAIR_PUSHES";
+
     /**
      * Allows an application to write the user's power data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String WRITE_POWER = "android.permission.health.WRITE_POWER";
+
     /**
      * Allows an application to write the user's speed data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String WRITE_SPEED = "android.permission.health.WRITE_SPEED";
+
     /**
      * Allows an application to write the user's basal metabolic rate data.
      *
@@ -459,12 +518,14 @@
      */
     public static final String WRITE_BASAL_METABOLIC_RATE =
             "android.permission.health.WRITE_BASAL_METABOLIC_RATE";
+
     /**
      * Allows an application to write the user's body fat data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String WRITE_BODY_FAT = "android.permission.health.WRITE_BODY_FAT";
+
     /**
      * Allows an application to write the user's body water mass data.
      *
@@ -472,18 +533,21 @@
      */
     public static final String WRITE_BODY_WATER_MASS =
             "android.permission.health.WRITE_BODY_WATER_MASS";
+
     /**
      * Allows an application to write the user's bone mass data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String WRITE_BONE_MASS = "android.permission.health.WRITE_BONE_MASS";
+
     /**
      * Allows an application to write the user's height data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String WRITE_HEIGHT = "android.permission.health.WRITE_HEIGHT";
+
     /**
      * Allows an application to write the user's lean body mass data.
      *
@@ -491,12 +555,14 @@
      */
     public static final String WRITE_LEAN_BODY_MASS =
             "android.permission.health.WRITE_LEAN_BODY_MASS";
+
     /**
      * Allows an application to write the user's weight data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String WRITE_WEIGHT = "android.permission.health.WRITE_WEIGHT";
+
     /**
      * Allows an application to write the user's cervical mucus data.
      *
@@ -504,12 +570,14 @@
      */
     public static final String WRITE_CERVICAL_MUCUS =
             "android.permission.health.WRITE_CERVICAL_MUCUS";
+
     /**
      * Allows an application to write the user's menstruation data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String WRITE_MENSTRUATION = "android.permission.health.WRITE_MENSTRUATION";
+
     /**
      * Allows an application to write the user's intermenstrual bleeding data.
      *
@@ -517,6 +585,7 @@
      */
     public static final String WRITE_INTERMENSTRUAL_BLEEDING =
             "android.permission.health.WRITE_INTERMENSTRUAL_BLEEDING";
+
     /**
      * Allows an application to write the user's ovulation test data.
      *
@@ -524,6 +593,7 @@
      */
     public static final String WRITE_OVULATION_TEST =
             "android.permission.health.WRITE_OVULATION_TEST";
+
     /**
      * Allows an application to write the user's sexual activity data.
      *
@@ -531,24 +601,28 @@
      */
     public static final String WRITE_SEXUAL_ACTIVITY =
             "android.permission.health.WRITE_SEXUAL_ACTIVITY";
+
     /**
      * Allows an application to write the user's hydration data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String WRITE_HYDRATION = "android.permission.health.WRITE_HYDRATION";
+
     /**
      * Allows an application to write the user's nutrition data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String WRITE_NUTRITION = "android.permission.health.WRITE_NUTRITION";
+
     /**
      * Allows an application to write the user's sleep data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String WRITE_SLEEP = "android.permission.health.WRITE_SLEEP";
+
     /**
      * Allows an application to write the user's basal body temperature data.
      *
@@ -556,6 +630,7 @@
      */
     public static final String WRITE_BASAL_BODY_TEMPERATURE =
             "android.permission.health.WRITE_BASAL_BODY_TEMPERATURE";
+
     /**
      * Allows an application to write the user's blood glucose data.
      *
@@ -563,6 +638,7 @@
      */
     public static final String WRITE_BLOOD_GLUCOSE =
             "android.permission.health.WRITE_BLOOD_GLUCOSE";
+
     /**
      * Allows an application to write the user's blood pressure data.
      *
@@ -570,6 +646,7 @@
      */
     public static final String WRITE_BLOOD_PRESSURE =
             "android.permission.health.WRITE_BLOOD_PRESSURE";
+
     /**
      * Allows an application to write the user's body temperature data.
      *
@@ -577,12 +654,14 @@
      */
     public static final String WRITE_BODY_TEMPERATURE =
             "android.permission.health.WRITE_BODY_TEMPERATURE";
+
     /**
      * Allows an application to write the user's heart rate data.
      *
      * <p>Protection level: dangerous.
      */
     public static final String WRITE_HEART_RATE = "android.permission.health.WRITE_HEART_RATE";
+
     /**
      * Allows an application to write the user's heart rate variability data.
      *
@@ -590,6 +669,7 @@
      */
     public static final String WRITE_HEART_RATE_VARIABILITY =
             "android.permission.health.WRITE_HEART_RATE_VARIABILITY";
+
     /**
      * Allows an application to write the user's oxygen saturation data.
      *
@@ -597,6 +677,7 @@
      */
     public static final String WRITE_OXYGEN_SATURATION =
             "android.permission.health.WRITE_OXYGEN_SATURATION";
+
     /**
      * Allows an application to write the user's respiratory rate data.
      *
@@ -604,6 +685,7 @@
      */
     public static final String WRITE_RESPIRATORY_RATE =
             "android.permission.health.WRITE_RESPIRATORY_RATE";
+
     /**
      * Allows an application to write the user's resting heart rate data.
      *
@@ -673,12 +755,12 @@
     }
 
     /**
-     * @return {@link HealthDataCategory} for {@code permissionName}. -1 if permission category for
-     *     {@code permissionName} is not found
+     * @return {@link HealthDataCategory} for a WRITE {@code permissionName}. -1 if permission
+     *     category for {@code permissionName} is not found (or if {@code permissionName} is READ)
      * @hide
      */
     @HealthDataCategory.Type
-    public static int getHealthDataCategory(@Nullable String permissionName) {
+    public static int getHealthDataCategoryForWritePermission(@Nullable String permissionName) {
         if (sWriteHealthPermissionToHealthDataCategoryMap.isEmpty()) {
             populateWriteHealthPermissionToHealthDataCategoryMap();
         }
@@ -726,6 +808,50 @@
         return healthWritePermission;
     }
 
+    /**
+     * Returns a set of dataCategories for which this package has WRITE permissions
+     *
+     * @hide
+     */
+    @NonNull
+    public static Set<Integer> getDataCategoriesWithWritePermissionsForPackage(
+            @NonNull PackageInfo packageInfo, @NonNull Context context) {
+
+        Set<Integer> dataCategoriesWithPermissions = new HashSet<>();
+
+        for (int i = 0; i < packageInfo.requestedPermissions.length; i++) {
+            String currPerm = packageInfo.requestedPermissions[i];
+            if (!HealthConnectManager.isHealthPermission(context, currPerm)) {
+                continue;
+            }
+            if ((packageInfo.requestedPermissionsFlags[i]
+                            & PackageInfo.REQUESTED_PERMISSION_GRANTED)
+                    == 0) {
+                continue;
+            }
+
+            int dataCategory = getHealthDataCategoryForWritePermission(currPerm);
+            if (dataCategory >= 0) {
+                dataCategoriesWithPermissions.add(dataCategory);
+            }
+        }
+
+        return dataCategoriesWithPermissions;
+    }
+
+    /**
+     * Returns true if this package has at least one granted WRITE permission for this category.
+     *
+     * @hide
+     */
+    public static boolean getPackageHasWriteHealthPermissionsForCategory(
+            @NonNull PackageInfo packageInfo,
+            @HealthDataCategory.Type int dataCategory,
+            @NonNull Context context) {
+        return getDataCategoriesWithWritePermissionsForPackage(packageInfo, context)
+                .contains(dataCategory);
+    }
+
     private static synchronized void populateHealthPermissionToHealthPermissionCategoryMap() {
         if (!sHealthCategoryToWritePermissionMap.isEmpty()) {
             return;
diff --git a/framework/java/android/health/connect/ReadRecordsRequestUsingFilters.java b/framework/java/android/health/connect/ReadRecordsRequestUsingFilters.java
index 8f7e3a9..f3329d2 100644
--- a/framework/java/android/health/connect/ReadRecordsRequestUsingFilters.java
+++ b/framework/java/android/health/connect/ReadRecordsRequestUsingFilters.java
@@ -62,7 +62,7 @@
         mDataOrigins = dataOrigins;
         mPageSize = pageSize;
         if (pageToken != DEFAULT_LONG) {
-            mAscending = pageToken % 2 == 0 ? true : false;
+            mAscending = pageToken % 2 == 0;
         } else {
             mAscending = ascending;
         }
diff --git a/framework/java/android/health/connect/ReadRecordsRequestUsingIds.java b/framework/java/android/health/connect/ReadRecordsRequestUsingIds.java
index ec71d2a..6e8b9ca 100644
--- a/framework/java/android/health/connect/ReadRecordsRequestUsingIds.java
+++ b/framework/java/android/health/connect/ReadRecordsRequestUsingIds.java
@@ -16,6 +16,8 @@
 
 package android.health.connect;
 
+import static android.health.connect.Constants.MAXIMUM_PAGE_SIZE;
+
 import android.annotation.NonNull;
 import android.annotation.SuppressLint;
 import android.health.connect.aidl.ReadRecordsRequestParcel;
@@ -79,23 +81,43 @@
         }
 
         /**
+         * Add an UUID to the read request.
+         *
+         * <p>The maximum number of ids in a single {@link ReadRecordsRequestUsingIds} that Health
+         * Connect accepts is 5000. The limit includes all {@code id}s and {@code clientId}s.
+         *
          * @param id Identifier generated by the platform and returned by {@link
          *     HealthConnectManager#insertRecords}
+         * @see #addClientRecordId(String)
          */
         @NonNull
         @SuppressLint("MissingGetterMatchingBuilder")
         public Builder<T> addId(@NonNull String id) {
+            if (mRecordIdFiltersList.size() >= MAXIMUM_PAGE_SIZE) {
+                throw new IllegalArgumentException(
+                        "Maximum allowed pageSize is " + MAXIMUM_PAGE_SIZE);
+            }
             mRecordIdFiltersList.add(RecordIdFilter.fromId(mRecordType, id));
             return this;
         }
 
         /**
+         * Add a client id to the read request.
+         *
+         * <p>The maximum number of ids in a single {@link ReadRecordsRequestUsingIds} that Health
+         * Connect accepts is 5000. The limit includes all {@code id}s and {@code clientId}s.
+         *
          * @param clientRecordId identifier that was set while inserting the record
          * @see Metadata
+         * @see #addId(String)
          */
         @SuppressLint("MissingGetterMatchingBuilder")
         @NonNull
         public Builder<T> addClientRecordId(@NonNull String clientRecordId) {
+            if (mRecordIdFiltersList.size() >= MAXIMUM_PAGE_SIZE) {
+                throw new IllegalArgumentException(
+                        "Maximum allowed pageSize is " + MAXIMUM_PAGE_SIZE);
+            }
             mRecordIdFiltersList.add(
                     RecordIdFilter.fromClientRecordId(mRecordType, clientRecordId));
             return this;
diff --git a/framework/java/android/health/connect/aidl/IHealthConnectService.aidl b/framework/java/android/health/connect/aidl/IHealthConnectService.aidl
index 542f2ae..e2d1c6f 100644
--- a/framework/java/android/health/connect/aidl/IHealthConnectService.aidl
+++ b/framework/java/android/health/connect/aidl/IHealthConnectService.aidl
@@ -34,6 +34,7 @@
 import android.os.UserHandle;
 
 import java.util.List;
+import java.util.Map;
 
 /**
  * Interface for {@link com.android.health.connect.HealthConnectManager}
@@ -45,6 +46,17 @@
     void revokeAllHealthPermissions(String packageName, String reason, in UserHandle user);
     List<String> getGrantedHealthPermissions(String packageName, in UserHandle user);
 
+    /**
+     * Returns a Map<String, Integer> from a permission name to permission flags.
+     * @hide
+     */
+    Map getHealthPermissionsFlags(String packageName, in UserHandle user, in List<String> permissions);
+
+    /**
+     * @hide
+     */
+    void makeHealthPermissionsRequestable(String packageName, in UserHandle user, in List<String> permissions);
+
     /* @hide */
     long getHistoricalAccessStartDateInMilliseconds(String packageName, in UserHandle user);
 
diff --git a/framework/java/android/health/connect/aidl/ReadRecordsRequestParcel.java b/framework/java/android/health/connect/aidl/ReadRecordsRequestParcel.java
index 67a3f6d..7dd5ad3 100644
--- a/framework/java/android/health/connect/aidl/ReadRecordsRequestParcel.java
+++ b/framework/java/android/health/connect/aidl/ReadRecordsRequestParcel.java
@@ -141,6 +141,10 @@
         return mPageToken;
     }
 
+    /**
+     * {@code mPageToken} should contain the correct value for {@code mAscending}. Only use this
+     * directly if {@code pageToken} is not set.
+     */
     public boolean isAscending() {
         return mAscending;
     }
diff --git a/framework/java/android/health/connect/datatypes/ExerciseSessionRecord.java b/framework/java/android/health/connect/datatypes/ExerciseSessionRecord.java
index eaceb07..9975479 100644
--- a/framework/java/android/health/connect/datatypes/ExerciseSessionRecord.java
+++ b/framework/java/android/health/connect/datatypes/ExerciseSessionRecord.java
@@ -409,6 +409,7 @@
             recordInternal.setTitle(getTitle().toString());
         }
 
+        recordInternal.setHasRoute(hasRoute());
         if (getRoute() != null) {
             recordInternal.setRoute(getRoute().toRouteInternal());
         }
diff --git a/service/Android.bp b/service/Android.bp
index 0744d79..f47fa05 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -40,6 +40,7 @@
         "framework-sdkextensions",
         "framework-configinfrastructure",
         "keepanno-annotations",
+        "modules-utils-preconditions",
     ],
     static_libs: [
         "guava",
diff --git a/service/java/com/android/server/healthconnect/HealthConnectDeviceConfigManager.java b/service/java/com/android/server/healthconnect/HealthConnectDeviceConfigManager.java
index 392abbd..5251636 100644
--- a/service/java/com/android/server/healthconnect/HealthConnectDeviceConfigManager.java
+++ b/service/java/com/android/server/healthconnect/HealthConnectDeviceConfigManager.java
@@ -118,6 +118,10 @@
     @VisibleForTesting
     public static final String BACKGROUND_READ_FEATURE_FLAG = "background_read_enable";
 
+    @VisibleForTesting
+    public static final String ENABLE_AGGREGATION_SOURCE_CONTROLS_FLAG =
+            "aggregation_source_controls_enable";
+
     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;
@@ -161,6 +165,9 @@
 
     @VisibleForTesting public static final boolean BACKGROUND_READ_DEFAULT_FLAG_VALUE = false;
 
+    @VisibleForTesting
+    public static final boolean ENABLE_AGGREGATION_SOURCE_CONTROLS_DEFAULT_FLAG_VALUE = false;
+
     private static HealthConnectDeviceConfigManager sDeviceConfigManager;
     private final ReentrantReadWriteLock mLock = new ReentrantReadWriteLock();
     private static final String HEALTH_FITNESS_NAMESPACE = DeviceConfig.NAMESPACE_HEALTH_FITNESS;
@@ -270,6 +277,13 @@
                     BACKGROUND_READ_FEATURE_FLAG,
                     BACKGROUND_READ_DEFAULT_FLAG_VALUE);
 
+    @GuardedBy("mLock")
+    private boolean mAggregationSourceControlsEnabled =
+            DeviceConfig.getBoolean(
+                    HEALTH_FITNESS_NAMESPACE,
+                    ENABLE_AGGREGATION_SOURCE_CONTROLS_FLAG,
+                    ENABLE_AGGREGATION_SOURCE_CONTROLS_DEFAULT_FLAG_VALUE);
+
     @NonNull
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     public static void initializeInstance(Context context) {
@@ -307,6 +321,7 @@
         sFlagsToTrack.add(ENABLE_COMPLETE_STATE_CHANGE_JOBS_FLAG);
         sFlagsToTrack.add(ENABLE_MIGRATION_NOTIFICATIONS_FLAG);
         sFlagsToTrack.add(BACKGROUND_READ_FEATURE_FLAG);
+        sFlagsToTrack.add(ENABLE_AGGREGATION_SOURCE_CONTROLS_FLAG);
     }
 
     /** Returns if operations with exercise route are enabled. */
@@ -478,6 +493,16 @@
         }
     }
 
+    /** Returns whether the new aggregation source control feature is enabled or not. */
+    public boolean isAggregationSourceControlsEnabled() {
+        mLock.readLock().lock();
+        try {
+            return mAggregationSourceControlsEnabled;
+        } finally {
+            mLock.readLock().unlock();
+        }
+    }
+
     /** Updates rate limiting quota values. */
     public void updateRateLimiterValues() {
         Map<Integer, Integer> quotaBucketToMaxRollingQuotaMap = new HashMap<>();
@@ -674,6 +699,11 @@
                                         BACKGROUND_READ_FEATURE_FLAG,
                                         BACKGROUND_READ_DEFAULT_FLAG_VALUE);
                         break;
+                    case ENABLE_AGGREGATION_SOURCE_CONTROLS_FLAG:
+                        mAggregationSourceControlsEnabled =
+                                properties.getBoolean(
+                                        ENABLE_AGGREGATION_SOURCE_CONTROLS_FLAG,
+                                        ENABLE_AGGREGATION_SOURCE_CONTROLS_DEFAULT_FLAG_VALUE);
                 }
             } finally {
                 mLock.writeLock().unlock();
diff --git a/service/java/com/android/server/healthconnect/HealthConnectManagerService.java b/service/java/com/android/server/healthconnect/HealthConnectManagerService.java
index e2ee115..158b2bb 100644
--- a/service/java/com/android/server/healthconnect/HealthConnectManagerService.java
+++ b/service/java/com/android/server/healthconnect/HealthConnectManagerService.java
@@ -110,6 +110,7 @@
         mHealthConnectService =
                 new HealthConnectServiceImpl(
                         mTransactionManager,
+                        HealthConnectDeviceConfigManager.getInitialisedInstance(),
                         permissionHelper,
                         migrationCleaner,
                         firstGrantTimeManager,
diff --git a/service/java/com/android/server/healthconnect/HealthConnectServiceImpl.java b/service/java/com/android/server/healthconnect/HealthConnectServiceImpl.java
index 3d0cf44..5db5b39 100644
--- a/service/java/com/android/server/healthconnect/HealthConnectServiceImpl.java
+++ b/service/java/com/android/server/healthconnect/HealthConnectServiceImpl.java
@@ -23,6 +23,7 @@
 import static android.health.connect.HealthConnectException.ERROR_INTERNAL;
 import static android.health.connect.HealthConnectException.ERROR_SECURITY;
 import static android.health.connect.HealthPermissions.MANAGE_HEALTH_DATA_PERMISSION;
+import static android.health.connect.HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND;
 
 import static com.android.server.healthconnect.logging.HealthConnectServiceLogger.ApiMethods.DELETE_DATA;
 import static com.android.server.healthconnect.logging.HealthConnectServiceLogger.ApiMethods.GET_CHANGES;
@@ -93,7 +94,6 @@
 import android.health.connect.internal.datatypes.RecordInternal;
 import android.health.connect.internal.datatypes.utils.AggregationTypeIdMapper;
 import android.health.connect.internal.datatypes.utils.RecordMapper;
-import android.health.connect.internal.datatypes.utils.RecordTypePermissionCategoryMapper;
 import android.health.connect.migration.HealthConnectMigrationUiState;
 import android.health.connect.migration.MigrationEntityParcel;
 import android.health.connect.migration.MigrationException;
@@ -111,7 +111,6 @@
 import android.os.UserHandle;
 import android.permission.PermissionManager;
 import android.util.ArrayMap;
-import android.util.ArraySet;
 import android.util.Log;
 import android.util.Pair;
 import android.util.Slog;
@@ -158,7 +157,7 @@
 import java.util.Map.Entry;
 import java.util.Objects;
 import java.util.Set;
-import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.UUID;
 import java.util.stream.Collectors;
 
 /**
@@ -179,6 +178,9 @@
     private static final String TAG_READ = "HealthConnectRead";
     private static final String TAG_GRANT_PERMISSION = "HealthConnectGrantReadPermissions";
     private static final String TAG_READ_PERMISSION = "HealthConnectReadPermission";
+    private static final String TAG_READ_PERMISSION_FLAGS = "HealthConnectReadPermissionFlags";
+    private static final String TAG_MAKE_PERMISSIONS_REQUESTABLE =
+            "HealthConnectMakePermissionsRequestable";
     private static final String TAG_INSERT_SUBTASKS = "HealthConnectInsertSubtasks";
 
     private static final String TAG_DELETE_SUBTASKS = "HealthConnectDeleteSubtasks";
@@ -187,11 +189,15 @@
     private static final int TRACE_TAG_READ = TAG_READ.hashCode();
     private static final int TRACE_TAG_GRANT_PERMISSION = TAG_GRANT_PERMISSION.hashCode();
     private static final int TRACE_TAG_READ_PERMISSION = TAG_READ_PERMISSION.hashCode();
+    private static final int TRACE_TAG_READ_PERMISSION_FLAGS = TAG_READ_PERMISSION_FLAGS.hashCode();
+    private static final int TRACE_TAG_MAKE_PERMISSIONS_REQUESTABLE =
+            TAG_MAKE_PERMISSIONS_REQUESTABLE.hashCode();
     private static final int TRACE_TAG_INSERT_SUBTASKS = TAG_INSERT_SUBTASKS.hashCode();
     private static final int TRACE_TAG_DELETE_SUBTASKS = TAG_DELETE_SUBTASKS.hashCode();
     private static final int TRACE_TAG_READ_SUBTASKS = TAG_READ_SUBTASKS.hashCode();
 
     private final TransactionManager mTransactionManager;
+    private final HealthConnectDeviceConfigManager mDeviceConfigManager;
     private final HealthConnectPermissionHelper mPermissionHelper;
     private final FirstGrantTimeManager mFirstGrantTimeManager;
     private final Context mContext;
@@ -209,6 +215,7 @@
 
     HealthConnectServiceImpl(
             TransactionManager transactionManager,
+            HealthConnectDeviceConfigManager deviceConfigManager,
             HealthConnectPermissionHelper permissionHelper,
             MigrationCleaner migrationCleaner,
             FirstGrantTimeManager firstGrantTimeManager,
@@ -216,13 +223,15 @@
             MigrationUiStateManager migrationUiStateManager,
             Context context) {
         mTransactionManager = transactionManager;
+        mDeviceConfigManager = deviceConfigManager;
         mPermissionHelper = permissionHelper;
         mFirstGrantTimeManager = firstGrantTimeManager;
         mContext = context;
         mCurrentForegroundUser = context.getUser();
         mPermissionManager = mContext.getSystemService(PermissionManager.class);
         mMigrationStateManager = migrationStateManager;
-        mDataPermissionEnforcer = new DataPermissionEnforcer(mPermissionManager, mContext);
+        mDataPermissionEnforcer =
+                new DataPermissionEnforcer(mPermissionManager, mContext, deviceConfigManager);
         mAppOpsManagerLocal = LocalManagerRegistry.getManager(AppOpsManagerLocal.class);
         mBackupRestore =
                 new BackupRestore(mFirstGrantTimeManager, mMigrationStateManager, mContext);
@@ -234,14 +243,13 @@
     public void onUserSwitching(UserHandle currentForegroundUser) {
         mCurrentForegroundUser = currentForegroundUser;
         mBackupRestore.setupForUser(currentForegroundUser);
+        HealthDataCategoryPriorityHelper.getInstance().maybeAddInactiveAppsToPriorityList(mContext);
     }
 
     @Override
     public void grantHealthPermission(
             @NonNull String packageName, @NonNull String permissionName, @NonNull UserHandle user) {
-        Objects.requireNonNull(packageName);
-        Objects.requireNonNull(permissionName);
-        Objects.requireNonNull(user);
+        checkParamsNonNull(packageName, permissionName, user);
 
         throwIllegalStateExceptionIfDataSyncInProgress();
         Trace.traceBegin(TRACE_TAG_GRANT_PERMISSION, TAG_GRANT_PERMISSION);
@@ -255,9 +263,7 @@
             @NonNull String permissionName,
             @Nullable String reason,
             @NonNull UserHandle user) {
-        Objects.requireNonNull(packageName);
-        Objects.requireNonNull(permissionName);
-        Objects.requireNonNull(user);
+        checkParamsNonNull(packageName, permissionName, user);
 
         throwIllegalStateExceptionIfDataSyncInProgress();
         mPermissionHelper.revokeHealthPermission(packageName, permissionName, reason, user);
@@ -266,8 +272,7 @@
     @Override
     public void revokeAllHealthPermissions(
             @NonNull String packageName, @Nullable String reason, @NonNull UserHandle user) {
-        Objects.requireNonNull(packageName);
-        Objects.requireNonNull(user);
+        checkParamsNonNull(packageName, user);
 
         throwIllegalStateExceptionIfDataSyncInProgress();
         mPermissionHelper.revokeAllHealthPermissions(packageName, reason, user);
@@ -276,8 +281,7 @@
     @Override
     public List<String> getGrantedHealthPermissions(
             @NonNull String packageName, @NonNull UserHandle user) {
-        Objects.requireNonNull(packageName);
-        Objects.requireNonNull(user);
+        checkParamsNonNull(packageName, user);
 
         throwIllegalStateExceptionIfDataSyncInProgress();
         Trace.traceBegin(TRACE_TAG_READ_PERMISSION, TAG_READ_PERMISSION);
@@ -288,10 +292,37 @@
     }
 
     @Override
+    public Map<String, Integer> getHealthPermissionsFlags(
+            @NonNull String packageName, @NonNull UserHandle user, List<String> permissions) {
+        checkParamsNonNull(packageName, user);
+        throwIllegalStateExceptionIfDataSyncInProgress();
+
+        Trace.traceBegin(TRACE_TAG_READ_PERMISSION_FLAGS, TAG_READ_PERMISSION_FLAGS);
+
+        Map<String, Integer> response =
+                mPermissionHelper.getHealthPermissionsFlags(packageName, user, permissions);
+
+        Trace.traceEnd(TRACE_TAG_READ_PERMISSION_FLAGS);
+        return response;
+    }
+
+    @Override
+    public void makeHealthPermissionsRequestable(
+            @NonNull String packageName, @NonNull UserHandle user, List<String> permissions) {
+        checkParamsNonNull(packageName, user);
+        throwIllegalStateExceptionIfDataSyncInProgress();
+
+        Trace.traceBegin(TRACE_TAG_MAKE_PERMISSIONS_REQUESTABLE, TAG_MAKE_PERMISSIONS_REQUESTABLE);
+
+        mPermissionHelper.makeHealthPermissionsRequestable(packageName, user, permissions);
+
+        Trace.traceEnd(TRACE_TAG_MAKE_PERMISSIONS_REQUESTABLE);
+    }
+
+    @Override
     public long getHistoricalAccessStartDateInMilliseconds(
             @NonNull String packageName, @NonNull UserHandle userHandle) {
-        Objects.requireNonNull(packageName);
-        Objects.requireNonNull(userHandle);
+        checkParamsNonNull(packageName, userHandle);
 
         throwIllegalStateExceptionIfDataSyncInProgress();
         Instant date = mPermissionHelper.getHealthDataStartDateAccess(packageName, userHandle);
@@ -317,14 +348,12 @@
             @NonNull AttributionSource attributionSource,
             @NonNull RecordsParcel recordsParcel,
             @NonNull IInsertRecordsResponseCallback callback) {
-        Objects.requireNonNull(attributionSource);
-        Objects.requireNonNull(recordsParcel);
-        Objects.requireNonNull(callback);
+        checkParamsNonNull(attributionSource, recordsParcel, callback);
 
         final int uid = Binder.getCallingUid();
         final int pid = Binder.getCallingPid();
         final UserHandle userHandle = Binder.getCallingUserHandle();
-        final HealthConnectServiceLogger.Builder builder =
+        final HealthConnectServiceLogger.Builder logger =
                 new HealthConnectServiceLogger.Builder(false, INSERT_DATA)
                         .setPackageName(attributionSource.getPackageName());
 
@@ -343,17 +372,17 @@
                                 recordsParcel.getRecordsSize(),
                                 recordsParcel.getRecordsChunkSize());
                         final List<RecordInternal<?>> recordInternals = recordsParcel.getRecords();
-                        builder.setNumberOfRecords(recordInternals.size());
+                        logger.setNumberOfRecords(recordInternals.size());
                         throwExceptionIfDataSyncInProgress();
-                        mDataPermissionEnforcer.enforceRecordsWritePermissions(
-                                recordInternals, attributionSource);
                         boolean isInForeground = mAppOpsManagerLocal.isUidInForeground(uid);
                         tryAcquireApiCallQuota(
                                 uid,
                                 QuotaCategory.QUOTA_CATEGORY_WRITE,
                                 isInForeground,
-                                builder,
+                                logger,
                                 recordsParcel.getRecordsChunkSize());
+                        mDataPermissionEnforcer.enforceRecordsWritePermissions(
+                                recordInternals, attributionSource);
                         Trace.traceBegin(TRACE_TAG_INSERT, TAG_INSERT);
                         UpsertTransactionRequest insertRequest =
                                 new UpsertTransactionRequest(
@@ -365,26 +394,25 @@
                                                 .collectExtraWritePermissionStateMapping(
                                                         recordInternals, attributionSource));
                         List<String> uuids = mTransactionManager.insertAll(insertRequest);
-                        tryAndReturnResult(callback, uuids, builder);
+                        tryAndReturnResult(callback, uuids, logger);
 
                         HealthConnectThreadScheduler.scheduleInternalTask(
                                 () -> postInsertTasks(attributionSource, recordsParcel));
 
-                        finishDataDeliveryWriteRecords(recordInternals, attributionSource);
                         logRecordTypeSpecificUpsertMetrics(
                                 recordInternals, attributionSource.getPackageName());
-                        builder.setDataTypesFromRecordInternals(recordInternals);
+                        logger.setDataTypesFromRecordInternals(recordInternals);
                     } catch (SQLiteException sqLiteException) {
-                        builder.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
+                        logger.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
                         Slog.e(TAG, "SQLiteException: ", sqLiteException);
                         tryAndThrowException(
                                 callback, sqLiteException, HealthConnectException.ERROR_IO);
                     } catch (SecurityException securityException) {
-                        builder.setHealthDataServiceApiStatusError(ERROR_SECURITY);
+                        logger.setHealthDataServiceApiStatusError(ERROR_SECURITY);
                         Slog.e(TAG, "SecurityException: ", securityException);
                         tryAndThrowException(callback, securityException, ERROR_SECURITY);
                     } catch (HealthConnectException healthConnectException) {
-                        builder.setHealthDataServiceApiStatusError(
+                        logger.setHealthDataServiceApiStatusError(
                                 healthConnectException.getErrorCode());
                         Slog.e(TAG, "HealthConnectException: ", healthConnectException);
                         tryAndThrowException(
@@ -392,12 +420,12 @@
                                 healthConnectException,
                                 healthConnectException.getErrorCode());
                     } catch (Exception e) {
-                        builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
+                        logger.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
                         Slog.e(TAG, "Exception: ", e);
                         tryAndThrowException(callback, e, ERROR_INTERNAL);
                     } finally {
                         Trace.traceEnd(TRACE_TAG_INSERT);
-                        builder.build().log();
+                        logger.build().log();
                     }
                 },
                 uid,
@@ -432,15 +460,13 @@
             @NonNull AttributionSource attributionSource,
             @NonNull AggregateDataRequestParcel request,
             @NonNull IAggregateRecordsResponseCallback callback) {
-        Objects.requireNonNull(attributionSource);
-        Objects.requireNonNull(request);
-        Objects.requireNonNull(callback);
+        checkParamsNonNull(attributionSource, request, callback);
 
         final int uid = Binder.getCallingUid();
         final int pid = Binder.getCallingPid();
         final UserHandle userHandle = Binder.getCallingUserHandle();
         final boolean holdsDataManagementPermission = hasDataManagementPermission(uid, pid);
-        final HealthConnectServiceLogger.Builder builder =
+        final HealthConnectServiceLogger.Builder logger =
                 new HealthConnectServiceLogger.Builder(
                                 holdsDataManagementPermission, READ_AGGREGATED_DATA)
                         .setPackageName(attributionSource.getPackageName());
@@ -451,7 +477,7 @@
                     try {
                         enforceIsForegroundUser(userHandle);
                         verifyPackageNameFromUid(uid, attributionSource);
-                        builder.setNumberOfRecords(request.getAggregateIds().length);
+                        logger.setNumberOfRecords(request.getAggregateIds().length);
                         throwExceptionIfDataSyncInProgress();
                         List<Integer> recordTypesToTest = new ArrayList<>();
                         for (int aggregateId : request.getAggregateIds()) {
@@ -464,18 +490,22 @@
                         long startDateAccess;
                         if (!holdsDataManagementPermission) {
                             boolean isInForeground = mAppOpsManagerLocal.isUidInForeground(uid);
+                            logger.setCallerForegroundState(isInForeground);
+
                             if (!isInForeground) {
-                                throwSecurityException(
-                                        attributionSource.getPackageName()
+                                mDataPermissionEnforcer.enforceBackgroundReadRestrictions(
+                                        uid,
+                                        pid,
+                                        /* errorMessage= */ attributionSource.getPackageName()
                                                 + "must be in foreground to call aggregate method");
                             }
-                            mDataPermissionEnforcer.enforceRecordIdsReadPermissions(
-                                    recordTypesToTest, attributionSource);
                             tryAcquireApiCallQuota(
                                     uid,
                                     RateLimiter.QuotaCategory.QUOTA_CATEGORY_READ,
                                     isInForeground,
-                                    builder);
+                                    logger);
+                            mDataPermissionEnforcer.enforceRecordIdsReadPermissions(
+                                    recordTypesToTest, attributionSource);
                             startDateAccess =
                                     mPermissionHelper
                                             .getHealthDataStartDateAccessOrThrow(
@@ -490,20 +520,19 @@
                                                 request,
                                                 startDateAccess)
                                         .getAggregateDataResponseParcel());
-                        finishDataDeliveryRead(recordTypesToTest, attributionSource);
-                        builder.setDataTypesFromRecordTypes(recordTypesToTest)
+                        logger.setDataTypesFromRecordTypes(recordTypesToTest)
                                 .setHealthDataServiceApiStatusSuccess();
                     } catch (SQLiteException sqLiteException) {
-                        builder.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
+                        logger.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
                         Slog.e(TAG, "SQLiteException: ", sqLiteException);
                         tryAndThrowException(
                                 callback, sqLiteException, HealthConnectException.ERROR_IO);
                     } catch (SecurityException securityException) {
-                        builder.setHealthDataServiceApiStatusError(ERROR_SECURITY);
+                        logger.setHealthDataServiceApiStatusError(ERROR_SECURITY);
                         Slog.e(TAG, "SecurityException: ", securityException);
                         tryAndThrowException(callback, securityException, ERROR_SECURITY);
                     } catch (HealthConnectException healthConnectException) {
-                        builder.setHealthDataServiceApiStatusError(
+                        logger.setHealthDataServiceApiStatusError(
                                 healthConnectException.getErrorCode());
                         Slog.e(TAG, "HealthConnectException: ", healthConnectException);
                         tryAndThrowException(
@@ -511,11 +540,11 @@
                                 healthConnectException,
                                 healthConnectException.getErrorCode());
                     } catch (Exception e) {
-                        builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
+                        logger.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
                         Slog.e(TAG, "Exception: ", e);
                         tryAndThrowException(callback, e, ERROR_INTERNAL);
                     } finally {
-                        builder.build().log();
+                        logger.build().log();
                     }
                 },
                 uid,
@@ -537,17 +566,17 @@
             @NonNull AttributionSource attributionSource,
             @NonNull ReadRecordsRequestParcel request,
             @NonNull IReadRecordsResponseCallback callback) {
-        Objects.requireNonNull(attributionSource);
-        Objects.requireNonNull(request);
-        Objects.requireNonNull(callback);
+        checkParamsNonNull(attributionSource, request, callback);
 
         final int uid = Binder.getCallingUid();
         final int pid = Binder.getCallingPid();
         final UserHandle userHandle = Binder.getCallingUserHandle();
         final boolean holdsDataManagementPermission = hasDataManagementPermission(uid, pid);
-        final HealthConnectServiceLogger.Builder builder =
+        final String callingPackageName =
+                Objects.requireNonNull(attributionSource.getPackageName());
+        final HealthConnectServiceLogger.Builder logger =
                 new HealthConnectServiceLogger.Builder(holdsDataManagementPermission, READ_DATA)
-                        .setPackageName(attributionSource.getPackageName());
+                        .setPackageName(callingPackageName);
 
         HealthConnectThreadScheduler.schedule(
                 mContext,
@@ -556,35 +585,44 @@
                         enforceIsForegroundUser(userHandle);
                         verifyPackageNameFromUid(uid, attributionSource);
                         throwExceptionIfDataSyncInProgress();
-                        AtomicBoolean enforceSelfRead = new AtomicBoolean();
+
+                        boolean enforceSelfRead = false;
+
                         if (!holdsDataManagementPermission) {
-                            boolean isInForeground = mAppOpsManagerLocal.isUidInForeground(uid);
-                            // If requesting app has only write permission allowed but no read
-                            // permission for the record type or if app is not in foreground then
-                            // allow to read its own records.
-                            enforceSelfRead.set(
-                                    mDataPermissionEnforcer.enforceReadAccessAndGetEnforceSelfRead(
-                                                    request.getRecordType(), attributionSource)
-                                            || !isInForeground);
+                            final boolean isInForeground =
+                                    mAppOpsManagerLocal.isUidInForeground(uid);
+
+                            logger.setCallerForegroundState(isInForeground);
+
+                            tryAcquireApiCallQuota(
+                                    uid, QuotaCategory.QUOTA_CATEGORY_READ, isInForeground, logger);
+
+                            if (mDataPermissionEnforcer.enforceReadAccessAndGetEnforceSelfRead(
+                                    request.getRecordType(), attributionSource)) {
+                                // If read permission is missing but write permission is granted,
+                                // then enforce self read
+                                enforceSelfRead = true;
+                            } else if (!isInForeground) {
+                                // If Background Read feature is disabled
+                                // or READ_HEALTH_DATA_IN_BACKGROUND permission is not granted,
+                                // then enforce self read
+                                enforceSelfRead = isOnlySelfReadInBackgroundAllowed(uid, pid);
+                            }
+
                             if (Constants.DEBUG) {
                                 Slog.d(
                                         TAG,
                                         "Enforce self read for package "
-                                                + attributionSource.getPackageName()
+                                                + callingPackageName
                                                 + ":"
-                                                + enforceSelfRead.get());
+                                                + enforceSelfRead);
                             }
-                            tryAcquireApiCallQuota(
-                                    uid,
-                                    QuotaCategory.QUOTA_CATEGORY_READ,
-                                    isInForeground,
-                                    builder);
                         }
                         final Map<String, Boolean> extraReadPermsToGrantState =
                                 Collections.unmodifiableMap(
                                         mDataPermissionEnforcer
                                                 .collectExtraReadPermissionToStateMapping(
-                                                        request.getRecordType(),
+                                                        Set.of(request.getRecordType()),
                                                         attributionSource));
 
                         Trace.traceBegin(TRACE_TAG_READ, TAG_READ);
@@ -593,7 +631,7 @@
                             if (!holdsDataManagementPermission) {
                                 Instant startDateAccessInstant =
                                         mPermissionHelper.getHealthDataStartDateAccessOrThrow(
-                                                attributionSource.getPackageName(), userHandle);
+                                                callingPackageName, userHandle);
 
                                 // Always set the startDateAccess for local time filter, as for
                                 // local date time we use it in conjunction with the time filter
@@ -605,57 +643,62 @@
                                             startDateAccessInstant.toEpochMilli();
                                 }
                             }
-                            Pair<List<RecordInternal<?>>, Long> readRecordsResponse =
-                                    mTransactionManager.readRecordsAndGetNextToken(
-                                            new ReadTransactionRequest(
-                                                    attributionSource.getPackageName(),
-                                                    request,
-                                                    startDateAccessEpochMilli,
-                                                    enforceSelfRead.get(),
-                                                    extraReadPermsToGrantState));
-                            builder.setNumberOfRecords(readRecordsResponse.first.size());
-                            long pageToken =
-                                    request.getRecordIdFiltersParcel() == null
-                                            ? readRecordsResponse.second
-                                            : DEFAULT_LONG;
-                            if (pageToken != DEFAULT_LONG) {
-                                // pagetoken is used here to store sorting order of the result.
-                                // An even pagetoken indicate ascending and Odd page token indicate
-                                // descending sort order. This detail from page token will be used
-                                // in next read request to have same sort order.
-                                pageToken =
-                                        request.isAscending() ? pageToken * 2 : pageToken * 2 + 1;
+
+                            ReadTransactionRequest readTransactionRequest =
+                                    new ReadTransactionRequest(
+                                            callingPackageName,
+                                            request,
+                                            startDateAccessEpochMilli,
+                                            enforceSelfRead,
+                                            extraReadPermsToGrantState);
+                            // throw an exception if read requested is not for a single record type
+                            // i.e. size of read table request is not equal to 1.
+                            if (readTransactionRequest.getReadRequests().size() != 1) {
+                                throw new IllegalArgumentException(
+                                        "Read requested is not for a single record type");
                             }
 
+                            List<RecordInternal<?>> records;
+                            long pageToken;
+                            if (request.getRecordIdFiltersParcel() != null) {
+                                records =
+                                        mTransactionManager.readRecordsByIds(
+                                                readTransactionRequest);
+                                pageToken = DEFAULT_LONG;
+                            } else {
+                                Pair<List<RecordInternal<?>>, Long> readRecordsResponse =
+                                        mTransactionManager.readRecordsAndPageToken(
+                                                readTransactionRequest);
+                                records = readRecordsResponse.first;
+                                pageToken = readRecordsResponse.second;
+                            }
+                            logger.setNumberOfRecords(records.size());
+
                             if (Constants.DEBUG) {
                                 Slog.d(TAG, "pageToken: " + pageToken);
                             }
 
-                            final String packageName = attributionSource.getPackageName();
                             final List<Integer> recordTypes =
                                     Collections.singletonList(request.getRecordType());
                             // Calls from controller APK should not be recorded in access logs
                             // If an app is reading only its own data then it is not recorded in
                             // access logs.
                             boolean requiresLogging =
-                                    !holdsDataManagementPermission && !enforceSelfRead.get();
+                                    !holdsDataManagementPermission && !enforceSelfRead;
                             if (requiresLogging) {
                                 Trace.traceBegin(
                                         TRACE_TAG_READ_SUBTASKS, TAG_READ.concat("AddAccessLog"));
                                 AccessLogsHelper.getInstance()
-                                        .addAccessLog(packageName, recordTypes, READ);
+                                        .addAccessLog(callingPackageName, recordTypes, READ);
                                 Trace.traceEnd(TRACE_TAG_READ_SUBTASKS);
                             }
                             callback.onResult(
                                     new ReadRecordsResponseParcel(
-                                            new RecordsParcel(readRecordsResponse.first),
-                                            pageToken));
-                            finishDataDeliveryRead(request.getRecordType(), attributionSource);
+                                            new RecordsParcel(records), pageToken));
                             if (requiresLogging) {
-                                logRecordTypeSpecificReadMetrics(
-                                        readRecordsResponse.first, packageName);
+                                logRecordTypeSpecificReadMetrics(records, callingPackageName);
                             }
-                            builder.setDataTypesFromRecordInternals(readRecordsResponse.first)
+                            logger.setDataTypesFromRecordInternals(records)
                                     .setHealthDataServiceApiStatusSuccess();
                         } catch (TypeNotPresentException exception) {
                             // All the requested package names are not present, so simply
@@ -663,37 +706,34 @@
                             if (ReadTransactionRequest.TYPE_NOT_PRESENT_PACKAGE_NAME.equals(
                                     exception.typeName())) {
                                 if (Constants.DEBUG) {
-                                    Slog.d(
-                                            TAG,
-                                            "No app info recorded for "
-                                                    + attributionSource.getPackageName());
+                                    Slog.d(TAG, "No app info recorded for " + callingPackageName);
                                 }
                                 callback.onResult(
                                         new ReadRecordsResponseParcel(
                                                 new RecordsParcel(new ArrayList<>()),
                                                 DEFAULT_LONG));
-                                builder.setHealthDataServiceApiStatusSuccess();
+                                logger.setHealthDataServiceApiStatusSuccess();
                             } else {
-                                builder.setHealthDataServiceApiStatusError(
+                                logger.setHealthDataServiceApiStatusError(
                                         HealthConnectException.ERROR_UNKNOWN);
                                 throw exception;
                             }
                         }
                     } catch (SQLiteException sqLiteException) {
-                        builder.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
+                        logger.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
                         Slog.e(TAG, "SQLiteException: ", sqLiteException);
                         tryAndThrowException(
                                 callback, sqLiteException, HealthConnectException.ERROR_IO);
                     } catch (SecurityException securityException) {
-                        builder.setHealthDataServiceApiStatusError(ERROR_SECURITY);
+                        logger.setHealthDataServiceApiStatusError(ERROR_SECURITY);
                         Slog.e(TAG, "SecurityException: ", securityException);
                         tryAndThrowException(callback, securityException, ERROR_SECURITY);
                     } catch (IllegalStateException illegalStateException) {
-                        builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
+                        logger.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
                         Slog.e(TAG, "IllegalStateException: ", illegalStateException);
                         tryAndThrowException(callback, illegalStateException, ERROR_INTERNAL);
                     } catch (HealthConnectException healthConnectException) {
-                        builder.setHealthDataServiceApiStatusError(
+                        logger.setHealthDataServiceApiStatusError(
                                 healthConnectException.getErrorCode());
                         Slog.e(TAG, "HealthConnectException: ", healthConnectException);
                         tryAndThrowException(
@@ -701,12 +741,12 @@
                                 healthConnectException,
                                 healthConnectException.getErrorCode());
                     } catch (Exception e) {
-                        builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
+                        logger.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
                         Slog.e(TAG, "Exception: ", e);
                         tryAndThrowException(callback, e, ERROR_INTERNAL);
                     } finally {
                         Trace.traceEnd(TRACE_TAG_READ);
-                        builder.build().log();
+                        logger.build().log();
                     }
                 },
                 uid,
@@ -726,14 +766,12 @@
             @NonNull AttributionSource attributionSource,
             @NonNull RecordsParcel recordsParcel,
             @NonNull IEmptyResponseCallback callback) {
-        Objects.requireNonNull(attributionSource);
-        Objects.requireNonNull(recordsParcel);
-        Objects.requireNonNull(callback);
+        checkParamsNonNull(attributionSource, recordsParcel, callback);
 
         final int uid = Binder.getCallingUid();
         final int pid = Binder.getCallingPid();
         final UserHandle userHandle = Binder.getCallingUserHandle();
-        final HealthConnectServiceLogger.Builder builder =
+        final HealthConnectServiceLogger.Builder logger =
                 new HealthConnectServiceLogger.Builder(false, UPDATE_DATA)
                         .setPackageName(attributionSource.getPackageName());
         HealthConnectThreadScheduler.schedule(
@@ -751,17 +789,17 @@
                                 recordsParcel.getRecordsSize(),
                                 recordsParcel.getRecordsChunkSize());
                         final List<RecordInternal<?>> recordInternals = recordsParcel.getRecords();
-                        builder.setNumberOfRecords(recordInternals.size());
+                        logger.setNumberOfRecords(recordInternals.size());
                         throwExceptionIfDataSyncInProgress();
-                        mDataPermissionEnforcer.enforceRecordsWritePermissions(
-                                recordInternals, attributionSource);
                         boolean isInForeground = mAppOpsManagerLocal.isUidInForeground(uid);
                         tryAcquireApiCallQuota(
                                 uid,
                                 QuotaCategory.QUOTA_CATEGORY_WRITE,
                                 isInForeground,
-                                builder,
+                                logger,
                                 recordsParcel.getRecordsChunkSize());
+                        mDataPermissionEnforcer.enforceRecordsWritePermissions(
+                                recordInternals, attributionSource);
                         UpsertTransactionRequest request =
                                 new UpsertTransactionRequest(
                                         attributionSource.getPackageName(),
@@ -772,11 +810,10 @@
                                                 .collectExtraWritePermissionStateMapping(
                                                         recordInternals, attributionSource));
                         mTransactionManager.updateAll(request);
-                        tryAndReturnResult(callback, builder);
-                        finishDataDeliveryWriteRecords(recordInternals, attributionSource);
+                        tryAndReturnResult(callback, logger);
                         logRecordTypeSpecificUpsertMetrics(
                                 recordInternals, attributionSource.getPackageName());
-                        builder.setDataTypesFromRecordInternals(recordInternals);
+                        logger.setDataTypesFromRecordInternals(recordInternals);
                         // Update activity dates table
                         HealthConnectThreadScheduler.scheduleInternalTask(
                                 () ->
@@ -786,15 +823,15 @@
                                                                 .map(RecordInternal::getRecordType)
                                                                 .toList()));
                     } catch (SecurityException securityException) {
-                        builder.setHealthDataServiceApiStatusError(ERROR_SECURITY);
+                        logger.setHealthDataServiceApiStatusError(ERROR_SECURITY);
                         tryAndThrowException(callback, securityException, ERROR_SECURITY);
                     } catch (SQLiteException sqLiteException) {
-                        builder.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
+                        logger.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
                         Slog.e(TAG, "SqlException: ", sqLiteException);
                         tryAndThrowException(
                                 callback, sqLiteException, HealthConnectException.ERROR_IO);
                     } catch (IllegalArgumentException illegalArgumentException) {
-                        builder.setHealthDataServiceApiStatusError(
+                        logger.setHealthDataServiceApiStatusError(
                                 HealthConnectException.ERROR_INVALID_ARGUMENT);
 
                         Slog.e(TAG, "IllegalArgumentException: ", illegalArgumentException);
@@ -803,7 +840,7 @@
                                 illegalArgumentException,
                                 HealthConnectException.ERROR_INVALID_ARGUMENT);
                     } catch (HealthConnectException healthConnectException) {
-                        builder.setHealthDataServiceApiStatusError(
+                        logger.setHealthDataServiceApiStatusError(
                                 healthConnectException.getErrorCode());
                         Slog.e(TAG, "HealthConnectException: ", healthConnectException);
                         tryAndThrowException(
@@ -811,12 +848,12 @@
                                 healthConnectException,
                                 healthConnectException.getErrorCode());
                     } catch (Exception e) {
-                        builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
+                        logger.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
 
                         Slog.e(TAG, "Exception: ", e);
                         tryAndThrowException(callback, e, ERROR_INTERNAL);
                     } finally {
-                        builder.build().log();
+                        logger.build().log();
                     }
                 },
                 uid,
@@ -831,13 +868,11 @@
             @NonNull AttributionSource attributionSource,
             @NonNull ChangeLogTokenRequest request,
             @NonNull IGetChangeLogTokenCallback callback) {
-        Objects.requireNonNull(attributionSource);
-        Objects.requireNonNull(request);
-        Objects.requireNonNull(callback);
+        checkParamsNonNull(attributionSource, request, callback);
 
         final int uid = Binder.getCallingUid();
         final UserHandle userHandle = Binder.getCallingUserHandle();
-        final HealthConnectServiceLogger.Builder builder =
+        final HealthConnectServiceLogger.Builder logger =
                 new HealthConnectServiceLogger.Builder(false, GET_CHANGES_TOKEN)
                         .setPackageName(attributionSource.getPackageName());
         HealthConnectThreadScheduler.schedule(
@@ -850,7 +885,7 @@
                                 uid,
                                 QuotaCategory.QUOTA_CATEGORY_READ,
                                 mAppOpsManagerLocal.isUidInForeground(uid),
-                                builder);
+                                logger);
                         throwExceptionIfDataSyncInProgress();
                         mDataPermissionEnforcer.enforceRecordIdsReadPermissions(
                                 request.getRecordTypesList(), attributionSource);
@@ -860,18 +895,18 @@
                                                 .getToken(
                                                         attributionSource.getPackageName(),
                                                         request)));
-                        builder.setHealthDataServiceApiStatusSuccess();
+                        logger.setHealthDataServiceApiStatusSuccess();
                     } catch (SQLiteException sqLiteException) {
-                        builder.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
+                        logger.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
                         Slog.e(TAG, "SQLiteException: ", sqLiteException);
                         tryAndThrowException(
                                 callback, sqLiteException, HealthConnectException.ERROR_IO);
                     } catch (SecurityException securityException) {
-                        builder.setHealthDataServiceApiStatusError(ERROR_SECURITY);
+                        logger.setHealthDataServiceApiStatusError(ERROR_SECURITY);
                         Slog.e(TAG, "SecurityException: ", securityException);
                         tryAndThrowException(callback, securityException, ERROR_SECURITY);
                     } catch (HealthConnectException healthConnectException) {
-                        builder.setHealthDataServiceApiStatusError(
+                        logger.setHealthDataServiceApiStatusError(
                                 healthConnectException.getErrorCode());
                         Slog.e(TAG, "HealthConnectException: ", healthConnectException);
                         tryAndThrowException(
@@ -879,10 +914,10 @@
                                 healthConnectException,
                                 healthConnectException.getErrorCode());
                     } catch (Exception e) {
-                        builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
+                        logger.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
                         tryAndThrowException(callback, e, ERROR_INTERNAL);
                     } finally {
-                        builder.build().log();
+                        logger.build().log();
                     }
                 },
                 uid,
@@ -896,17 +931,17 @@
     @Override
     public void getChangeLogs(
             @NonNull AttributionSource attributionSource,
-            @NonNull ChangeLogsRequest token,
+            @NonNull ChangeLogsRequest request,
             @NonNull IChangeLogsResponseCallback callback) {
-        Objects.requireNonNull(attributionSource);
-        Objects.requireNonNull(token);
-        Objects.requireNonNull(callback);
+        checkParamsNonNull(attributionSource, request, callback);
 
         final int uid = Binder.getCallingUid();
+        final int pid = Binder.getCallingPid();
         final UserHandle userHandle = Binder.getCallingUserHandle();
-        final HealthConnectServiceLogger.Builder builder =
+        final String callerPackageName = Objects.requireNonNull(attributionSource.getPackageName());
+        final HealthConnectServiceLogger.Builder logger =
                 new HealthConnectServiceLogger.Builder(false, GET_CHANGES)
-                        .setPackageName(attributionSource.getPackageName());
+                        .setPackageName(callerPackageName);
 
         HealthConnectThreadScheduler.schedule(
                 mContext,
@@ -915,33 +950,49 @@
                         enforceIsForegroundUser(userHandle);
                         verifyPackageNameFromUid(uid, attributionSource);
                         throwExceptionIfDataSyncInProgress();
+
+                        boolean isInForeground = mAppOpsManagerLocal.isUidInForeground(uid);
+                        logger.setCallerForegroundState(isInForeground);
+
+                        if (!isInForeground) {
+                            mDataPermissionEnforcer.enforceBackgroundReadRestrictions(
+                                    uid,
+                                    pid,
+                                    /* errorMessage= */ callerPackageName
+                                            + "must be in foreground to call getChangeLogs method");
+                        }
+
                         ChangeLogsRequestHelper.TokenRequest changeLogsTokenRequest =
                                 ChangeLogsRequestHelper.getRequest(
-                                        attributionSource.getPackageName(), token.getToken());
+                                        callerPackageName, request.getToken());
+                        tryAcquireApiCallQuota(
+                                uid, QuotaCategory.QUOTA_CATEGORY_READ, isInForeground, logger);
                         mDataPermissionEnforcer.enforceRecordIdsReadPermissions(
                                 changeLogsTokenRequest.getRecordTypes(), attributionSource);
-                        boolean isInForeground = mAppOpsManagerLocal.isUidInForeground(uid);
-                        if (!isInForeground) {
-                            throwSecurityException(
-                                    attributionSource.getPackageName()
-                                            + " must be in foreground to read the change logs");
-                        }
-                        tryAcquireApiCallQuota(
-                                uid, QuotaCategory.QUOTA_CATEGORY_READ, isInForeground, builder);
                         Instant startDateAccessInstant =
                                 mPermissionHelper.getHealthDataStartDateAccessOrThrow(
-                                        attributionSource.getPackageName(), userHandle);
+                                        callerPackageName, userHandle);
                         long startDateAccessEpochMilli = startDateAccessInstant.toEpochMilli();
                         final ChangeLogsHelper.ChangeLogsResponse changeLogsResponse =
                                 ChangeLogsHelper.getInstance()
-                                        .getChangeLogs(changeLogsTokenRequest, token);
+                                        .getChangeLogs(changeLogsTokenRequest, request);
+
+                        Map<Integer, List<UUID>> recordTypeToInsertedUuids =
+                                ChangeLogsHelper.getRecordTypeToInsertedUuids(
+                                        changeLogsResponse.getChangeLogsMap());
+
+                        Map<String, Boolean> extraReadPermsToGrantState =
+                                mDataPermissionEnforcer.collectExtraReadPermissionToStateMapping(
+                                        recordTypeToInsertedUuids.keySet(), attributionSource);
 
                         List<RecordInternal<?>> recordInternals =
-                                mTransactionManager.readRecords(
+                                mTransactionManager.readRecordsByIds(
                                         new ReadTransactionRequest(
-                                                ChangeLogsHelper.getRecordTypeToInsertedUuids(
-                                                        changeLogsResponse.getChangeLogsMap()),
-                                                startDateAccessEpochMilli));
+                                                callerPackageName,
+                                                recordTypeToInsertedUuids,
+                                                startDateAccessEpochMilli,
+                                                extraReadPermsToGrantState));
+
                         List<DeletedLog> deletedLogs =
                                 ChangeLogsHelper.getDeletedLogs(
                                         changeLogsResponse.getChangeLogsMap());
@@ -952,13 +1003,11 @@
                                         deletedLogs,
                                         changeLogsResponse.getNextPageToken(),
                                         changeLogsResponse.hasMorePages()));
-                        finishDataDeliveryRead(
-                                changeLogsTokenRequest.getRecordTypes(), attributionSource);
-                        builder.setHealthDataServiceApiStatusSuccess()
+                        logger.setHealthDataServiceApiStatusSuccess()
                                 .setNumberOfRecords(recordInternals.size() + deletedLogs.size())
                                 .setDataTypesFromRecordInternals(recordInternals);
                     } catch (IllegalArgumentException illegalArgumentException) {
-                        builder.setHealthDataServiceApiStatusError(
+                        logger.setHealthDataServiceApiStatusError(
                                 HealthConnectException.ERROR_INVALID_ARGUMENT);
                         Slog.e(TAG, "IllegalArgumentException: ", illegalArgumentException);
                         tryAndThrowException(
@@ -966,20 +1015,20 @@
                                 illegalArgumentException,
                                 HealthConnectException.ERROR_INVALID_ARGUMENT);
                     } catch (SQLiteException sqLiteException) {
-                        builder.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
+                        logger.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
                         Slog.e(TAG, "SQLiteException: ", sqLiteException);
                         tryAndThrowException(
                                 callback, sqLiteException, HealthConnectException.ERROR_IO);
                     } catch (SecurityException securityException) {
-                        builder.setHealthDataServiceApiStatusError(ERROR_SECURITY);
+                        logger.setHealthDataServiceApiStatusError(ERROR_SECURITY);
                         Slog.e(TAG, "SecurityException: ", securityException);
                         tryAndThrowException(callback, securityException, ERROR_SECURITY);
                     } catch (IllegalStateException illegalStateException) {
-                        builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
+                        logger.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
                         Slog.e(TAG, "IllegalStateException: ", illegalStateException);
                         tryAndThrowException(callback, illegalStateException, ERROR_INTERNAL);
                     } catch (HealthConnectException healthConnectException) {
-                        builder.setHealthDataServiceApiStatusError(
+                        logger.setHealthDataServiceApiStatusError(
                                 healthConnectException.getErrorCode());
                         Slog.e(TAG, "HealthConnectException: ", healthConnectException);
                         tryAndThrowException(
@@ -987,11 +1036,11 @@
                                 healthConnectException,
                                 healthConnectException.getErrorCode());
                     } catch (Exception exception) {
-                        builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
+                        logger.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
                         Slog.e(TAG, "Exception: ", exception);
                         tryAndThrowException(callback, exception, ERROR_INTERNAL);
                     } finally {
-                        builder.build().log();
+                        logger.build().log();
                     }
                 },
                 uid,
@@ -1011,15 +1060,13 @@
             @NonNull AttributionSource attributionSource,
             @NonNull DeleteUsingFiltersRequestParcel request,
             @NonNull IEmptyResponseCallback callback) {
-        Objects.requireNonNull(attributionSource);
-        Objects.requireNonNull(request);
-        Objects.requireNonNull(callback);
+        checkParamsNonNull(attributionSource, request, callback);
 
         final int uid = Binder.getCallingUid();
         final int pid = Binder.getCallingPid();
         final UserHandle userHandle = Binder.getCallingUserHandle();
         final boolean holdsDataManagementPermission = hasDataManagementPermission(uid, pid);
-        final HealthConnectServiceLogger.Builder builder =
+        final HealthConnectServiceLogger.Builder logger =
                 new HealthConnectServiceLogger.Builder(holdsDataManagementPermission, DELETE_DATA)
                         .setPackageName(attributionSource.getPackageName());
 
@@ -1043,29 +1090,29 @@
                                 Collections.singletonList(attributionSource.getPackageName()));
 
                         if (!holdsDataManagementPermission) {
-                            mDataPermissionEnforcer.enforceRecordIdsWritePermissions(
-                                    recordTypeIdsToDelete, attributionSource);
                             tryAcquireApiCallQuota(
                                     uid,
                                     QuotaCategory.QUOTA_CATEGORY_WRITE,
                                     mAppOpsManagerLocal.isUidInForeground(uid),
-                                    builder);
+                                    logger);
+                            mDataPermissionEnforcer.enforceRecordIdsWritePermissions(
+                                    recordTypeIdsToDelete, attributionSource);
                         }
 
                         deleteUsingFiltersInternal(
                                 attributionSource,
                                 request,
                                 callback,
-                                builder,
+                                logger,
                                 recordTypeIdsToDelete,
                                 uid,
                                 pid);
                     } catch (SQLiteException sqLiteException) {
-                        builder.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
+                        logger.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
                         tryAndThrowException(
                                 callback, sqLiteException, HealthConnectException.ERROR_IO);
                     } catch (IllegalArgumentException illegalArgumentException) {
-                        builder.setHealthDataServiceApiStatusError(
+                        logger.setHealthDataServiceApiStatusError(
                                 HealthConnectException.ERROR_INVALID_ARGUMENT);
                         Slog.e(TAG, "IllegalArgumentException: ", illegalArgumentException);
                         tryAndThrowException(
@@ -1073,11 +1120,11 @@
                                 illegalArgumentException,
                                 HealthConnectException.ERROR_INVALID_ARGUMENT);
                     } catch (SecurityException securityException) {
-                        builder.setHealthDataServiceApiStatusError(ERROR_SECURITY);
+                        logger.setHealthDataServiceApiStatusError(ERROR_SECURITY);
                         Slog.e(TAG, "SecurityException: ", securityException);
                         tryAndThrowException(callback, securityException, ERROR_SECURITY);
                     } catch (HealthConnectException healthConnectException) {
-                        builder.setHealthDataServiceApiStatusError(
+                        logger.setHealthDataServiceApiStatusError(
                                 healthConnectException.getErrorCode());
                         Slog.e(TAG, "HealthConnectException: ", healthConnectException);
                         tryAndThrowException(
@@ -1085,11 +1132,11 @@
                                 healthConnectException,
                                 healthConnectException.getErrorCode());
                     } catch (Exception exception) {
-                        builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
+                        logger.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
                         Slog.e(TAG, "Exception: ", exception);
                         tryAndThrowException(callback, exception, ERROR_INTERNAL);
                     } finally {
-                        builder.build().log();
+                        logger.build().log();
                     }
                 },
                 uid,
@@ -1108,15 +1155,13 @@
             @NonNull AttributionSource attributionSource,
             @NonNull DeleteUsingFiltersRequestParcel request,
             @NonNull IEmptyResponseCallback callback) {
-        Objects.requireNonNull(attributionSource);
-        Objects.requireNonNull(request);
-        Objects.requireNonNull(callback);
+        checkParamsNonNull(attributionSource, request, callback);
 
         final int uid = Binder.getCallingUid();
         final int pid = Binder.getCallingPid();
         final UserHandle userHandle = Binder.getCallingUserHandle();
         final boolean holdsDataManagementPermission = hasDataManagementPermission(uid, pid);
-        final HealthConnectServiceLogger.Builder builder =
+        final HealthConnectServiceLogger.Builder logger =
                 new HealthConnectServiceLogger.Builder(holdsDataManagementPermission, DELETE_DATA)
                         .setPackageName(attributionSource.getPackageName());
 
@@ -1140,16 +1185,16 @@
                                 attributionSource,
                                 request,
                                 callback,
-                                builder,
+                                logger,
                                 recordTypeIdsToDelete,
                                 uid,
                                 pid);
                     } catch (SQLiteException sqLiteException) {
-                        builder.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
+                        logger.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
                         tryAndThrowException(
                                 callback, sqLiteException, HealthConnectException.ERROR_IO);
                     } catch (IllegalArgumentException illegalArgumentException) {
-                        builder.setHealthDataServiceApiStatusError(
+                        logger.setHealthDataServiceApiStatusError(
                                 HealthConnectException.ERROR_INVALID_ARGUMENT);
                         Slog.e(TAG, "IllegalArgumentException: ", illegalArgumentException);
                         tryAndThrowException(
@@ -1157,11 +1202,11 @@
                                 illegalArgumentException,
                                 HealthConnectException.ERROR_INVALID_ARGUMENT);
                     } catch (SecurityException securityException) {
-                        builder.setHealthDataServiceApiStatusError(ERROR_SECURITY);
+                        logger.setHealthDataServiceApiStatusError(ERROR_SECURITY);
                         Slog.e(TAG, "SecurityException: ", securityException);
                         tryAndThrowException(callback, securityException, ERROR_SECURITY);
                     } catch (HealthConnectException healthConnectException) {
-                        builder.setHealthDataServiceApiStatusError(
+                        logger.setHealthDataServiceApiStatusError(
                                 healthConnectException.getErrorCode());
                         Slog.e(TAG, "HealthConnectException: ", healthConnectException);
                         tryAndThrowException(
@@ -1169,11 +1214,11 @@
                                 healthConnectException,
                                 healthConnectException.getErrorCode());
                     } catch (Exception exception) {
-                        builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
+                        logger.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
                         Slog.e(TAG, "Exception: ", exception);
                         tryAndThrowException(callback, exception, ERROR_INTERNAL);
                     } finally {
-                        builder.build().log();
+                        logger.build().log();
                     }
                 },
                 uid,
@@ -1184,7 +1229,7 @@
             @NonNull AttributionSource attributionSource,
             @NonNull DeleteUsingFiltersRequestParcel request,
             @NonNull IEmptyResponseCallback callback,
-            @NonNull HealthConnectServiceLogger.Builder builder,
+            @NonNull HealthConnectServiceLogger.Builder logger,
             List<Integer> recordTypeIdsToDelete,
             int uid,
             int pid) {
@@ -1197,12 +1242,11 @@
                         new DeleteTransactionRequest(attributionSource.getPackageName(), request)
                                 .setHasManageHealthDataPermission(
                                         hasDataManagementPermission(uid, pid)));
-        tryAndReturnResult(callback, builder);
-        finishDataDeliveryWrite(recordTypeIdsToDelete, attributionSource);
+        tryAndReturnResult(callback, logger);
         HealthConnectThreadScheduler.scheduleInternalTask(
                 () -> postDeleteTasks(recordTypeIdsToDelete));
 
-        builder.setNumberOfRecords(numberOfRecordsDeleted)
+        logger.setNumberOfRecords(numberOfRecordsDeleted)
                 .setDataTypesFromRecordTypes(recordTypeIdsToDelete);
     }
 
@@ -1212,8 +1256,7 @@
             @NonNull String packageName,
             @HealthDataCategory.Type int dataCategory,
             @NonNull IGetPriorityResponseCallback callback) {
-        Objects.requireNonNull(packageName);
-        Objects.requireNonNull(callback);
+        checkParamsNonNull(packageName, callback);
 
         final int uid = Binder.getCallingUid();
         final int pid = Binder.getCallingPid();
@@ -1224,10 +1267,10 @@
                         enforceIsForegroundUser(userHandle);
                         mContext.enforcePermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid, null);
                         throwExceptionIfDataSyncInProgress();
+                        HealthDataCategoryPriorityHelper priorityHelper =
+                                HealthDataCategoryPriorityHelper.getInstance();
                         List<DataOrigin> dataOriginInPriorityOrder =
-                                HealthDataCategoryPriorityHelper.getInstance()
-                                        .getPriorityOrder(dataCategory)
-                                        .stream()
+                                priorityHelper.getPriorityOrder(dataCategory, mContext).stream()
                                         .map(
                                                 (name) ->
                                                         new DataOrigin.Builder()
@@ -1264,9 +1307,7 @@
             @NonNull String packageName,
             @NonNull UpdatePriorityRequestParcel updatePriorityRequest,
             @NonNull IEmptyResponseCallback callback) {
-        Objects.requireNonNull(packageName);
-        Objects.requireNonNull(updatePriorityRequest);
-        Objects.requireNonNull(callback);
+        checkParamsNonNull(packageName, updatePriorityRequest, callback);
 
         final int uid = Binder.getCallingUid();
         final int pid = Binder.getCallingPid();
@@ -1305,8 +1346,7 @@
     @Override
     public void setRecordRetentionPeriodInDays(
             int days, @NonNull UserHandle user, @NonNull IEmptyResponseCallback callback) {
-        Objects.requireNonNull(user);
-        Objects.requireNonNull(callback);
+        checkParamsNonNull(user, callback);
 
         final int uid = Binder.getCallingUid();
         final int pid = Binder.getCallingPid();
@@ -1341,7 +1381,7 @@
 
     @Override
     public int getRecordRetentionPeriodInDays(@NonNull UserHandle user) {
-        Objects.requireNonNull(user);
+        checkParamsNonNull(user);
 
         enforceIsForegroundUser(getCallingUserHandle());
         throwExceptionIfDataSyncInProgress();
@@ -1368,7 +1408,7 @@
      */
     @Override
     public void getContributorApplicationsInfo(@NonNull IApplicationInfoResponseCallback callback) {
-        Objects.requireNonNull(callback);
+        checkParamsNonNull(callback);
 
         final int uid = Binder.getCallingUid();
         final int pid = Binder.getCallingPid();
@@ -1406,7 +1446,7 @@
     /** Retrieves {@link RecordTypeInfoResponse} for each RecordType. */
     @Override
     public void queryAllRecordTypesInfo(@NonNull IRecordTypeInfoResponseCallback callback) {
-        Objects.requireNonNull(callback);
+        checkParamsNonNull(callback);
 
         final int uid = Binder.getCallingUid();
         final int pid = Binder.getCallingPid();
@@ -1444,8 +1484,7 @@
     @Override
     public void queryAccessLogs(
             @NonNull String packageName, @NonNull IAccessLogsResponseCallback callback) {
-        Objects.requireNonNull(packageName);
-        Objects.requireNonNull(callback);
+        checkParamsNonNull(packageName, callback);
 
         final int uid = Binder.getCallingUid();
         final int pid = Binder.getCallingPid();
@@ -1489,8 +1528,7 @@
     public void getActivityDates(
             @NonNull ActivityDatesRequestParcel activityDatesRequestParcel,
             @NonNull IActivityDatesResponseCallback callback) {
-        Objects.requireNonNull(activityDatesRequestParcel);
-        Objects.requireNonNull(callback);
+        checkParamsNonNull(activityDatesRequestParcel, callback);
 
         final int uid = Binder.getCallingUid();
         final int pid = Binder.getCallingPid();
@@ -1538,8 +1576,7 @@
      */
     @Override
     public void startMigration(@NonNull String packageName, @NonNull IMigrationCallback callback) {
-        Objects.requireNonNull(packageName);
-        Objects.requireNonNull(callback);
+        checkParamsNonNull(packageName, callback);
 
         int uid = Binder.getCallingUid();
         int pid = Binder.getCallingPid();
@@ -1585,8 +1622,7 @@
      */
     @Override
     public void finishMigration(@NonNull String packageName, @NonNull IMigrationCallback callback) {
-        Objects.requireNonNull(packageName);
-        Objects.requireNonNull(callback);
+        checkParamsNonNull(packageName, callback);
 
         int uid = Binder.getCallingUid();
         int pid = Binder.getCallingPid();
@@ -1607,7 +1643,6 @@
                         callback.onSuccess();
                     } catch (Exception e) {
                         Slog.e(TAG, "Exception: ", e);
-                        // TODO(b/263897830): Verify migration state and send errors properly
                         tryAndThrowException(callback, e, MigrationException.ERROR_INTERNAL, null);
                     }
                 });
@@ -1627,9 +1662,7 @@
             @NonNull String packageName,
             @NonNull MigrationEntityParcel parcel,
             @NonNull IMigrationCallback callback) {
-        Objects.requireNonNull(packageName);
-        Objects.requireNonNull(parcel);
-        Objects.requireNonNull(callback);
+        checkParamsNonNull(packageName, parcel, callback);
 
         int uid = Binder.getCallingUid();
         int pid = Binder.getCallingPid();
@@ -1674,8 +1707,7 @@
             @NonNull String packageName,
             int requiredSdkExtension,
             @NonNull IMigrationCallback callback) {
-        Objects.requireNonNull(packageName);
-        Objects.requireNonNull(callback);
+        checkParamsNonNull(packageName, callback);
 
         int uid = Binder.getCallingUid();
         int pid = Binder.getCallingPid();
@@ -1711,9 +1743,7 @@
             @NonNull StageRemoteDataRequest stageRemoteDataRequest,
             @NonNull UserHandle userHandle,
             @NonNull IDataStagingFinishedCallback callback) {
-        Objects.requireNonNull(stageRemoteDataRequest);
-        Objects.requireNonNull(userHandle);
-        Objects.requireNonNull(callback);
+        checkParamsNonNull(stageRemoteDataRequest, userHandle, callback);
 
         Map<String, ParcelFileDescriptor> origPfdsByFileName =
                 stageRemoteDataRequest.getPfdsByFileName();
@@ -1778,11 +1808,15 @@
     public void getAllDataForBackup(
             @NonNull StageRemoteDataRequest stageRemoteDataRequest,
             @NonNull UserHandle userHandle) {
-        Objects.requireNonNull(stageRemoteDataRequest);
-        Objects.requireNonNull(userHandle);
+        checkParamsNonNull(stageRemoteDataRequest, userHandle);
 
         mContext.enforceCallingPermission(HEALTH_CONNECT_BACKUP_INTER_AGENT_PERMISSION, null);
-        mBackupRestore.getAllDataForBackup(stageRemoteDataRequest, userHandle);
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mBackupRestore.getAllDataForBackup(stageRemoteDataRequest, userHandle);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
     }
 
     /**
@@ -1799,7 +1833,7 @@
      */
     @Override
     public void deleteAllStagedRemoteData(@NonNull UserHandle userHandle) {
-        Objects.requireNonNull(userHandle);
+        checkParamsNonNull(userHandle);
 
         mContext.enforceCallingPermission(
                 DELETE_STAGED_HEALTH_CONNECT_REMOTE_DATA_PERMISSION, null);
@@ -1829,7 +1863,7 @@
      */
     @Override
     public void getHealthConnectDataState(@NonNull IGetHealthConnectDataStateCallback callback) {
-        Objects.requireNonNull(callback);
+        checkParamsNonNull(callback);
 
         try {
             mDataPermissionEnforcer.enforceAnyOfPermissions(
@@ -1893,7 +1927,7 @@
     @Override
     public void getHealthConnectMigrationUiState(
             @NonNull IGetHealthConnectMigrationUiStateCallback callback) {
-        Objects.requireNonNull(callback);
+        checkParamsNonNull(callback);
 
         final int uid = Binder.getCallingUid();
         final int pid = Binder.getCallingPid();
@@ -1958,11 +1992,11 @@
             int uid,
             @QuotaCategory.Type int quotaCategory,
             boolean isInForeground,
-            HealthConnectServiceLogger.Builder builder) {
+            HealthConnectServiceLogger.Builder logger) {
         try {
             RateLimiter.tryAcquireApiCallQuota(uid, quotaCategory, isInForeground);
         } catch (RateLimiterException rateLimiterException) {
-            builder.setRateLimit(
+            logger.setRateLimit(
                     rateLimiterException.getRateLimiterQuotaBucket(),
                     rateLimiterException.getRateLimiterQuotaLimit());
             throw new HealthConnectException(
@@ -1974,12 +2008,12 @@
             int uid,
             @QuotaCategory.Type int quotaCategory,
             boolean isInForeground,
-            HealthConnectServiceLogger.Builder builder,
+            HealthConnectServiceLogger.Builder logger,
             long memoryCost) {
         try {
             RateLimiter.tryAcquireApiCallQuota(uid, quotaCategory, isInForeground, memoryCost);
         } catch (RateLimiterException rateLimiterException) {
-            builder.setRateLimit(
+            logger.setRateLimit(
                     rateLimiterException.getRateLimiterQuotaBucket(),
                     rateLimiterException.getRateLimiterQuotaLimit());
             throw new HealthConnectException(
@@ -2140,57 +2174,21 @@
     }
 
     private boolean hasDataManagementPermission(int uid, int pid) {
-        return mContext.checkPermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid)
-                == PERMISSION_GRANTED;
+        return isPermissionGranted(MANAGE_HEALTH_DATA_PERMISSION, uid, pid);
     }
 
-    private void finishDataDeliveryRead(int recordTypeId, AttributionSource attributionSource) {
-        finishDataDeliveryRead(Collections.singletonList(recordTypeId), attributionSource);
+    /**
+     * Returns true if Background Read feature is disabled or {@link
+     * HealthPermissions#READ_HEALTH_DATA_IN_BACKGROUND} permission is not granted for the provided
+     * uid and pid, false otherwise.
+     */
+    private boolean isOnlySelfReadInBackgroundAllowed(int uid, int pid) {
+        return !mDeviceConfigManager.isBackgroundReadFeatureEnabled()
+                || !isPermissionGranted(READ_HEALTH_DATA_IN_BACKGROUND, uid, pid);
     }
 
-    private void finishDataDeliveryRead(
-            List<Integer> recordTypeIds, AttributionSource attributionSource) {
-        Trace.traceBegin(TRACE_TAG_READ_SUBTASKS, TAG_READ.concat("FinishDataDeliveryRead"));
-
-        try {
-            for (Integer recordTypeId : recordTypeIds) {
-                String permissionName =
-                        HealthPermissions.getHealthReadPermission(
-                                RecordTypePermissionCategoryMapper
-                                        .getHealthPermissionCategoryForRecordType(recordTypeId));
-                mPermissionManager.finishDataDelivery(permissionName, attributionSource);
-            }
-        } catch (Exception exception) {
-            // Ignore: HC API has already fulfilled the result, ignore any exception we hit here
-        }
-        Trace.traceEnd(TRACE_TAG_READ_SUBTASKS);
-    }
-
-    private void finishDataDeliveryWriteRecords(
-            List<RecordInternal<?>> recordInternals, AttributionSource attributionSource) {
-        Trace.traceBegin(TRACE_TAG_READ_SUBTASKS, TAG_READ.concat(".FinishDataDeliveryWrite"));
-        Set<Integer> recordTypeIdsToEnforce = new ArraySet<>();
-        for (RecordInternal<?> recordInternal : recordInternals) {
-            recordTypeIdsToEnforce.add(recordInternal.getRecordType());
-        }
-
-        finishDataDeliveryWrite(recordTypeIdsToEnforce.stream().toList(), attributionSource);
-        Trace.traceEnd(TRACE_TAG_READ_SUBTASKS);
-    }
-
-    private void finishDataDeliveryWrite(
-            List<Integer> recordTypeIds, AttributionSource attributionSource) {
-        try {
-            for (Integer recordTypeId : recordTypeIds) {
-                String permissionName =
-                        HealthPermissions.getHealthWritePermission(
-                                RecordTypePermissionCategoryMapper
-                                        .getHealthPermissionCategoryForRecordType(recordTypeId));
-                mPermissionManager.finishDataDelivery(permissionName, attributionSource);
-            }
-        } catch (Exception exception) {
-            // Ignore: HC API has already fulfilled the result, ignore any exception we hit here
-        }
+    private boolean isPermissionGranted(String permission, int uid, int pid) {
+        return mContext.checkPermission(permission, pid, uid) == PERMISSION_GRANTED;
     }
 
     private void enforceBinderUidIsSameAsAttributionSourceUid(
@@ -2202,8 +2200,7 @@
 
     private void logRecordTypeSpecificUpsertMetrics(
             @NonNull List<RecordInternal<?>> recordInternals, @NonNull String packageName) {
-        Objects.requireNonNull(recordInternals);
-        Objects.requireNonNull(packageName);
+        checkParamsNonNull(recordInternals, packageName);
 
         Map<Integer, List<RecordInternal<?>>> recordTypeToRecordInternals =
                 getRecordTypeToListOfRecords(recordInternals);
@@ -2218,8 +2215,7 @@
 
     private void logRecordTypeSpecificReadMetrics(
             @NonNull List<RecordInternal<?>> recordInternals, @NonNull String packageName) {
-        Objects.requireNonNull(recordInternals);
-        Objects.requireNonNull(packageName);
+        checkParamsNonNull(recordInternals, packageName);
 
         Map<Integer, List<RecordInternal<?>>> recordTypeToRecordInternals =
                 getRecordTypeToListOfRecords(recordInternals);
@@ -2274,26 +2270,26 @@
     }
 
     private static void tryAndReturnResult(
-            IEmptyResponseCallback callback, HealthConnectServiceLogger.Builder builder) {
+            IEmptyResponseCallback callback, HealthConnectServiceLogger.Builder logger) {
         try {
             callback.onResult();
-            builder.setHealthDataServiceApiStatusSuccess();
+            logger.setHealthDataServiceApiStatusSuccess();
         } catch (RemoteException e) {
             Slog.e(TAG, "Remote call failed", e);
-            builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
+            logger.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
         }
     }
 
     private static void tryAndReturnResult(
             IInsertRecordsResponseCallback callback,
             List<String> uuids,
-            HealthConnectServiceLogger.Builder builder) {
+            HealthConnectServiceLogger.Builder logger) {
         try {
             callback.onResult(new InsertRecordsResponseParcel(uuids));
-            builder.setHealthDataServiceApiStatusSuccess();
+            logger.setHealthDataServiceApiStatusSuccess();
         } catch (RemoteException e) {
             Slog.e(TAG, "Remote call failed", e);
-            builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
+            logger.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
         }
     }
 
@@ -2452,4 +2448,10 @@
             Log.e(TAG, "Unable to send result to the callback", e);
         }
     }
+
+    private static void checkParamsNonNull(Object... params) {
+        for (Object param : params) {
+            Objects.requireNonNull(param);
+        }
+    }
 }
diff --git a/service/java/com/android/server/healthconnect/backuprestore/BackupRestore.java b/service/java/com/android/server/healthconnect/backuprestore/BackupRestore.java
index 584c5b0..fd4788a 100644
--- a/service/java/com/android/server/healthconnect/backuprestore/BackupRestore.java
+++ b/service/java/com/android/server/healthconnect/backuprestore/BackupRestore.java
@@ -18,6 +18,7 @@
 
 import static android.health.connect.Constants.DEFAULT_INT;
 import static android.health.connect.Constants.DEFAULT_LONG;
+import static android.health.connect.Constants.DEFAULT_PAGE_SIZE;
 import static android.health.connect.HealthConnectDataState.RESTORE_ERROR_FETCHING_DATA;
 import static android.health.connect.HealthConnectDataState.RESTORE_ERROR_NONE;
 import static android.health.connect.HealthConnectDataState.RESTORE_ERROR_UNKNOWN;
@@ -37,6 +38,8 @@
 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorLong;
 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorString;
 
+import static java.util.Objects.requireNonNull;
+
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.app.job.JobInfo;
@@ -47,6 +50,7 @@
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
 import android.health.connect.HealthConnectDataState;
 import android.health.connect.HealthConnectException;
 import android.health.connect.HealthConnectManager.DataDownloadState;
@@ -120,12 +124,22 @@
     @VisibleForTesting
     public static final String DATA_DOWNLOAD_STATE_KEY = "data_download_state_key";
     // The below values for the IntDef are defined in chronological order of the restore process.
+    @VisibleForTesting
     public static final int INTERNAL_RESTORE_STATE_UNKNOWN = 0;
+    @VisibleForTesting
     public static final int INTERNAL_RESTORE_STATE_WAITING_FOR_STAGING = 1;
+    @VisibleForTesting
     public static final int INTERNAL_RESTORE_STATE_STAGING_IN_PROGRESS = 2;
+    @VisibleForTesting
     public static final int INTERNAL_RESTORE_STATE_STAGING_DONE = 3;
+    @VisibleForTesting
     public static final int INTERNAL_RESTORE_STATE_MERGING_IN_PROGRESS = 4;
-    public static final int INTERNAL_RESTORE_STATE_MERGING_DONE = 5;
+    // See b/290172311 for details.
+    @VisibleForTesting
+    public static final int INTERNAL_RESTORE_STATE_MERGING_DONE_OLD_CODE = 5;
+
+    @VisibleForTesting
+    public static final int INTERNAL_RESTORE_STATE_MERGING_DONE = 6;
 
     @VisibleForTesting
     static final long DATA_DOWNLOAD_TIMEOUT_INTERVAL_MILLIS = 14 * DateUtils.DAY_IN_MILLIS;
@@ -163,6 +177,7 @@
             INTERNAL_RESTORE_STATE_STAGING_IN_PROGRESS,
             INTERNAL_RESTORE_STATE_STAGING_DONE,
             INTERNAL_RESTORE_STATE_MERGING_IN_PROGRESS,
+            INTERNAL_RESTORE_STATE_MERGING_DONE_OLD_CODE,
             INTERNAL_RESTORE_STATE_MERGING_DONE
     })
     public @interface InternalRestoreState {}
@@ -175,6 +190,9 @@
     @VisibleForTesting
     static final String GRANT_TIME_FILE_NAME = "health-permissions-first-grant-times.xml";
 
+    @VisibleForTesting
+    static final String STAGED_DATABASE_NAME = "healthconnect_staged.db";
+
     private static final String TAG = "HealthConnectBackupRestore";
     private final ReentrantReadWriteLock mStatesLock = new ReentrantReadWriteLock(true);
     private final FirstGrantTimeManager mFirstGrantTimeManager;
@@ -200,7 +218,7 @@
         mMigrationStateManager = migrationStateManager;
         mContext = context;
         mCurrentForegroundUser = mContext.getUser();
-        mStagedDbContext = new StagedDatabaseContext(context, mCurrentForegroundUser);
+        mStagedDbContext = StagedDatabaseContext.create(context, mCurrentForegroundUser);
     }
 
     public void setupForUser(UserHandle currentForegroundUser) {
@@ -359,8 +377,7 @@
     public BackupFileNamesSet getAllBackupFileNames(boolean forDeviceToDevice) {
         ArraySet<String> backupFileNames = new ArraySet<>();
         if (forDeviceToDevice) {
-            backupFileNames.add(
-                    TransactionManager.getInitialisedInstance().getDatabasePath().getName());
+            backupFileNames.add(STAGED_DATABASE_NAME);
         }
         backupFileNames.add(GRANT_TIME_FILE_NAME);
         return new BackupFileNamesSet(backupFileNames);
@@ -373,7 +390,8 @@
         if (downloadState == DATA_DOWNLOAD_COMPLETE) {
             setInternalRestoreState(INTERNAL_RESTORE_STATE_WAITING_FOR_STAGING, false /* force */);
         } else if (downloadState == DATA_DOWNLOAD_FAILED) {
-            setInternalRestoreState(INTERNAL_RESTORE_STATE_MERGING_DONE, false /* force */);
+            setInternalRestoreState(
+                    INTERNAL_RESTORE_STATE_MERGING_DONE, false /* force */);
             setDataRestoreError(RESTORE_ERROR_FETCHING_DATA);
         }
     }
@@ -382,7 +400,7 @@
     public void deleteAndResetEverything(@NonNull UserHandle userHandle) {
         // Don't delete anything while we are in the process of merging staged data.
         synchronized (mMergingLock) {
-            mStagedDbContext.deleteDatabase(HealthConnectDatabase.getName());
+            mStagedDbContext.deleteDatabase(STAGED_DATABASE_NAME);
             mStagedDatabase = null;
             FilesUtil.deleteDir(getStagedRemoteDataDirectoryForUser(userHandle.getIdentifier()));
         }
@@ -552,8 +570,9 @@
     boolean shouldAttemptMerging() {
         @InternalRestoreState int internalRestoreState = getInternalRestoreState();
         if (internalRestoreState == INTERNAL_RESTORE_STATE_STAGING_DONE
-                || internalRestoreState == INTERNAL_RESTORE_STATE_MERGING_IN_PROGRESS) {
-            Slog.i(TAG, "Will attempt merging as it was already happening or bound to happen");
+                || internalRestoreState == INTERNAL_RESTORE_STATE_MERGING_IN_PROGRESS
+                || internalRestoreState == INTERNAL_RESTORE_STATE_MERGING_DONE_OLD_CODE) {
+            Slog.i(TAG, "Should attempt merging now with state = " + internalRestoreState);
             return true;
         }
         return false;
@@ -574,13 +593,30 @@
         }
 
         int currentDbVersion = TransactionManager.getInitialisedInstance().getDatabaseVersion();
-        int stagedDbVersion = getStagedDatabase().getReadableDatabase().getVersion();
-        if (currentDbVersion < stagedDbVersion) {
-            Slog.i(TAG, "Module needs upgrade for merging to version " + stagedDbVersion);
-            setDataRestoreError(RESTORE_ERROR_VERSION_DIFF);
-            return;
+        File stagedDbFile = mStagedDbContext.getDatabasePath(STAGED_DATABASE_NAME);
+        if (stagedDbFile.exists()) {
+            try (SQLiteDatabase stagedDb =
+                         SQLiteDatabase.openDatabase(
+                                 stagedDbFile,
+                                 new SQLiteDatabase.OpenParams.Builder().build())) {
+                int stagedDbVersion = stagedDb.getVersion();
+                Slog.i(
+                        TAG,
+                        "merging staged data, current version = "
+                                + currentDbVersion
+                                + ", staged version = "
+                                + stagedDbVersion);
+                if (currentDbVersion < stagedDbVersion) {
+                    Slog.i(TAG, "Module needs upgrade for merging to version " + stagedDbVersion);
+                    setDataRestoreError(RESTORE_ERROR_VERSION_DIFF);
+                    return;
+                }
+            }
+        } else {
+            Slog.i(TAG, "No database file found to merge.");
         }
 
+        Slog.i(TAG, "Starting the data merge.");
         setInternalRestoreState(INTERNAL_RESTORE_STATE_MERGING_IN_PROGRESS, false);
         mergeGrantTimes();
         mergeDatabase();
@@ -591,7 +627,7 @@
         ArrayMap<String, File> backupFilesByFileNames = new ArrayMap<>();
 
         File databasePath = TransactionManager.getInitialisedInstance().getDatabasePath();
-        backupFilesByFileNames.put(databasePath.getName(), databasePath);
+        backupFilesByFileNames.put(STAGED_DATABASE_NAME, databasePath);
 
         File backupDataDir = getBackupDataDirectoryForUser(userHandle.getIdentifier());
         backupDataDir.mkdirs();
@@ -954,7 +990,7 @@
 
     private void mergeDatabase() {
         synchronized (mMergingLock) {
-            if (!mStagedDbContext.getDatabasePath(HealthConnectDatabase.getName()).exists()) {
+            if (!mStagedDbContext.getDatabasePath(STAGED_DATABASE_NAME).exists()) {
                 Slog.i(TAG, "No staged db found.");
                 // no db was staged
                 return;
@@ -974,9 +1010,12 @@
                 mergeRecordsOfType(recordTypeMapEntry.getKey(), recordTypeMapEntry.getValue());
             }
 
+            Slog.i(TAG, "Sync app info records after restored data merge.");
+            AppInfoHelper.getInstance().syncAppInfoRecordTypesUsed();
+
             // Delete the staged db as we are done merging.
             Slog.i(TAG, "Deleting staged db after merging.");
-            mStagedDbContext.deleteDatabase(HealthConnectDatabase.getName());
+            mStagedDbContext.deleteDatabase(STAGED_DATABASE_NAME);
             mStagedDatabase = null;
         }
     }
@@ -1007,10 +1046,7 @@
             TransactionManager.getInitialisedInstance()
                     .insertAll(upsertTransactionRequest.getUpsertRequests());
 
-            token = DEFAULT_LONG;
-            if (recordsToMergeAndToken.second != DEFAULT_LONG) {
-                token = recordsToMergeAndToken.second * 2;
-            }
+            token = recordsToMergeAndToken.second;
         } while (token != DEFAULT_LONG);
 
         // Once all the records of this type have been merged we can delete the table.
@@ -1031,7 +1067,6 @@
             Class<T> recordTypeClass, long requestToken, RecordHelper<?> recordHelper) {
         ReadRecordsRequestUsingFilters<T> readRecordsRequest =
                 new ReadRecordsRequestUsingFilters.Builder<>(recordTypeClass)
-                        .setAscending(true)
                         .setPageSize(2000)
                         .setPageToken(requestToken)
                         .build();
@@ -1048,32 +1083,33 @@
                 new ReadTransactionRequest(
                         null,
                         readRecordsRequest.toReadRecordsRequestParcel(),
-                        DEFAULT_LONG /* startDateAccess */,
+                        DEFAULT_LONG /* startDateAccessMillis */,
                         false,
                         extraReadPermsMapping);
 
         List<RecordInternal<?>> recordInternalList;
-        long token = DEFAULT_LONG;
+        long token;
         ReadTableRequest readTableRequest = readTransactionRequest.getReadRequests().get(0);
         try (Cursor cursor = read(readTableRequest)) {
-            recordInternalList =
-                    recordHelper.getInternalRecords(
-                            cursor, readTableRequest.getPageSize(), mStagedPackageNamesByAppIds);
-            String startTimeColumnName = recordHelper.getStartTimeColumnName();
-
+            Pair<List<RecordInternal<?>>, Long> readResult =
+                    recordHelper.getNextInternalRecordsPageAndToken(
+                            cursor,
+                            readTransactionRequest.getPageSize().orElse(DEFAULT_PAGE_SIZE),
+                            requireNonNull(readTransactionRequest.getPageToken()),
+                            mStagedPackageNamesByAppIds);
+            recordInternalList = readResult.first;
+            token = readResult.second;
             populateInternalRecordsWithExtraData(recordInternalList, readTableRequest);
-
-            // Get the token for the next read request.
-            if (cursor.moveToNext()) {
-                token = getCursorLong(cursor, startTimeColumnName);
-            }
         }
         return Pair.create(recordInternalList, token);
     }
 
     private Cursor read(ReadTableRequest request) {
         synchronized (mMergingLock) {
-            return mStagedDatabase.getReadableDatabase().rawQuery(request.getReadCommand(), null);
+            return getStagedDatabase()
+                    .getReadableDatabase()
+                    .rawQuery(request.getReadCommand(), null);
+
         }
     }
 
@@ -1112,7 +1148,7 @@
     HealthConnectDatabase getStagedDatabase() {
         synchronized (mMergingLock) {
             if (mStagedDatabase == null) {
-                mStagedDatabase = new HealthConnectDatabase(mStagedDbContext);
+                mStagedDatabase = new HealthConnectDatabase(mStagedDbContext, STAGED_DATABASE_NAME);
             }
             return mStagedDatabase;
         }
@@ -1123,12 +1159,12 @@
      *
      * @hide
      */
-    private static final class StagedDatabaseContext extends ContextWrapper {
+    static final class StagedDatabaseContext extends ContextWrapper {
         private volatile UserHandle mCurrentForegroundUser;
 
         StagedDatabaseContext(@NonNull Context context, UserHandle userHandle) {
             super(context);
-            Objects.requireNonNull(context);
+            requireNonNull(context);
             mCurrentForegroundUser = userHandle;
         }
 
@@ -1143,6 +1179,10 @@
             stagedDataDir.mkdirs();
             return new File(stagedDataDir, name);
         }
+
+        static StagedDatabaseContext create(@NonNull Context context, UserHandle handle) {
+            return new StagedDatabaseContext(context, handle);
+        }
     }
 
     /** Execute the task as critical section by holding read lock. */
@@ -1201,7 +1241,7 @@
             final long token = Binder.clearCallingIdentity();
             try {
                 int result =
-                        Objects.requireNonNull(context.getSystemService(JobScheduler.class))
+                        requireNonNull(context.getSystemService(JobScheduler.class))
                                 .forNamespace(BACKUP_RESTORE_JOBS_NAMESPACE)
                                 .schedule(jobInfo);
 
@@ -1218,7 +1258,7 @@
 
         /** Cancels all jobs for our namespace. */
         public static void cancelAllJobs(Context context) {
-            Objects.requireNonNull(context.getSystemService(JobScheduler.class))
+            requireNonNull(context.getSystemService(JobScheduler.class))
                     .forNamespace(BACKUP_RESTORE_JOBS_NAMESPACE)
                     .cancelAll();
         }
diff --git a/service/java/com/android/server/healthconnect/logging/HealthConnectServiceLogger.java b/service/java/com/android/server/healthconnect/logging/HealthConnectServiceLogger.java
index c88bae4..0c7f528 100644
--- a/service/java/com/android/server/healthconnect/logging/HealthConnectServiceLogger.java
+++ b/service/java/com/android/server/healthconnect/logging/HealthConnectServiceLogger.java
@@ -30,6 +30,9 @@
 import static android.health.HealthFitnessStatsLog.HEALTH_CONNECT_API_CALLED__API_STATUS__ERROR;
 import static android.health.HealthFitnessStatsLog.HEALTH_CONNECT_API_CALLED__API_STATUS__STATUS_UNKNOWN;
 import static android.health.HealthFitnessStatsLog.HEALTH_CONNECT_API_CALLED__API_STATUS__SUCCESS;
+import static android.health.HealthFitnessStatsLog.HEALTH_CONNECT_API_CALLED__CALLER_FOREGROUND_STATE__BACKGROUND;
+import static android.health.HealthFitnessStatsLog.HEALTH_CONNECT_API_CALLED__CALLER_FOREGROUND_STATE__FOREGROUND;
+import static android.health.HealthFitnessStatsLog.HEALTH_CONNECT_API_CALLED__CALLER_FOREGROUND_STATE__UNSPECIFIED;
 import static android.health.HealthFitnessStatsLog.HEALTH_CONNECT_API_CALLED__RATE_LIMIT__NOT_DEFINED;
 import static android.health.HealthFitnessStatsLog.HEALTH_CONNECT_API_CALLED__RATE_LIMIT__NOT_USED;
 import static android.health.HealthFitnessStatsLog.HEALTH_CONNECT_API_CALLED__RATE_LIMIT__RATE_LIMIT_BACKGROUND_15_MIN_ABOVE_3000;
@@ -166,6 +169,7 @@
     private final int mNumberOfRecords;
     private final int[] mRecordTypes;
     private final String mPackageName;
+    private final int mCallerForegroundState;
     private static final int MAX_NUMBER_OF_LOGGED_DATA_TYPES = 6;
     private static final int RECORD_TYPE_NOT_ASSIGNED_DEFAULT_VALUE = -1;
 
@@ -356,6 +360,7 @@
         private final boolean mHoldsDataManagementPermission;
         private int[] mRecordTypes;
         private String mPackageName;
+        private int mCallerForegroundState;
 
         public Builder(boolean holdsDataManagementPermission, @ApiMethods.ApiMethod int apiMethod) {
             mStartTime = System.currentTimeMillis();
@@ -368,6 +373,8 @@
             mRecordTypes = new int[MAX_NUMBER_OF_LOGGED_DATA_TYPES];
             Arrays.fill(mRecordTypes, RECORD_TYPE_NOT_ASSIGNED_DEFAULT_VALUE);
             mPackageName = "UNKNOWN";
+            mCallerForegroundState =
+                    HEALTH_CONNECT_API_CALLED__CALLER_FOREGROUND_STATE__UNSPECIFIED;
         }
 
         /** Set the API was called successfully. */
@@ -475,6 +482,20 @@
             return this;
         }
 
+        /**
+         * Sets the caller's foreground state.
+         *
+         * @param isCallerInForeground whether the caller is in foreground or background.
+         */
+        @NonNull
+        public Builder setCallerForegroundState(boolean isCallerInForeground) {
+            mCallerForegroundState =
+                    isCallerInForeground
+                            ? HEALTH_CONNECT_API_CALLED__CALLER_FOREGROUND_STATE__FOREGROUND
+                            : HEALTH_CONNECT_API_CALLED__CALLER_FOREGROUND_STATE__BACKGROUND;
+            return this;
+        }
+
         /** Returns an object of {@link HealthConnectServiceLogger}. */
         public HealthConnectServiceLogger build() {
             mDuration = System.currentTimeMillis() - mStartTime;
@@ -595,6 +616,7 @@
         mNumberOfRecords = builder.mNumberOfRecords;
         mRecordTypes = builder.mRecordTypes;
         mPackageName = builder.mPackageName;
+        mCallerForegroundState = builder.mCallerForegroundState;
     }
 
     /** Log to statsd. */
@@ -611,7 +633,8 @@
                 mErrorCode,
                 mDuration,
                 mNumberOfRecords,
-                mRateLimit);
+                mRateLimit,
+                mCallerForegroundState);
 
         // For private logging, max 6 data types per request are being logged
         // rest will be ignored
diff --git a/service/java/com/android/server/healthconnect/logging/UsageStatsCollector.java b/service/java/com/android/server/healthconnect/logging/UsageStatsCollector.java
index eaf6bbe..ff77f30 100644
--- a/service/java/com/android/server/healthconnect/logging/UsageStatsCollector.java
+++ b/service/java/com/android/server/healthconnect/logging/UsageStatsCollector.java
@@ -26,7 +26,11 @@
 import android.os.UserHandle;
 
 import com.android.server.healthconnect.permission.PackageInfoUtils;
+import com.android.server.healthconnect.storage.datatypehelpers.AccessLogsHelper;
+import com.android.server.healthconnect.storage.datatypehelpers.PreferenceHelper;
 
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
 import java.util.List;
 import java.util.Objects;
 
@@ -36,7 +40,9 @@
  * @hide
  */
 final class UsageStatsCollector {
-
+    private static final String USER_MOST_RECENT_ACCESS_LOG_TIME =
+            "USER_MOST_RECENT_ACCESS_LOG_TIME";
+    private static final int NUMBER_OF_DAYS_FOR_USER_TO_BE_MONTHLY_ACTIVE = 30;
     private final Context mContext;
     private final List<PackageInfo> mAllPackagesInstalledForUser;
 
@@ -86,6 +92,38 @@
         return count;
     }
 
+    boolean isUserMonthlyActive() {
+        PreferenceHelper preferenceHelper = PreferenceHelper.getInstance();
+
+        String latestAccessLogTimeStampString =
+                preferenceHelper.getPreference(USER_MOST_RECENT_ACCESS_LOG_TIME);
+
+        // Return false if preference is empty and make sure latest access was within past
+        // 30 days.
+        return latestAccessLogTimeStampString != null
+                && Instant.now()
+                                .minus(
+                                        NUMBER_OF_DAYS_FOR_USER_TO_BE_MONTHLY_ACTIVE,
+                                        ChronoUnit.DAYS)
+                                .toEpochMilli()
+                        <= Long.parseLong(latestAccessLogTimeStampString);
+    }
+
+    void upsertLastAccessLogTimeStamp() {
+        PreferenceHelper preferenceHelper = PreferenceHelper.getInstance();
+
+        long latestAccessLogTimeStamp =
+                AccessLogsHelper.getInstance().getLatestAccessLogTimeStamp();
+
+        // Access logs are only stored for 7 days, therefore only update this value if there is an
+        // access log. Last access timestamp can be before 7 days and might already exist in
+        // preference and in that case we should not overwrite the existing value.
+        if (latestAccessLogTimeStamp != Long.MIN_VALUE) {
+            preferenceHelper.insertOrReplacePreference(
+                    USER_MOST_RECENT_ACCESS_LOG_TIME, String.valueOf(latestAccessLogTimeStamp));
+        }
+    }
+
     private boolean hasRequestedHealthPermission(@NonNull PackageInfo packageInfo) {
         if (packageInfo == null || packageInfo.requestedPermissions == null) {
             return false;
diff --git a/service/java/com/android/server/healthconnect/logging/UsageStatsLogger.java b/service/java/com/android/server/healthconnect/logging/UsageStatsLogger.java
index 7d7c6bd..01a3399 100644
--- a/service/java/com/android/server/healthconnect/logging/UsageStatsLogger.java
+++ b/service/java/com/android/server/healthconnect/logging/UsageStatsLogger.java
@@ -36,10 +36,12 @@
         Objects.requireNonNull(context);
 
         UsageStatsCollector usageStatsCollector = new UsageStatsCollector(context, userHandle);
+        usageStatsCollector.upsertLastAccessLogTimeStamp();
 
         HealthFitnessStatsLog.write(
                 HealthFitnessStatsLog.HEALTH_CONNECT_USAGE_STATS,
                 usageStatsCollector.getPackagesHoldingHealthPermissions(),
-                usageStatsCollector.getNumberOfAppsCompatibleWithHealthConnect());
+                usageStatsCollector.getNumberOfAppsCompatibleWithHealthConnect(),
+                usageStatsCollector.isUserMonthlyActive());
     }
 }
diff --git a/service/java/com/android/server/healthconnect/permission/DataPermissionEnforcer.java b/service/java/com/android/server/healthconnect/permission/DataPermissionEnforcer.java
index 2ea5c2c..f0acfc6 100644
--- a/service/java/com/android/server/healthconnect/permission/DataPermissionEnforcer.java
+++ b/service/java/com/android/server/healthconnect/permission/DataPermissionEnforcer.java
@@ -17,6 +17,9 @@
 package com.android.server.healthconnect.permission;
 
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.health.connect.HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND;
+
+import static java.util.stream.Collectors.toMap;
 
 import android.annotation.NonNull;
 import android.content.AttributionSource;
@@ -29,6 +32,7 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 
+import com.android.server.healthconnect.HealthConnectDeviceConfigManager;
 import com.android.server.healthconnect.storage.datatypehelpers.RecordHelper;
 import com.android.server.healthconnect.storage.utils.RecordHelperProvider;
 
@@ -36,6 +40,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Function;
 
 /**
  * Helper class to force caller of data apis to hold api required permissions.
@@ -45,10 +50,15 @@
 public class DataPermissionEnforcer {
     private final PermissionManager mPermissionManager;
     private final Context mContext;
+    private final HealthConnectDeviceConfigManager mDeviceConfigManager;
 
-    public DataPermissionEnforcer(PermissionManager permissionManager, Context context) {
+    public DataPermissionEnforcer(
+            @NonNull PermissionManager permissionManager,
+            @NonNull Context context,
+            @NonNull HealthConnectDeviceConfigManager deviceConfigManager) {
         mPermissionManager = permissionManager;
         mContext = context;
+        mDeviceConfigManager = deviceConfigManager;
     }
 
     /** Enforces default write permissions for given recordTypeIds */
@@ -146,22 +156,33 @@
     }
 
     /**
+     * Checks the Background Read feature flags, enforces {@link
+     * HealthPermissions#READ_HEALTH_DATA_IN_BACKGROUND} permission if the flag is enabled,
+     * otherwise throws {@link SecurityException}.
+     */
+    public void enforceBackgroundReadRestrictions(int uid, int pid, @NonNull String errorMessage) {
+        if (mDeviceConfigManager.isBackgroundReadFeatureEnabled()) {
+            mContext.enforcePermission(READ_HEALTH_DATA_IN_BACKGROUND, pid, uid, errorMessage);
+        } else {
+            throw new SecurityException(errorMessage);
+        }
+    }
+
+    /**
      * Collects extra read permissions to its grant state. Used to not expose extra data if caller
      * doesn't have corresponding permission.
      */
     public Map<String, Boolean> collectExtraReadPermissionToStateMapping(
-            int recordTypeId, AttributionSource attributionSource) {
-        RecordHelper<?> recordHelper =
-                RecordHelperProvider.getInstance().getRecordHelper(recordTypeId);
-        if (recordHelper.getExtraReadPermissions().isEmpty()) {
-            return Collections.emptyMap();
-        }
-
-        Map<String, Boolean> mapping = new ArrayMap<>();
-        for (String permissionName : recordHelper.getExtraReadPermissions()) {
-            mapping.put(permissionName, isPermissionGranted(permissionName, attributionSource));
-        }
-        return mapping;
+            Set<Integer> recordTypeIds, AttributionSource attributionSource) {
+        RecordHelperProvider recordHelperProvider = RecordHelperProvider.getInstance();
+        return recordTypeIds.stream()
+                .map(recordHelperProvider::getRecordHelper)
+                .flatMap(recordHelper -> recordHelper.getExtraReadPermissions().stream())
+                .distinct()
+                .collect(
+                        toMap(
+                                Function.identity(),
+                                permission -> isPermissionGranted(permission, attributionSource)));
     }
 
     public Map<String, Boolean> collectExtraWritePermissionStateMapping(
@@ -211,7 +232,7 @@
 
     private boolean isPermissionGranted(
             String permissionName, AttributionSource attributionSource) {
-        return mPermissionManager.checkPermissionForStartDataDelivery(
+        return mPermissionManager.checkPermissionForDataDelivery(
                         permissionName, attributionSource, null)
                 == PERMISSION_GRANTED;
     }
diff --git a/service/java/com/android/server/healthconnect/permission/FirstGrantTimeManager.java b/service/java/com/android/server/healthconnect/permission/FirstGrantTimeManager.java
index ce2d612..eaf9f5b 100644
--- a/service/java/com/android/server/healthconnect/permission/FirstGrantTimeManager.java
+++ b/service/java/com/android/server/healthconnect/permission/FirstGrantTimeManager.java
@@ -145,7 +145,7 @@
         initAndValidateUserStateIfNeedLocked(user);
 
         if (!checkSupportPermissionsUsageIntent(packageNames, user)) {
-            logIfInDebugMode("Can find health intent declaration in ", packageNames[0]);
+            logIfInDebugMode("Cannot find health intent declaration in ", packageNames[0]);
             return;
         }
 
@@ -164,7 +164,7 @@
                     // already take care of merging permissions.
                     if (!MigrationStateManager.getInitialisedInstance().isMigrationInProgress()) {
                         HealthConnectThreadScheduler.scheduleInternalTask(
-                                () -> removeAppFromPriorityList(packageNames));
+                                () -> removeAppsFromPriorityList(packageNames));
                     }
                 } else {
                     // An app got new health permission, set current time as it's first grant
@@ -650,9 +650,10 @@
         }
     }
 
-    private void removeAppFromPriorityList(String[] packageNames) {
+    private void removeAppsFromPriorityList(String[] packageNames) {
         for (String packageName : packageNames) {
-            HealthDataCategoryPriorityHelper.getInstance().removeAppFromPriorityList(packageName);
+            HealthDataCategoryPriorityHelper.getInstance()
+                    .maybeRemoveAppWithoutWritePermissionsFromPriorityList(packageName);
         }
     }
 
diff --git a/service/java/com/android/server/healthconnect/permission/HealthConnectPermissionHelper.java b/service/java/com/android/server/healthconnect/permission/HealthConnectPermissionHelper.java
index d16faec..8e5d9a1 100644
--- a/service/java/com/android/server/healthconnect/permission/HealthConnectPermissionHelper.java
+++ b/service/java/com/android/server/healthconnect/permission/HealthConnectPermissionHelper.java
@@ -30,6 +30,8 @@
 import android.health.connect.HealthPermissions;
 import android.os.Binder;
 import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.ArraySet;
 
 import com.android.server.healthconnect.storage.datatypehelpers.HealthDataCategoryPriorityHelper;
 
@@ -37,6 +39,7 @@
 import java.time.Period;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 
@@ -95,7 +98,7 @@
         Objects.requireNonNull(packageName);
         Objects.requireNonNull(permissionName);
         enforceManageHealthPermissions(/* message= */ "grantHealthPermission");
-        enforceValidPermission(permissionName);
+        enforceValidHealthPermission(permissionName);
         UserHandle checkedUser = UserHandle.of(handleIncomingUser(user.getIdentifier()));
         enforceValidPackage(packageName, checkedUser);
         enforceSupportPermissionsUsageIntent(packageName, checkedUser);
@@ -124,7 +127,7 @@
         Objects.requireNonNull(packageName);
         Objects.requireNonNull(permissionName);
         enforceManageHealthPermissions(/* message= */ "revokeHealthPermission");
-        enforceValidPermission(permissionName);
+        enforceValidHealthPermission(permissionName);
         UserHandle checkedUser = UserHandle.of(handleIncomingUser(user.getIdentifier()));
         enforceValidPackage(packageName, checkedUser);
         final long token = Binder.clearCallingIdentity();
@@ -151,7 +154,9 @@
                     MASK_PERMISSION_FLAGS,
                     permissionFlags,
                     checkedUser);
+
             removeFromPriorityListIfRequired(packageName, permissionName);
+
         } finally {
             Binder.restoreCallingIdentity(token);
         }
@@ -188,6 +193,47 @@
         }
     }
 
+    /** See {@link HealthConnectManager#getHealthPermissionsFlags(String, List)}. */
+    @NonNull
+    public Map<String, Integer> getHealthPermissionsFlags(
+            @NonNull String packageName,
+            @NonNull UserHandle user,
+            @NonNull List<String> permissions) {
+        Objects.requireNonNull(packageName);
+        Objects.requireNonNull(user);
+        Objects.requireNonNull(permissions);
+
+        enforceManageHealthPermissions(/* message= */ "getHealthPermissionsFlags");
+        UserHandle checkedUser = UserHandle.of(handleIncomingUser(user.getIdentifier()));
+        enforceValidPackage(packageName, checkedUser);
+        final long token = Binder.clearCallingIdentity();
+        try {
+            return getHealthPermissionsFlagsUnchecked(packageName, checkedUser, permissions);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /** See {@link HealthConnectManager#makeHealthPermissionsRequestable(String, List)}. */
+    public void makeHealthPermissionsRequestable(
+            @NonNull String packageName,
+            @NonNull UserHandle user,
+            @NonNull List<String> permissions) {
+        Objects.requireNonNull(packageName);
+        Objects.requireNonNull(user);
+        Objects.requireNonNull(permissions);
+
+        enforceManageHealthPermissions(/* message= */ "makeHealthPermissionsRequestable");
+        UserHandle checkedUser = UserHandle.of(handleIncomingUser(user.getIdentifier()));
+        enforceValidPackage(packageName, checkedUser);
+        final long token = Binder.clearCallingIdentity();
+        try {
+            makeHealthPermissionsRequestableUnchecked(packageName, checkedUser, permissions);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
     /**
      * Returns {@code true} if there is at least one granted permission for the provided {@code
      * packageName}, {@code false} otherwise.
@@ -220,7 +266,7 @@
     /**
      * Same as {@link #getHealthDataStartDateAccess(String, UserHandle)} except this method also
      * throws {@link IllegalAccessException} if health permission is in an incorrect state where
-     * first grant time can't be fetch.
+     * first grant time can't be fetched.
      */
     @NonNull
     public Instant getHealthDataStartDateAccessOrThrow(String packageName, UserHandle user) {
@@ -245,17 +291,20 @@
             HealthDataCategoryPriorityHelper.getInstance()
                     .appendToPriorityList(
                             packageName,
-                            HealthPermissions.getHealthDataCategory(permissionName),
-                            mContext);
+                            HealthPermissions.getHealthDataCategoryForWritePermission(
+                                    permissionName),
+                            mContext,
+                            /* isInactiveApp= */ false);
         }
     }
 
     private void removeFromPriorityListIfRequired(String packageName, String permissionName) {
         if (HealthPermissions.isWritePermission(permissionName)) {
             HealthDataCategoryPriorityHelper.getInstance()
-                    .removeFromPriorityList(
+                    .maybeRemoveAppFromPriorityList(
                             packageName,
-                            HealthPermissions.getHealthDataCategory(permissionName),
+                            HealthPermissions.getHealthDataCategoryForWritePermission(
+                                    permissionName),
                             this,
                             mContext.getUser());
         }
@@ -264,17 +313,11 @@
     @NonNull
     private List<String> getGrantedHealthPermissionsUnchecked(
             @NonNull String packageName, @NonNull UserHandle user) {
-        PackageInfo packageInfo;
-        try {
-            PackageManager packageManager =
-                    mContext.createContextAsUser(user, /* flags= */ 0).getPackageManager();
-            packageInfo =
-                    packageManager.getPackageInfo(
-                            packageName,
-                            PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS));
-        } catch (PackageManager.NameNotFoundException e) {
-            throw new IllegalArgumentException("Invalid package", e);
-        }
+        PackageInfo packageInfo =
+                getPackageInfoUnchecked(
+                        packageName,
+                        user,
+                        PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS));
 
         if (packageInfo.requestedPermissions == null) {
             return List.of();
@@ -293,6 +336,37 @@
         return grantedHealthPerms;
     }
 
+    @NonNull
+    private Map<String, Integer> getHealthPermissionsFlagsUnchecked(
+            @NonNull String packageName,
+            @NonNull UserHandle user,
+            @NonNull List<String> permissions) {
+        enforceValidHealthPermissions(packageName, user, permissions);
+
+        Map<String, Integer> result = new ArrayMap<>();
+
+        for (String permission : permissions) {
+            result.put(
+                    permission, mPackageManager.getPermissionFlags(permission, packageName, user));
+        }
+
+        return result;
+    }
+
+    private void makeHealthPermissionsRequestableUnchecked(
+            @NonNull String packageName,
+            @NonNull UserHandle user,
+            @NonNull List<String> permissions) {
+        enforceValidHealthPermissions(packageName, user, permissions);
+
+        int flagMask = PackageManager.FLAG_PERMISSION_USER_FIXED;
+
+        for (String permission : permissions) {
+            mPackageManager.updatePermissionFlags(
+                    permission, packageName, flagMask, /* flagValues= */ 0, user);
+        }
+    }
+
     private void revokeAllHealthPermissionsUnchecked(
             String packageName, UserHandle user, String reason) {
         List<String> grantedHealthPermissions =
@@ -309,23 +383,28 @@
         }
     }
 
-    private void enforceValidPermission(String permissionName) {
+    private void enforceValidHealthPermission(String permissionName) {
         if (!mHealthPermissions.contains(permissionName)) {
             throw new IllegalArgumentException("invalid health permission");
         }
     }
 
-    private void enforceValidPackage(String packageName, UserHandle user) {
+    private PackageInfo getPackageInfoUnchecked(
+            String packageName, UserHandle user, PackageManager.PackageInfoFlags flags) {
         try {
             PackageManager packageManager =
                     mContext.createContextAsUser(user, /* flags= */ 0).getPackageManager();
 
-            packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0));
+            return packageManager.getPackageInfo(packageName, flags);
         } catch (PackageManager.NameNotFoundException e) {
             throw new IllegalArgumentException("invalid package", e);
         }
     }
 
+    private void enforceValidPackage(String packageName, UserHandle user) {
+        getPackageInfoUnchecked(packageName, user, PackageManager.PackageInfoFlags.of(0));
+    }
+
     private void enforceManageHealthPermissions(String message) {
         mContext.enforceCallingOrSelfPermission(
                 HealthPermissions.MANAGE_HEALTH_PERMISSIONS, message);
@@ -373,4 +452,24 @@
                         + INTERACT_ACROSS_USERS_FULL
                         + " permission");
     }
+
+    private void enforceValidHealthPermissions(
+            String packageName, UserHandle user, List<String> permissions) {
+        PackageInfo packageInfo =
+                getPackageInfoUnchecked(
+                        packageName,
+                        user,
+                        PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS));
+
+        Set<String> requestedPermissions = new ArraySet<>(packageInfo.requestedPermissions);
+
+        for (String permission : permissions) {
+            if (!requestedPermissions.contains(permission)) {
+                throw new IllegalArgumentException(
+                        "undeclared permission " + permission + " for package " + packageName);
+            }
+
+            enforceValidHealthPermission(permission);
+        }
+    }
 }
diff --git a/service/java/com/android/server/healthconnect/permission/PermissionPackageChangesOrchestrator.java b/service/java/com/android/server/healthconnect/permission/PermissionPackageChangesOrchestrator.java
index 2e36708..a5f266c 100644
--- a/service/java/com/android/server/healthconnect/permission/PermissionPackageChangesOrchestrator.java
+++ b/service/java/com/android/server/healthconnect/permission/PermissionPackageChangesOrchestrator.java
@@ -107,7 +107,8 @@
                 HealthConnectThreadScheduler.scheduleInternalTask(
                         () ->
                                 HealthDataCategoryPriorityHelper.getInstance()
-                                        .removeAppFromPriorityList(packageName));
+                                        .maybeRemoveAppWithoutWritePermissionsFromPriorityList(
+                                                packageName));
             }
         } else if (isHealthIntentRemoved) {
             // Revoke all health permissions as we don't grant health permissions if permissions
@@ -121,8 +122,17 @@
                                 + userHandle);
             }
 
-            mPermissionHelper.revokeAllHealthPermissions(
-                    packageName, "Health permissions usage activity has been removed.", userHandle);
+            try {
+                mPermissionHelper.revokeAllHealthPermissions(
+                        packageName,
+                        "Health permissions usage activity has been removed.",
+                        userHandle);
+            } catch (IllegalArgumentException ex) {
+                // Catch IllegalArgumentException to fix a crash (b/24679220) due to race condition
+                // in case this `revokeAllHealthPermissions()` method is called right after the
+                // client app is uninstalled.
+                Slog.e(TAG, "Revoking all health permissions failed", ex);
+            }
         }
     }
 
diff --git a/service/java/com/android/server/healthconnect/permission/UserGrantTimeState.java b/service/java/com/android/server/healthconnect/permission/UserGrantTimeState.java
index 5e0863d..e064b01 100644
--- a/service/java/com/android/server/healthconnect/permission/UserGrantTimeState.java
+++ b/service/java/com/android/server/healthconnect/permission/UserGrantTimeState.java
@@ -20,6 +20,8 @@
 import android.annotation.Nullable;
 import android.util.ArrayMap;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.time.Instant;
 import java.util.Map;
 
@@ -41,14 +43,15 @@
     /** The version of the grant times state. */
     private final int mVersion;
 
-    UserGrantTimeState(@NonNull int version) {
+    UserGrantTimeState(int version) {
         this(new ArrayMap<>(), new ArrayMap<>(), version);
     }
 
-    UserGrantTimeState(
+    @VisibleForTesting
+    public UserGrantTimeState(
             @NonNull Map<String, Instant> packagePermissions,
             @NonNull Map<String, Instant> sharedUserPermissions,
-            @NonNull int version) {
+            int version) {
         mPackagePermissions = packagePermissions;
         mSharedUserPermissions = sharedUserPermissions;
         mVersion = version;
diff --git a/service/java/com/android/server/healthconnect/storage/HealthConnectDatabase.java b/service/java/com/android/server/healthconnect/storage/HealthConnectDatabase.java
index e094299..a08c3de 100644
--- a/service/java/com/android/server/healthconnect/storage/HealthConnectDatabase.java
+++ b/service/java/com/android/server/healthconnect/storage/HealthConnectDatabase.java
@@ -55,12 +55,16 @@
     public static final int DB_VERSION_GENERATED_LOCAL_TIME = 10;
     private static final String TAG = "HealthConnectDatabase";
     private static final int DATABASE_VERSION = 10;
-    private static final String DATABASE_NAME = "healthconnect.db";
+    private static final String DEFAULT_DATABASE_NAME = "healthconnect.db";
     @NonNull private final Collection<RecordHelper<?>> mRecordHelpers;
     private final Context mContext;
 
     public HealthConnectDatabase(@NonNull Context context) {
-        super(context, DATABASE_NAME, null, DATABASE_VERSION);
+        this(context, DEFAULT_DATABASE_NAME);
+    }
+
+    public HealthConnectDatabase(@NonNull Context context, String databaseName) {
+        super(context, databaseName, null, DATABASE_VERSION);
         mRecordHelpers = RecordHelperProvider.getInstance().getRecordHelpers().values();
         mContext = context;
     }
@@ -98,7 +102,7 @@
     }
 
     public File getDatabasePath() {
-        return mContext.getDatabasePath(DATABASE_NAME);
+        return mContext.getDatabasePath(getDatabaseName());
     }
 
     private void dropAllTables(SQLiteDatabase db) {
@@ -146,8 +150,4 @@
         db.execSQL(createTableRequest.getCreateCommand());
         createTableRequest.getCreateIndexStatements().forEach(db::execSQL);
     }
-
-    public static String getName() {
-        return DATABASE_NAME;
-    }
 }
diff --git a/service/java/com/android/server/healthconnect/storage/TransactionManager.java b/service/java/com/android/server/healthconnect/storage/TransactionManager.java
index 8366a7e..5bcd364 100644
--- a/service/java/com/android/server/healthconnect/storage/TransactionManager.java
+++ b/service/java/com/android/server/healthconnect/storage/TransactionManager.java
@@ -21,9 +21,13 @@
 import static android.health.connect.Constants.PARENT_KEY;
 import static android.health.connect.HealthConnectException.ERROR_INTERNAL;
 
+import static com.android.internal.util.Preconditions.checkArgument;
 import static com.android.server.healthconnect.storage.datatypehelpers.RecordHelper.APP_INFO_ID_COLUMN_NAME;
 import static com.android.server.healthconnect.storage.datatypehelpers.RecordHelper.PRIMARY_COLUMN_NAME;
-import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorLong;
+
+import static com.google.common.collect.Iterables.getOnlyElement;
+
+import static java.util.Objects.requireNonNull;
 
 import android.annotation.NonNull;
 import android.content.Context;
@@ -52,12 +56,13 @@
 import com.android.server.healthconnect.storage.utils.RecordHelperProvider;
 import com.android.server.healthconnect.storage.utils.StorageUtils;
 
+import com.google.common.annotations.VisibleForTesting;
+
 import java.io.File;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.BiConsumer;
@@ -251,67 +256,72 @@
      * Reads the records {@link RecordInternal} stored in the HealthConnect database.
      *
      * @param request a read request.
+     * @throws IllegalArgumentException if the {@link ReadTransactionRequest} contains pagination
+     *     information, which should use {@link #readRecordsAndPageToken(ReadTransactionRequest)}
+     *     instead.
      * @return List of records read {@link RecordInternal} from table based on ids.
      */
-    public List<RecordInternal<?>> readRecords(@NonNull ReadTransactionRequest request)
+    public List<RecordInternal<?>> readRecordsByIds(@NonNull ReadTransactionRequest request)
             throws SQLiteException {
+        // TODO(b/308158714): Make this build time check once we have different classes.
+        checkArgument(
+                request.getPageToken() == null && request.getPageSize().isEmpty(),
+                "Expect read by id request, but request contains pagination info.");
         List<RecordInternal<?>> recordInternals = new ArrayList<>();
-        request.getReadRequests()
-                .forEach(
-                        (readTableRequest -> {
-                            if (readTableRequest.getRecordHelper().isRecordOperationsEnabled()) {
-                                try (Cursor cursor = read(readTableRequest)) {
-                                    Objects.requireNonNull(readTableRequest.getRecordHelper());
-                                    List<RecordInternal<?>> internalRecords =
-                                            readTableRequest
-                                                    .getRecordHelper()
-                                                    .getInternalRecords(cursor, DEFAULT_PAGE_SIZE);
-
-                                    populateInternalRecordsWithExtraData(
-                                            internalRecords, readTableRequest);
-
-                                    recordInternals.addAll(internalRecords);
-                                }
-                            }
-                        }));
+        for (ReadTableRequest readTableRequest : request.getReadRequests()) {
+            RecordHelper<?> helper = readTableRequest.getRecordHelper();
+            requireNonNull(helper);
+            if (helper.isRecordOperationsEnabled()) {
+                try (Cursor cursor = read(readTableRequest)) {
+                    List<RecordInternal<?>> internalRecords = helper.getInternalRecords(cursor);
+                    populateInternalRecordsWithExtraData(internalRecords, readTableRequest);
+                    recordInternals.addAll(internalRecords);
+                }
+            }
+        }
         return recordInternals;
     }
 
     /**
      * Reads the records {@link RecordInternal} stored in the HealthConnect database and returns the
-     * max row_id as next page token.
+     * next page token.
      *
-     * @param request a read request.
-     * @return Pair containing records list read {@link RecordInternal} from the table and a next
-     *     page token for pagination
+     * @param request a read request. Only one {@link ReadTableRequest} is expected in the {@link
+     *     ReadTransactionRequest request}.
+     * @throws IllegalArgumentException if the {@link ReadTransactionRequest} doesn't contain
+     *     pagination information, which should use {@link
+     *     #readRecordsByIds(ReadTransactionRequest)} instead.
+     * @return Pair containing records list read {@link RecordInternal} from the table and a page
+     *     token for pagination.
      */
-    public Pair<List<RecordInternal<?>>, Long> readRecordsAndGetNextToken(
+    public Pair<List<RecordInternal<?>>, Long> readRecordsAndPageToken(
             @NonNull ReadTransactionRequest request) throws SQLiteException {
-        // throw an exception if read requested is not for a single record type
-        // i.e. size of read table request is not equal to 1.
-        if (request.getReadRequests().size() != 1) {
-            throw new IllegalArgumentException("Read requested is not for a single record type");
-        }
+        // TODO(b/308158714): Make this build time check once we have different classes.
+        checkArgument(
+                request.getPageToken() != null && request.getPageSize().isPresent(),
+                "Expect read by filter request, but request doesn't contain pagination info.");
+        ReadTableRequest readTableRequest = getOnlyElement(request.getReadRequests());
         List<RecordInternal<?>> recordInternalList;
-        long token = DEFAULT_LONG;
-        ReadTableRequest readTableRequest = request.getReadRequests().get(0);
         RecordHelper<?> helper = readTableRequest.getRecordHelper();
-        Objects.requireNonNull(helper);
+        requireNonNull(helper);
         if (!helper.isRecordOperationsEnabled()) {
             recordInternalList = new ArrayList<>(0);
-            return Pair.create(recordInternalList, token);
+            return Pair.create(recordInternalList, DEFAULT_LONG);
         }
 
+        long pageToken;
         try (Cursor cursor = read(readTableRequest)) {
-            recordInternalList = helper.getInternalRecords(cursor, readTableRequest.getPageSize());
-            String startTimeColumnName = helper.getStartTimeColumnName();
-
+            Pair<List<RecordInternal<?>>, Long> readResult =
+                    helper.getNextInternalRecordsPageAndToken(
+                            cursor,
+                            request.getPageSize().orElse(DEFAULT_PAGE_SIZE),
+                            // pageToken is never null for read by filter requests
+                            requireNonNull(request.getPageToken()));
+            recordInternalList = readResult.first;
+            pageToken = readResult.second;
             populateInternalRecordsWithExtraData(recordInternalList, readTableRequest);
-            if (cursor.moveToNext()) {
-                token = getCursorLong(cursor, startTimeColumnName);
-            }
         }
-        return Pair.create(recordInternalList, token);
+        return Pair.create(recordInternalList, pageToken);
     }
 
     /**
@@ -393,7 +403,7 @@
      * @return Number of entries in the given table
      */
     public long getNumberOfEntriesInTheTable(@NonNull String tableName) {
-        Objects.requireNonNull(tableName);
+        requireNonNull(tableName);
         return DatabaseUtils.queryNumEntries(getReadableDb(), tableName);
     }
 
@@ -404,7 +414,7 @@
      * @return Size of the database
      */
     public long getDatabaseSize(@NonNull Context context) {
-        Objects.requireNonNull(context);
+        requireNonNull(context);
         return context.getDatabasePath(getReadableDb().getPath()).length();
     }
 
@@ -483,7 +493,7 @@
      * make sure that either all its operation succeed or fail in a single run.
      */
     public void deleteWithoutChangeLogs(@NonNull List<DeleteTableRequest> deleteTableRequests) {
-        Objects.requireNonNull(deleteTableRequests);
+        requireNonNull(deleteTableRequests);
         final SQLiteDatabase db = getWritableDb();
         db.beginTransaction();
         try {
@@ -751,11 +761,17 @@
 
     @NonNull
     public static TransactionManager getInitialisedInstance() {
-        Objects.requireNonNull(sTransactionManager);
+        requireNonNull(sTransactionManager);
 
         return sTransactionManager;
     }
 
+    /** Clear the static instance held in memory, so unit tests can perform correctly. */
+    @VisibleForTesting
+    public static void clearInstance() {
+        sTransactionManager = null;
+    }
+
     @NonNull
     public UserHandle getCurrentUserHandle() {
         return mUserHandle;
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 7ea56a0..8a104a2 100644
--- a/service/java/com/android/server/healthconnect/storage/datatypehelpers/AccessLogsHelper.java
+++ b/service/java/com/android/server/healthconnect/storage/datatypehelpers/AccessLogsHelper.java
@@ -39,6 +39,7 @@
 import com.android.server.healthconnect.storage.request.DeleteTableRequest;
 import com.android.server.healthconnect.storage.request.ReadTableRequest;
 import com.android.server.healthconnect.storage.request.UpsertTableRequest;
+import com.android.server.healthconnect.storage.utils.OrderByClause;
 
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
@@ -97,6 +98,30 @@
         return accessLogsList;
     }
 
+    /**
+     * Returns the timestamp of the latest access log and {@link Long.MIN_VALUE} if there is no
+     * access log.
+     */
+    public long getLatestAccessLogTimeStamp() {
+
+        final ReadTableRequest readTableRequest =
+                new ReadTableRequest(TABLE_NAME)
+                        .setOrderBy(
+                                new OrderByClause()
+                                        .addOrderByClause(ACCESS_TIME_COLUMN_NAME, false))
+                        .setLimit(1);
+
+        long mostRecentAccessTime = Long.MIN_VALUE;
+        final TransactionManager transactionManager = TransactionManager.getInitialisedInstance();
+        try (Cursor cursor = transactionManager.read(readTableRequest)) {
+            while (cursor.moveToNext()) {
+                long accessTime = getCursorLong(cursor, ACCESS_TIME_COLUMN_NAME);
+                mostRecentAccessTime = Math.max(mostRecentAccessTime, accessTime);
+            }
+        }
+        return mostRecentAccessTime;
+    }
+
     /** Adds an entry in to the access logs table for every insert or read operation request */
     public void addAccessLog(
             String packageName,
diff --git a/service/java/com/android/server/healthconnect/storage/datatypehelpers/ActivityDateHelper.java b/service/java/com/android/server/healthconnect/storage/datatypehelpers/ActivityDateHelper.java
index 92db3c4..71b75f4 100644
--- a/service/java/com/android/server/healthconnect/storage/datatypehelpers/ActivityDateHelper.java
+++ b/service/java/com/android/server/healthconnect/storage/datatypehelpers/ActivityDateHelper.java
@@ -19,6 +19,7 @@
 import static com.android.server.healthconnect.storage.utils.StorageUtils.INTEGER_NOT_NULL;
 import static com.android.server.healthconnect.storage.utils.StorageUtils.PRIMARY_AUTOINCREMENT;
 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorLong;
+import static com.android.server.healthconnect.storage.utils.WhereClauses.LogicalOperator.AND;
 
 import android.annotation.NonNull;
 import android.content.ContentValues;
@@ -98,7 +99,7 @@
         return readDates(
                 new ReadTableRequest(TABLE_NAME)
                         .setWhereClause(
-                                new WhereClauses()
+                                new WhereClauses(AND)
                                         .addWhereInIntsClause(
                                                 RECORD_TYPE_ID_COLUMN_NAME, recordTypeIds))
                         .setColumnNames(List.of(EPOCH_DAYS_COLUMN_NAME))
diff --git a/service/java/com/android/server/healthconnect/storage/datatypehelpers/AppInfoHelper.java b/service/java/com/android/server/healthconnect/storage/datatypehelpers/AppInfoHelper.java
index 912987a..25110ad 100644
--- a/service/java/com/android/server/healthconnect/storage/datatypehelpers/AppInfoHelper.java
+++ b/service/java/com/android/server/healthconnect/storage/datatypehelpers/AppInfoHelper.java
@@ -27,6 +27,7 @@
 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorBlob;
 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorLong;
 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorString;
+import static com.android.server.healthconnect.storage.utils.WhereClauses.LogicalOperator.AND;
 
 import static java.util.Objects.requireNonNull;
 
@@ -98,6 +99,7 @@
      * <p>TO HAVE THREAD SAFETY DON'T USE THESE VARIABLES DIRECTLY, INSTEAD USE ITS GETTER
      */
     private volatile ConcurrentHashMap<Long, String> mIdPackageNameMap;
+
     /**
      * Map to store application package-name -> AppInfo mapping (such as packageName -> appName,
      * icon, rowId in the DB etc.)
@@ -195,25 +197,13 @@
     }
 
     /**
-     * Populates record with package name
-     *
-     * @param appInfoId rowId from {@code application_info_table }
-     * @param record The record to be populated with package name
-     * @param idPackageNameMap the map from which to get the package name
-     */
-    public void populateRecordWithValue(
-            long appInfoId, @NonNull RecordInternal<?> record, Map<Long, String> idPackageNameMap) {
-        if (idPackageNameMap != null) {
-            record.setPackageName(idPackageNameMap.get(appInfoId));
-            return;
-        }
-        record.setPackageName(getIdPackageNameMap().get(appInfoId));
-    }
-
-    /**
      * @return id of {@code packageName} or {@link Constants#DEFAULT_LONG} if the id is not found
      */
     public long getAppInfoId(String packageName) {
+        if (packageName == null) {
+            return DEFAULT_LONG;
+        }
+
         AppInfoInternal appInfo = getAppInfoMap().getOrDefault(packageName, null);
 
         if (appInfo == null) {
@@ -258,7 +248,7 @@
         List<String> packageNames = new ArrayList<>();
         packageIds.forEach(
                 (packageId) -> {
-                    String packageName = getIdPackageNameMap().get(packageId);
+                    String packageName = getPackageName(packageId);
                     requireNonNull(packageName);
 
                     packageNames.add(packageName);
@@ -536,7 +526,7 @@
             Set<Integer> recordTypesUsed) {
         appInfo.setRecordTypesUsed(recordTypesUsed);
         // create upsert table request to modify app info table, keyed by packages name.
-        WhereClauses whereClauseForAppInfoTableUpdate = new WhereClauses();
+        WhereClauses whereClauseForAppInfoTableUpdate = new WhereClauses(AND);
         whereClauseForAppInfoTableUpdate.addWhereEqualsClause(
                 PACKAGE_COLUMN_NAME, appInfo.getPackageName());
         UpsertTableRequest upsertRequestForAppInfoUpdate =
diff --git a/service/java/com/android/server/healthconnect/storage/datatypehelpers/ChangeLogsHelper.java b/service/java/com/android/server/healthconnect/storage/datatypehelpers/ChangeLogsHelper.java
index 4407bde..330c361 100644
--- a/service/java/com/android/server/healthconnect/storage/datatypehelpers/ChangeLogsHelper.java
+++ b/service/java/com/android/server/healthconnect/storage/datatypehelpers/ChangeLogsHelper.java
@@ -27,6 +27,7 @@
 import static com.android.server.healthconnect.storage.utils.StorageUtils.PRIMARY_AUTOINCREMENT;
 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorInt;
 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorLong;
+import static com.android.server.healthconnect.storage.utils.WhereClauses.LogicalOperator.AND;
 
 import android.annotation.NonNull;
 import android.content.ContentValues;
@@ -101,7 +102,7 @@
             ChangeLogsRequest changeLogsRequest) {
         long token = changeLogTokenRequest.getRowIdChangeLogs();
         WhereClauses whereClause =
-                new WhereClauses()
+                new WhereClauses(AND)
                         .addWhereGreaterThanClause(PRIMARY_COLUMN_NAME, String.valueOf(token));
         if (!changeLogTokenRequest.getRecordTypes().isEmpty()) {
             whereClause.addWhereInIntsClause(
@@ -115,12 +116,12 @@
                             .getAppInfoIds(changeLogTokenRequest.getPackageNamesToFilter()));
         }
 
-        // In setLimit(pagesize) method size will be set to pageSize + 1,so that if number of
-        // records returned is more than pageSize we know there are more records available to return
-        // for the next read.
+        // We set limit size to requested pageSize plus extra 1 record so that if number of records
+        // queried is more than pageSize we know there are more records available to return for the
+        // next read.
         int pageSize = changeLogsRequest.getPageSize();
         final ReadTableRequest readTableRequest =
-                new ReadTableRequest(TABLE_NAME).setWhereClause(whereClause).setLimit(pageSize);
+                new ReadTableRequest(TABLE_NAME).setWhereClause(whereClause).setLimit(pageSize + 1);
 
         Map<Integer, ChangeLogs> operationToChangeLogMap = new ArrayMap<>();
         TransactionManager transactionManager = TransactionManager.getInitialisedInstance();
@@ -221,6 +222,7 @@
         @OperationType.OperationTypes private final int mOperationType;
         private final String mPackageName;
         private final long mChangeLogTimeStamp;
+
         /**
          * Creates a change logs object used to add a new change log for {@code operationType} for
          * {@code packageName} logged at time {@code timeStamp }
@@ -238,6 +240,7 @@
             mPackageName = packageName;
             mChangeLogTimeStamp = timeStamp;
         }
+
         /**
          * Creates a change logs object used to add a new change log for {@code operationType}
          * logged at time {@code timeStamp }
diff --git a/service/java/com/android/server/healthconnect/storage/datatypehelpers/ChangeLogsRequestHelper.java b/service/java/com/android/server/healthconnect/storage/datatypehelpers/ChangeLogsRequestHelper.java
index 24f573e..980c54d 100644
--- a/service/java/com/android/server/healthconnect/storage/datatypehelpers/ChangeLogsRequestHelper.java
+++ b/service/java/com/android/server/healthconnect/storage/datatypehelpers/ChangeLogsRequestHelper.java
@@ -26,6 +26,7 @@
 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorIntegerList;
 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorString;
 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorStringList;
+import static com.android.server.healthconnect.storage.utils.WhereClauses.LogicalOperator.AND;
 
 import android.annotation.NonNull;
 import android.content.ContentValues;
@@ -141,7 +142,7 @@
         ReadTableRequest readTableRequest =
                 new ReadTableRequest(TABLE_NAME)
                         .setWhereClause(
-                                new WhereClauses()
+                                new WhereClauses(AND)
                                         .addWhereEqualsClause(PRIMARY_COLUMN_NAME, token)
                                         .addWhereEqualsClause(
                                                 PACKAGE_NAME_COLUMN_NAME, packageName));
diff --git a/service/java/com/android/server/healthconnect/storage/datatypehelpers/DeriveBasalCaloriesBurnedHelper.java b/service/java/com/android/server/healthconnect/storage/datatypehelpers/DeriveBasalCaloriesBurnedHelper.java
index 463030f..015e945 100644
--- a/service/java/com/android/server/healthconnect/storage/datatypehelpers/DeriveBasalCaloriesBurnedHelper.java
+++ b/service/java/com/android/server/healthconnect/storage/datatypehelpers/DeriveBasalCaloriesBurnedHelper.java
@@ -24,6 +24,7 @@
 import static com.android.server.healthconnect.storage.datatypehelpers.LeanBodyMassRecordHelper.MASS_COLUMN_NAME;
 import static com.android.server.healthconnect.storage.datatypehelpers.WeightRecordHelper.WEIGHT_COLUMN_NAME;
 import static com.android.server.healthconnect.storage.datatypehelpers.WeightRecordHelper.WEIGHT_RECORD_TABLE_NAME;
+import static com.android.server.healthconnect.storage.utils.WhereClauses.LogicalOperator.AND;
 
 import android.annotation.NonNull;
 import android.database.Cursor;
@@ -148,10 +149,10 @@
                         new ReadTableRequest(BASAL_METABOLIC_RATE_RECORD_TABLE_NAME)
                                 .setColumnNames(List.of(BASAL_METABOLIC_RATE_COLUMN_NAME))
                                 .setWhereClause(
-                                        new WhereClauses()
+                                        new WhereClauses(AND)
                                                 .addWhereLessThanOrEqualClause(
                                                         mTimeColumnName, intervalStartTime))
-                                .setLimit(0)
+                                .setLimit(1)
                                 .setOrderBy(
                                         new OrderByClause()
                                                 .addOrderByClause(mTimeColumnName, false)))) {
@@ -214,6 +215,7 @@
         return (370 + 21.6 * (massInGms / GMS_IN_KG)) * KCAL_TO_CAL;
     }
 
+    // TODO(b/302521219): Restructure this derivation logic
     private double derivedBasalCaloriesBurnedFromProfile(
             long intervalStartTime, long intervalEndTime) {
         double caloriesFromProfile = 0;
@@ -233,8 +235,8 @@
             double height = DEFAULT_HEIGHT_IN_METERS;
             double weight = DEFAULT_WEIGHT_IN_GMS;
 
-            long heightTime = Integer.MAX_VALUE;
-            long weightTime = Integer.MAX_VALUE;
+            long heightTime = Long.MAX_VALUE;
+            long weightTime = Long.MAX_VALUE;
             while (hasHeight || hasWeight) {
                 if (hasHeight) {
                     heightTime = StorageUtils.getCursorLong(heightCursor, mTimeColumnName);
@@ -281,7 +283,12 @@
             if (lastTimeUsed < intervalEndTime) {
                 caloriesFromProfile +=
                         getCaloriesFromHeightAndWeight(
-                                height, weight, lastTimeUsed, intervalEndTime);
+                                height,
+                                weight,
+                                // Snap to startTime in case the last-used record is still before
+                                // the startTime of the interval
+                                Math.max(intervalStartTime, lastTimeUsed),
+                                intervalEndTime);
             }
         }
 
@@ -313,7 +320,7 @@
                 new ReadTableRequest(tableName)
                         .setColumnNames(List.of(colName, mTimeColumnName))
                         .setWhereClause(
-                                new WhereClauses()
+                                new WhereClauses(AND)
                                         .addWhereBetweenTimeClause(
                                                 mTimeColumnName,
                                                 intervalStartTime,
@@ -324,11 +331,11 @@
                                         new ReadTableRequest(tableName)
                                                 .setColumnNames(List.of(colName, mTimeColumnName))
                                                 .setWhereClause(
-                                                        new WhereClauses()
+                                                        new WhereClauses(AND)
                                                                 .addWhereLessThanOrEqualClause(
                                                                         mTimeColumnName,
                                                                         intervalStartTime))
-                                                .setLimit(0)
+                                                .setLimit(1)
                                                 .setOrderBy(
                                                         new OrderByClause()
                                                                 .addOrderByClause(
diff --git a/service/java/com/android/server/healthconnect/storage/datatypehelpers/DeriveTotalCaloriesBurnedHelper.java b/service/java/com/android/server/healthconnect/storage/datatypehelpers/DeriveTotalCaloriesBurnedHelper.java
index 8fa9c1f..6dabe58 100644
--- a/service/java/com/android/server/healthconnect/storage/datatypehelpers/DeriveTotalCaloriesBurnedHelper.java
+++ b/service/java/com/android/server/healthconnect/storage/datatypehelpers/DeriveTotalCaloriesBurnedHelper.java
@@ -24,6 +24,7 @@
 import static com.android.server.healthconnect.storage.datatypehelpers.InstantRecordHelper.TIME_COLUMN_NAME;
 import static com.android.server.healthconnect.storage.datatypehelpers.IntervalRecordHelper.LOCAL_DATE_TIME_START_TIME_COLUMN_NAME;
 import static com.android.server.healthconnect.storage.datatypehelpers.IntervalRecordHelper.START_TIME_COLUMN_NAME;
+import static com.android.server.healthconnect.storage.utils.WhereClauses.LogicalOperator.AND;
 
 import android.annotation.NonNull;
 import android.database.Cursor;
@@ -83,7 +84,7 @@
                 transactionManager.read(
                         new ReadTableRequest(ACTIVE_CALORIES_BURNED_RECORD_TABLE_NAME)
                                 .setWhereClause(
-                                        new WhereClauses()
+                                        new WhereClauses(AND)
                                                 .addWhereBetweenTimeClause(
                                                         mIntervalStartTimeColumnName,
                                                         mStartTime,
@@ -96,7 +97,7 @@
                 transactionManager.read(
                         new ReadTableRequest(BASAL_METABOLIC_RATE_RECORD_TABLE_NAME)
                                 .setWhereClause(
-                                        new WhereClauses()
+                                        new WhereClauses(AND)
                                                 .addWhereBetweenTimeClause(
                                                         mInstantRecordTimeColumnName,
                                                         mStartTime,
diff --git a/service/java/com/android/server/healthconnect/storage/datatypehelpers/ExerciseSegmentRecordHelper.java b/service/java/com/android/server/healthconnect/storage/datatypehelpers/ExerciseSegmentRecordHelper.java
index 46cbad6..11bb4d0 100644
--- a/service/java/com/android/server/healthconnect/storage/datatypehelpers/ExerciseSegmentRecordHelper.java
+++ b/service/java/com/android/server/healthconnect/storage/datatypehelpers/ExerciseSegmentRecordHelper.java
@@ -24,6 +24,7 @@
 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorInt;
 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorLong;
 import static com.android.server.healthconnect.storage.utils.StorageUtils.isNullValue;
+import static com.android.server.healthconnect.storage.utils.WhereClauses.LogicalOperator.AND;
 
 import android.annotation.NonNull;
 import android.content.ContentValues;
@@ -129,7 +130,7 @@
     /** Returns sql join needed for calculating exercise sessions duration */
     public static SqlJoin getJoinForDurationAggregation(String parentTableName) {
         SqlJoin join = getJoinReadRequest(parentTableName);
-        WhereClauses filterPauses = new WhereClauses();
+        WhereClauses filterPauses = new WhereClauses(AND);
         filterPauses.addWhereInIntsClause(EXERCISE_SEGMENT_TYPE, DURATION_EXCLUDE_TYPES);
         join.setSecondTableWhereClause(filterPauses);
         return join;
diff --git a/service/java/com/android/server/healthconnect/storage/datatypehelpers/ExerciseSessionRecordHelper.java b/service/java/com/android/server/healthconnect/storage/datatypehelpers/ExerciseSessionRecordHelper.java
index 0c2516a..6285309 100644
--- a/service/java/com/android/server/healthconnect/storage/datatypehelpers/ExerciseSessionRecordHelper.java
+++ b/service/java/com/android/server/healthconnect/storage/datatypehelpers/ExerciseSessionRecordHelper.java
@@ -34,6 +34,7 @@
 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorString;
 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorUUID;
 import static com.android.server.healthconnect.storage.utils.StorageUtils.getIntegerAndConvertToBoolean;
+import static com.android.server.healthconnect.storage.utils.WhereClauses.LogicalOperator.AND;
 
 import android.annotation.NonNull;
 import android.content.ContentValues;
@@ -90,6 +91,10 @@
     private static final String TITLE_COLUMN_NAME = "title";
     private static final String HAS_ROUTE_COLUMN_NAME = "has_route";
 
+    private static final int ROUTE_READ_ACCESS_TYPE_NONE = 0;
+    private static final int ROUTE_READ_ACCESS_TYPE_OWN = 1;
+    private static final int ROUTE_READ_ACCESS_TYPE_ALL = 2;
+
     public ExerciseSessionRecordHelper() {
         super(RecordTypeIdentifier.RECORD_TYPE_EXERCISE_SESSION);
     }
@@ -245,25 +250,17 @@
             String packageName,
             long startDateAccess,
             Map<String, Boolean> extraPermsState) {
-        if (!isExerciseRouteFeatureEnabled()) {
+        int routeAccessType = getExerciseRouteReadAccessType(packageName, extraPermsState);
+
+        if (routeAccessType == ROUTE_READ_ACCESS_TYPE_NONE) {
             return Collections.emptyList();
         }
 
-        boolean canReadAnyRoute = extraPermsState.get(READ_EXERCISE_ROUTE);
-        if (!canReadAnyRoute
-                && AppInfoHelper.getInstance().getAppInfoId(packageName) == DEFAULT_LONG) {
-            // If the package doesn't have app info and cannot read any route,
-            // then no route is accessible for it.
-            return Collections.emptyList();
-        }
+        boolean enforceSelfRead = routeAccessType == ROUTE_READ_ACCESS_TYPE_OWN;
 
-        WhereClauses whereClause =
-                getReadTableWhereClause(
-                        request,
-                        packageName,
-                        /* enforceSelfRead= */ !canReadAnyRoute,
-                        startDateAccess);
-        return List.of(getRouteReadRequest(whereClause));
+        WhereClauses sessionsWithAccessibleRouteClause =
+                getReadTableWhereClause(request, packageName, enforceSelfRead, startDateAccess);
+        return List.of(getRouteReadRequest(sessionsWithAccessibleRouteClause));
     }
 
     @Override
@@ -315,17 +312,30 @@
     }
 
     @Override
-    List<ReadTableRequest> getExtraDataReadRequests(List<UUID> uuids, long startDateAccess) {
-        if (!isExerciseRouteFeatureEnabled()) {
+    List<ReadTableRequest> getExtraDataReadRequests(
+            String packageName,
+            List<UUID> uuids,
+            long startDateAccess,
+            Map<String, Boolean> extraPermsState) {
+        int routeAccessType = getExerciseRouteReadAccessType(packageName, extraPermsState);
+
+        if (routeAccessType == ROUTE_READ_ACCESS_TYPE_NONE) {
             return Collections.emptyList();
         }
 
-        WhereClauses whereClause =
-                new WhereClauses()
+        WhereClauses sessionsWithAccessibleRouteClause =
+                new WhereClauses(AND)
                         .addWhereInClauseWithoutQuotes(
-                                UUID_COLUMN_NAME, StorageUtils.getListOfHexString(uuids));
-        whereClause.addWhereLaterThanTimeClause(getStartTimeColumnName(), startDateAccess);
-        return List.of(getRouteReadRequest(whereClause));
+                                UUID_COLUMN_NAME, StorageUtils.getListOfHexString(uuids))
+                        .addWhereLaterThanTimeClause(getStartTimeColumnName(), startDateAccess);
+
+        if (routeAccessType == ROUTE_READ_ACCESS_TYPE_OWN) {
+            long appId = AppInfoHelper.getInstance().getAppInfoId(packageName);
+            sessionsWithAccessibleRouteClause.addWhereInLongsClause(
+                    APP_INFO_ID_COLUMN_NAME, List.of(appId));
+        }
+
+        return List.of(getRouteReadRequest(sessionsWithAccessibleRouteClause));
     }
 
     @Override
@@ -403,9 +413,23 @@
         sessionsIdsRequest.setColumnNames(List.of(PRIMARY_COLUMN_NAME));
         sessionsIdsRequest.setWhereClause(clauseToFilterSessionIds);
 
-        WhereClauses inClause = new WhereClauses();
+        WhereClauses inClause = new WhereClauses(AND);
         inClause.addWhereInSQLRequestClause(PARENT_KEY_COLUMN_NAME, sessionsIdsRequest);
         routeReadRequest.setWhereClause(inClause);
         return routeReadRequest;
     }
+
+    private int getExerciseRouteReadAccessType(
+            String packageName, Map<String, Boolean> extraPermsState) {
+        if (!isExerciseRouteFeatureEnabled()) {
+            return ROUTE_READ_ACCESS_TYPE_NONE;
+        }
+
+        if (extraPermsState.getOrDefault(READ_EXERCISE_ROUTE, false)) {
+            return ROUTE_READ_ACCESS_TYPE_ALL;
+        }
+
+        long appId = AppInfoHelper.getInstance().getAppInfoId(packageName);
+        return appId == DEFAULT_LONG ? ROUTE_READ_ACCESS_TYPE_NONE : ROUTE_READ_ACCESS_TYPE_OWN;
+    }
 }
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 f40def4..a2cea14 100644
--- a/service/java/com/android/server/healthconnect/storage/datatypehelpers/HealthDataCategoryPriorityHelper.java
+++ b/service/java/com/android/server/healthconnect/storage/datatypehelpers/HealthDataCategoryPriorityHelper.java
@@ -16,6 +16,10 @@
 
 package com.android.server.healthconnect.storage.datatypehelpers;
 
+import static android.health.connect.HealthPermissions.getDataCategoriesWithWritePermissionsForPackage;
+import static android.health.connect.HealthPermissions.getPackageHasWriteHealthPermissionsForCategory;
+import static android.health.connect.internal.datatypes.utils.RecordTypeRecordCategoryMapper.getRecordCategoryForRecordType;
+
 import static com.android.server.healthconnect.storage.request.UpsertTableRequest.TYPE_STRING;
 import static com.android.server.healthconnect.storage.utils.StorageUtils.DELIMITER;
 import static com.android.server.healthconnect.storage.utils.StorageUtils.INTEGER_UNIQUE;
@@ -28,14 +32,14 @@
 import android.content.pm.PackageInfo;
 import android.content.res.Resources;
 import android.database.Cursor;
-import android.health.connect.HealthConnectManager;
 import android.health.connect.HealthDataCategory;
 import android.health.connect.HealthPermissions;
 import android.os.UserHandle;
-import android.util.ArraySet;
 import android.util.Pair;
 import android.util.Slog;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.healthconnect.HealthConnectDeviceConfigManager;
 import com.android.server.healthconnect.permission.HealthConnectPermissionHelper;
 import com.android.server.healthconnect.permission.PackageInfoUtils;
 import com.android.server.healthconnect.storage.TransactionManager;
@@ -47,6 +51,7 @@
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -69,6 +74,9 @@
     private static final String TAG = "HealthConnectPrioHelper";
     private static final String DEFAULT_APP_RESOURCE_NAME =
             "android:string/config_defaultHealthConnectApp";
+
+    public static final String INACTIVE_APPS_ADDED = "inactive_apps_added";
+
     private static volatile HealthDataCategoryPriorityHelper sHealthDataCategoryPriorityHelper;
 
     /**
@@ -88,10 +96,18 @@
         return new CreateTableRequest(TABLE_NAME, getColumnInfo());
     }
 
+    /**
+     * Appends a packageName to the priority list for this category when an app gets write
+     * permissions or during the one-time operation to add inactive apps.
+     *
+     * <p>Inactive apps are added at the bottom of the priority list even if they are the default
+     * app.
+     */
     public synchronized void appendToPriorityList(
             @NonNull String packageName,
             @HealthDataCategory.Type int dataCategory,
-            Context context) {
+            Context context,
+            boolean isInactiveApp) {
         List<Long> newPriorityOrder;
         getHealthDataCategoryToAppIdPriorityMap().putIfAbsent(dataCategory, new ArrayList<>());
         long appInfoId = AppInfoHelper.getInstance().getOrInsertAppInfoId(packageName, context);
@@ -101,12 +117,7 @@
         newPriorityOrder =
                 new ArrayList<>(getHealthDataCategoryToAppIdPriorityMap().get(dataCategory));
 
-        String defaultApp =
-                context.getResources()
-                        .getString(
-                                Resources.getSystem()
-                                        .getIdentifier(DEFAULT_APP_RESOURCE_NAME, null, null));
-        if (Objects.equals(packageName, defaultApp)) {
+        if (isDefaultApp(packageName, context) && !isInactiveApp) {
             newPriorityOrder.add(0, appInfoId);
         } else {
             newPriorityOrder.add(appInfoId);
@@ -120,7 +131,23 @@
                 newPriorityOrder);
     }
 
-    public synchronized void removeFromPriorityList(
+    @VisibleForTesting
+    boolean isDefaultApp(@NonNull String packageName, @NonNull Context context) {
+        String defaultApp =
+                context.getResources()
+                        .getString(
+                                Resources.getSystem()
+                                        .getIdentifier(DEFAULT_APP_RESOURCE_NAME, null, null));
+
+        return Objects.equals(packageName, defaultApp);
+    }
+
+    /**
+     * Removes a packageName from the priority list of a particular category if the package name
+     * does not have any granted write permissions. In the new aggregation source control, the
+     * package name is not removed if it has data in this category.
+     */
+    public synchronized void maybeRemoveAppFromPriorityList(
             @NonNull String packageName,
             @HealthDataCategory.Type int dataCategory,
             HealthConnectPermissionHelper permissionHelper,
@@ -133,9 +160,17 @@
                 return;
             }
         }
-        removeFromPriorityListInternal(dataCategory, packageName);
+
+        maybeRemoveAppFromPriorityListInternal(dataCategory, packageName);
     }
 
+    /**
+     * Removes apps from the priority list if they no longer hold write permissions to the category
+     * and have no data for that category.
+     *
+     * <p>If the new aggregation source control flag is off, apps that don't have write permissions
+     * are removed regardless of whether they hold data in that category.
+     */
     public synchronized void updateHealthDataPriority(
             @NonNull String[] packageNames, @NonNull UserHandle user, @NonNull Context context) {
         Objects.requireNonNull(packageNames);
@@ -146,25 +181,41 @@
             PackageInfo packageInfo =
                     packageInfoUtils.getPackageInfoWithPermissionsAsUser(
                             packageName, user, context);
-            if (packageInfoUtils.anyRequestedHealthPermissionGranted(context, packageInfo)) {
-                removeFromPriorityListIfNeeded(packageInfo, context);
-            } else {
-                removeAppFromPriorityList(packageName);
+
+            Set<Integer> dataCategoriesWithWritePermission =
+                    getDataCategoriesWithWritePermissionsForPackage(packageInfo, context);
+
+            for (int category : getHealthDataCategoryToAppIdPriorityMap().keySet()) {
+                if (!dataCategoriesWithWritePermission.contains(category)) {
+                    maybeRemoveAppFromPriorityListInternal(category, packageInfo.packageName);
+                }
             }
         }
     }
 
-    /** Removes app from priorityList for all HealthData Categories if the package is uninstalled */
-    public synchronized void removeAppFromPriorityList(@NonNull String packageName) {
+    /**
+     * Removes app from priorityList for all HealthData Categories if the package is uninstalled or
+     * if it has no health permissions. In the new aggregation source behaviour, the package name is
+     * not removed if it still has health data in a category.
+     */
+    public synchronized void maybeRemoveAppWithoutWritePermissionsFromPriorityList(
+            @NonNull String packageName) {
         Objects.requireNonNull(packageName);
         for (Integer dataCategory : getHealthDataCategoryToAppIdPriorityMap().keySet()) {
-            removeFromPriorityListInternal(dataCategory, packageName);
+            maybeRemoveAppFromPriorityListInternal(dataCategory, packageName);
         }
     }
 
     /** Returns list of package names based on priority for the input {@link HealthDataCategory} */
     @NonNull
-    public List<String> getPriorityOrder(@HealthDataCategory.Type int type) {
+    public List<String> getPriorityOrder(
+            @HealthDataCategory.Type int type, @NonNull Context context) {
+        boolean newAggregationSourceControl =
+                HealthConnectDeviceConfigManager.getInitialisedInstance()
+                        .isAggregationSourceControlsEnabled();
+        if (newAggregationSourceControl) {
+            reSyncHealthDataPriorityTable(context);
+        }
         return AppInfoHelper.getInstance().getPackageNames(getAppIdPriorityOrder(type));
     }
 
@@ -179,12 +230,43 @@
         return packageIds;
     }
 
+    /**
+     * Sets a new priority order for the given category, and allows adding and removing packages
+     * from the priority list.
+     *
+     * <p>In the old behaviour it is not allowed to add or remove packages so the new priority order
+     * needs to be sanitised before applying the operation.
+     */
     public void setPriorityOrder(int dataCategory, @NonNull List<String> packagePriorityOrder) {
+        boolean newAggregationSourceControl =
+                HealthConnectDeviceConfigManager.getInitialisedInstance()
+                        .isAggregationSourceControlsEnabled();
+
+        List<Long> newPriorityOrder =
+                AppInfoHelper.getInstance().getAppInfoIds(packagePriorityOrder);
+
+        if (!newAggregationSourceControl) {
+            newPriorityOrder = sanitizePriorityOder(dataCategory, newPriorityOrder);
+        }
+
+        safelyUpdateDBAndUpdateCache(
+                new UpsertTableRequest(
+                        TABLE_NAME,
+                        getContentValuesFor(dataCategory, newPriorityOrder),
+                        UNIQUE_COLUMN_INFO),
+                dataCategory,
+                newPriorityOrder);
+    }
+
+    /**
+     * Sanitizes the new priority order by ensuring it contains the same elements as the old
+     * priority order, for the old behaviour of aggregation source control.
+     */
+    private List<Long> sanitizePriorityOder(int dataCategory, List<Long> newPriorityOrder) {
+
         List<Long> currentPriorityOrder =
                 getHealthDataCategoryToAppIdPriorityMap()
                         .getOrDefault(dataCategory, Collections.emptyList());
-        List<Long> newPriorityOrder =
-                AppInfoHelper.getInstance().getAppInfoIds(packagePriorityOrder);
 
         // Remove appId from the priority order if it is not part of the current priority order,
         // this is because in the time app tried to update the order an app permission might
@@ -196,13 +278,7 @@
         newPriorityOrder.addAll(currentPriorityOrder);
         newPriorityOrder = newPriorityOrder.stream().distinct().collect(Collectors.toList());
 
-        safelyUpdateDBAndUpdateCache(
-                new UpsertTableRequest(
-                        TABLE_NAME,
-                        getContentValuesFor(dataCategory, newPriorityOrder),
-                        UNIQUE_COLUMN_INFO),
-                dataCategory,
-                newPriorityOrder);
+        return newPriorityOrder;
     }
 
     @Override
@@ -318,80 +394,82 @@
         return sHealthDataCategoryPriorityHelper;
     }
 
-    /** Syncs priority table with the permissions */
+    /** Syncs priority table with the permissions and data. */
     public synchronized void reSyncHealthDataPriorityTable(@NonNull Context context) {
         Objects.requireNonNull(context);
+        boolean newAggregationSourceControl =
+                HealthConnectDeviceConfigManager.getInitialisedInstance()
+                        .isAggregationSourceControlsEnabled();
+        // Candidates to be added to the priority list
         Map<Integer, Set<Long>> dataCategoryToAppIdMapHavingPermission =
                 getHealthDataCategoryToAppIdPriorityMap().entrySet().stream()
                         .collect(
                                 Collectors.toMap(
                                         Map.Entry::getKey, e -> new HashSet<>(e.getValue())));
+        // Candidates to be removed from the priority list
         Map<Integer, Set<Long>> dataCategoryToAppIdMapWithoutPermission =
                 getHealthDataCategoryToAppIdPriorityMap().entrySet().stream()
                         .collect(
                                 Collectors.toMap(
                                         Map.Entry::getKey, e -> new HashSet<>(e.getValue())));
-        UserHandle user = TransactionManager.getInitialisedInstance().getCurrentUserHandle();
-        Context currentUserContext = context.createContextAsUser(user, /*flags*/ 0);
-        List<PackageInfo> validHealthApps =
-                PackageInfoUtils.getInstance()
-                        .getPackagesHoldingHealthPermissions(user, currentUserContext);
+
+        List<PackageInfo> validHealthApps = getValidHealthApps(context);
         AppInfoHelper appInfoHelper = AppInfoHelper.getInstance();
         for (PackageInfo packageInfo : validHealthApps) {
-            long appInfoId = appInfoHelper.getAppInfoId(packageInfo.packageName);
-            for (int i = 0; i < packageInfo.requestedPermissions.length; i++) {
-                String currPerm = packageInfo.requestedPermissions[i];
-                if (HealthConnectManager.isHealthPermission(currentUserContext, currPerm)
-                        && ((packageInfo.requestedPermissionsFlags[i]
-                                        & PackageInfo.REQUESTED_PERMISSION_GRANTED)
-                                != 0)) {
-                    int dataCategory = HealthPermissions.getHealthDataCategory(currPerm);
-                    if (dataCategory != -1) {
-                        Set<Long> appIdsHavingPermission =
-                                dataCategoryToAppIdMapHavingPermission.getOrDefault(
-                                        dataCategory, new HashSet<>());
-                        if (appIdsHavingPermission.add(appInfoId)) {
-                            dataCategoryToAppIdMapHavingPermission.put(
-                                    dataCategory, appIdsHavingPermission);
-                        }
-                        Set<Long> appIdsWithoutPermission =
-                                dataCategoryToAppIdMapWithoutPermission.get(dataCategory);
-                        if (appIdsWithoutPermission.remove(appInfoId)) {
-                            dataCategoryToAppIdMapWithoutPermission.put(
-                                    dataCategory, appIdsWithoutPermission);
-                        }
-                    }
+            Set<Integer> dataCategoriesWithWritePermissionsForThisPackage =
+                    getDataCategoriesWithWritePermissionsForPackage(packageInfo, context);
+            long appInfoId = appInfoHelper.getOrInsertAppInfoId(packageInfo.packageName, context);
+
+            for (int dataCategory : dataCategoriesWithWritePermissionsForThisPackage) {
+                Set<Long> appIdsHavingPermission =
+                        dataCategoryToAppIdMapHavingPermission.getOrDefault(
+                                dataCategory, new HashSet<>());
+                if (appIdsHavingPermission.add(appInfoId)) {
+                    dataCategoryToAppIdMapHavingPermission.put(
+                            dataCategory, appIdsHavingPermission);
+                }
+
+                Set<Long> appIdsWithoutPermission =
+                        dataCategoryToAppIdMapWithoutPermission.getOrDefault(
+                                dataCategory, new HashSet<>());
+                if (appIdsWithoutPermission.remove(appInfoId)) {
+                    dataCategoryToAppIdMapWithoutPermission.put(
+                            dataCategory, appIdsWithoutPermission);
                 }
             }
         }
-        updateTableWithNewPriorityList(dataCategoryToAppIdMapHavingPermission);
-        removeAppsWithoutPermission(dataCategoryToAppIdMapWithoutPermission);
+
+        // The new behaviour does not automatically add to the priority list if there is
+        // a write permission for a package name
+        if (!newAggregationSourceControl) {
+            updateTableWithNewPriorityList(dataCategoryToAppIdMapHavingPermission);
+        }
+        maybeRemoveAppsFromPriorityList(dataCategoryToAppIdMapWithoutPermission);
     }
 
-    private synchronized void removeFromPriorityListIfNeeded(
-            @NonNull PackageInfo packageInfo, @NonNull Context context) {
-        Set<Integer> dataCategoryWithPermission = new ArraySet<>();
-        for (int i = 0; i < packageInfo.requestedPermissions.length; i++) {
-            String currPerm = packageInfo.requestedPermissions[i];
-            if (HealthConnectManager.isHealthPermission(context, currPerm)
-                    && ((packageInfo.requestedPermissionsFlags[i]
-                                    & PackageInfo.REQUESTED_PERMISSION_GRANTED)
-                            != 0)) {
-                int dataCategory = HealthPermissions.getHealthDataCategory(currPerm);
-                if (dataCategory != -1) {
-                    dataCategoryWithPermission.add(dataCategory);
-                }
-            }
-        }
-        for (int category : getHealthDataCategoryToAppIdPriorityMap().keySet()) {
-            if (!dataCategoryWithPermission.contains(category)) {
-                removeFromPriorityListInternal(category, packageInfo.packageName);
-            }
-        }
+    /** Returns a list of PackageInfos holding health permissions for this user. */
+    private List<PackageInfo> getValidHealthApps(@NonNull Context context) {
+        UserHandle user = TransactionManager.getInitialisedInstance().getCurrentUserHandle();
+        Context currentUserContext = context.createContextAsUser(user, /*flags*/ 0);
+        return PackageInfoUtils.getInstance()
+                .getPackagesHoldingHealthPermissions(user, currentUserContext);
     }
 
-    private synchronized void removeFromPriorityListInternal(
-            int dataCategory, @NonNull String packageName) {
+    /**
+     * Removes a packageName from the priority list of a category. For the new aggregation source
+     * control, the package name is not removed if it has data in that category.
+     */
+    private synchronized void maybeRemoveAppFromPriorityListInternal(
+            @HealthDataCategory.Type int dataCategory, @NonNull String packageName) {
+        boolean newAggregationSourceControl =
+                HealthConnectDeviceConfigManager.getInitialisedInstance()
+                        .isAggregationSourceControlsEnabled();
+        boolean dataExistsForPackageName = appHasDataInCategory(packageName, dataCategory);
+        if (newAggregationSourceControl && dataExistsForPackageName) {
+            // Do not remove if data exists for packageName in the new aggregation
+            return;
+        }
+
         List<Long> newPriorityList =
                 new ArrayList<>(
                         getHealthDataCategoryToAppIdPriorityMap()
@@ -418,11 +496,16 @@
                 newPriorityList);
     }
 
-    private synchronized void removeAppsWithoutPermission(
-            Map<Integer, Set<Long>> healthDataCategoryToAppIdPriorityMap) {
-        for (int dataCategory : healthDataCategoryToAppIdPriorityMap.keySet()) {
-            for (Long appInfoId : healthDataCategoryToAppIdPriorityMap.get(dataCategory)) {
-                removeFromPriorityListInternal(
+    /**
+     * Removes apps without permissions for these categories from the priority list. In the new
+     * aggregation source control, the packages are not removed if they still have data in these
+     * categories.
+     */
+    private synchronized void maybeRemoveAppsFromPriorityList(
+            Map<Integer, Set<Long>> dataCategoryToAppIdsWithoutPermissions) {
+        for (int dataCategory : dataCategoryToAppIdsWithoutPermissions.keySet()) {
+            for (Long appInfoId : dataCategoryToAppIdsWithoutPermissions.get(dataCategory)) {
+                maybeRemoveAppFromPriorityListInternal(
                         dataCategory, AppInfoHelper.getInstance().getPackageName(appInfoId));
             }
         }
@@ -445,4 +528,154 @@
             }
         }
     }
+
+    /**
+     * A one-time operation which adds inactive apps (without permissions but with data) to the
+     * priority list if the new aggregation source controls are available.
+     *
+     * <p>The inactive apps are added in ascending order of their package names.
+     */
+    public void maybeAddInactiveAppsToPriorityList(Context context) {
+        if (!shouldAddInactiveApps()) {
+            return;
+        }
+
+        Map<Integer, Set<String>> inactiveApps = getAllInactiveApps(context);
+
+        for (Map.Entry<Integer, Set<String>> entry : inactiveApps.entrySet()) {
+            int category = entry.getKey();
+            entry.getValue().stream()
+                    .sorted()
+                    .forEach(
+                            packageName ->
+                                    appendToPriorityList(
+                                            packageName,
+                                            category,
+                                            context,
+                                            /* isInactiveApp= */ true));
+        }
+
+        PreferenceHelper.getInstance()
+                .insertOrReplacePreference(INACTIVE_APPS_ADDED, String.valueOf(true));
+    }
+
+    private boolean shouldAddInactiveApps() {
+        boolean newAggregationSourceControl =
+                HealthConnectDeviceConfigManager.getInitialisedInstance()
+                        .isAggregationSourceControlsEnabled();
+
+        if (!newAggregationSourceControl) {
+            return false;
+        }
+
+        String haveInactiveAppsBeenAddedString =
+                PreferenceHelper.getInstance().getPreference(INACTIVE_APPS_ADDED);
+
+        // No-op if this operation has already been completed
+        if (haveInactiveAppsBeenAddedString != null
+                && Boolean.parseBoolean(haveInactiveAppsBeenAddedString)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @VisibleForTesting
+    boolean appHasDataInCategory(String packageName, int category) {
+        return getDataCategoriesWithDataForPackage(packageName).contains(category);
+    }
+
+    @VisibleForTesting
+    Set<Integer> getDataCategoriesWithDataForPackage(String packageName) {
+        Map<Integer, Set<String>> recordTypeToContributingPackages =
+                AppInfoHelper.getInstance().getRecordTypesToContributingPackagesMap();
+        Set<Integer> dataCategoriesWithData = new HashSet<>();
+
+        for (Map.Entry<Integer, Set<String>> entry : recordTypeToContributingPackages.entrySet()) {
+            Integer recordType = entry.getKey();
+            Set<String> contributingPackages = entry.getValue();
+            int recordCategory = getRecordCategoryForRecordType(recordType);
+            boolean isPackageNameContributor = contributingPackages.contains(packageName);
+            if (isPackageNameContributor) {
+                dataCategoriesWithData.add(recordCategory);
+            }
+        }
+        return dataCategoriesWithData;
+    }
+
+    /**
+     * Returns a set of contributing apps for each dataCategory. If a dataCategory does not have any
+     * data it will not be present in the map.
+     */
+    @VisibleForTesting
+    Map<Integer, Set<String>> getAllContributorApps() {
+        Map<Integer, Set<String>> recordTypeToContributingPackages =
+                AppInfoHelper.getInstance().getRecordTypesToContributingPackagesMap();
+
+        Map<Integer, Set<String>> allContributorApps = new HashMap<>();
+
+        for (Map.Entry<Integer, Set<String>> entry : recordTypeToContributingPackages.entrySet()) {
+            int recordCategory = getRecordCategoryForRecordType(entry.getKey());
+            Set<String> contributingPackages = entry.getValue();
+
+            Set<String> currentPackages =
+                    allContributorApps.getOrDefault(recordCategory, new HashSet<>());
+            currentPackages.addAll(contributingPackages);
+            allContributorApps.put(recordCategory, currentPackages);
+        }
+
+        return allContributorApps;
+    }
+
+    /**
+     * Returns a map of dataCategory to sets of packageNames that are inactive.
+     *
+     * <p>An inactive app is one that has data for the dataCategory but no write permissions.
+     */
+    @VisibleForTesting
+    Map<Integer, Set<String>> getAllInactiveApps(Context context) {
+        Map<Integer, Set<String>> allContributorApps = getAllContributorApps();
+        Map<Integer, Set<String>> inactiveApps = new HashMap<>();
+
+        for (Map.Entry<Integer, Set<String>> entry : allContributorApps.entrySet()) {
+            int category = entry.getKey();
+            Set<String> contributorApps = entry.getValue();
+
+            for (String app : contributorApps) {
+                if (!appHasWriteHealthPermissionsForCategory(app, category, context)) {
+                    Set<String> currentPackages =
+                            inactiveApps.getOrDefault(category, new HashSet<>());
+                    if (currentPackages.add(app)) {
+                        inactiveApps.put(category, currentPackages);
+                    }
+                }
+            }
+        }
+
+        return inactiveApps;
+    }
+
+    /**
+     * Returns true if this packageName has at least one granted WRITE permission for this
+     * dataCategory.
+     */
+    @VisibleForTesting
+    boolean appHasWriteHealthPermissionsForCategory(
+            @NonNull String packageName,
+            @HealthDataCategory.Type int dataCategory,
+            @NonNull Context context) {
+
+        List<PackageInfo> validHealthApps = getValidHealthApps(context);
+
+        for (PackageInfo validHealthApp : validHealthApps) {
+            if (Objects.equals(validHealthApp.packageName, packageName)) {
+                if (getPackageHasWriteHealthPermissionsForCategory(
+                        validHealthApp, dataCategory, context)) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
 }
diff --git a/service/java/com/android/server/healthconnect/storage/datatypehelpers/MergeDataHelper.java b/service/java/com/android/server/healthconnect/storage/datatypehelpers/MergeDataHelper.java
index dd36b52..36913c1 100644
--- a/service/java/com/android/server/healthconnect/storage/datatypehelpers/MergeDataHelper.java
+++ b/service/java/com/android/server/healthconnect/storage/datatypehelpers/MergeDataHelper.java
@@ -29,6 +29,7 @@
 import android.database.Cursor;
 import android.util.Pair;
 
+import com.android.server.healthconnect.HealthConnectDeviceConfigManager;
 import com.android.server.healthconnect.storage.utils.StorageUtils;
 import com.android.server.healthconnect.storage.utils.TimeUtils;
 
@@ -134,7 +135,7 @@
      * <p>Example: App1 > App2 > App3 Before:App1 : T1-T2 -> value1, App2 : T1-T3 -> value2 , App3 :
      * T2-T4 -> value3
      *
-     * <p>After merging overlapping data between T1-T4 below values will be taken from eachapp:
+     * <p>After merging overlapping data between T1-T4 below values will be taken from each app:
      *
      * <p>App1 : T1-T2 -> value1, App2 : T2-T3 -> value2*(T3-T2)/(T3-T1), App3 : T3-T4 ->
      * value3*(T4-T3)/(T4-T2)
@@ -161,7 +162,8 @@
                     continue;
                 }
                 RecordData recordData = getRecordData(mCursor);
-                if (recordData != null) {
+
+                if (shouldAddDataPoint(recordData)) {
                     mBufferWindow.add(recordData);
                 }
             }
@@ -176,6 +178,17 @@
         return getTotal();
     }
 
+    // Only add this datapoint to the TreeSet in the new behaviour
+    // if its app has a priority assigned
+    private boolean shouldAddDataPoint(RecordData recordData) {
+        if (recordData == null) return false;
+        if (HealthConnectDeviceConfigManager.getInitialisedInstance()
+                .isAggregationSourceControlsEnabled()) {
+            return mReversedPriorityList.contains(recordData.mAppId);
+        }
+        return true;
+    }
+
     private boolean cursorOutOfRange() {
         long cursorStartTime = StorageUtils.getCursorLong(mCursor, getStartTimeColumnName());
         long cursorEndTime = StorageUtils.getCursorLong(mCursor, getEndTimeColumnName());
@@ -309,6 +322,7 @@
     private RecordData getRecordData(Cursor cursor) {
         if (cursor != null) {
             double factor = 1;
+
             Instant startTime =
                     Instant.ofEpochMilli(
                             StorageUtils.getCursorLong(cursor, getStartTimeColumnName()));
diff --git a/service/java/com/android/server/healthconnect/storage/datatypehelpers/PreferenceHelper.java b/service/java/com/android/server/healthconnect/storage/datatypehelpers/PreferenceHelper.java
index 95d8bd6..323c8be 100644
--- a/service/java/com/android/server/healthconnect/storage/datatypehelpers/PreferenceHelper.java
+++ b/service/java/com/android/server/healthconnect/storage/datatypehelpers/PreferenceHelper.java
@@ -45,16 +45,17 @@
  *
  * @hide
  */
-public final class PreferenceHelper extends DatabaseHelper {
+// TODO(b/303023796): Make this final.
+public class PreferenceHelper extends DatabaseHelper {
     private static final String TABLE_NAME = "preference_table";
     private static final String KEY_COLUMN_NAME = "key";
     public static final List<Pair<String, Integer>> UNIQUE_COLUMN_INFO =
             Collections.singletonList(new Pair<>(KEY_COLUMN_NAME, TYPE_STRING));
     private static final String VALUE_COLUMN_NAME = "value";
     private static volatile PreferenceHelper sPreferenceHelper;
-    private volatile ConcurrentHashMap<String, String> mPreferences;
+    protected volatile ConcurrentHashMap<String, String> mPreferences;
 
-    private PreferenceHelper() {}
+    protected PreferenceHelper() {}
 
     /** Note: Overrides existing preference (if it exists) with the new value */
     public synchronized void insertOrReplacePreference(String key, String value) {
@@ -112,7 +113,7 @@
         populatePreferences();
     }
 
-    private Map<String, String> getPreferences() {
+    protected Map<String, String> getPreferences() {
         if (mPreferences == null) {
             populatePreferences();
         }
diff --git a/service/java/com/android/server/healthconnect/storage/datatypehelpers/RecordHelper.java b/service/java/com/android/server/healthconnect/storage/datatypehelpers/RecordHelper.java
index aa089c5..d7afbf5 100644
--- a/service/java/com/android/server/healthconnect/storage/datatypehelpers/RecordHelper.java
+++ b/service/java/com/android/server/healthconnect/storage/datatypehelpers/RecordHelper.java
@@ -18,6 +18,7 @@
 
 import static android.health.connect.Constants.DEFAULT_INT;
 import static android.health.connect.Constants.DEFAULT_LONG;
+import static android.health.connect.Constants.MAXIMUM_ALLOWED_CURSOR_COUNT;
 import static android.health.connect.Constants.MAXIMUM_PAGE_SIZE;
 
 import static com.android.server.healthconnect.storage.datatypehelpers.IntervalRecordHelper.END_TIME_COLUMN_NAME;
@@ -33,6 +34,8 @@
 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorUUID;
 import static com.android.server.healthconnect.storage.utils.StorageUtils.getDedupeByteBuffer;
 import static com.android.server.healthconnect.storage.utils.StorageUtils.supportsPriority;
+import static com.android.server.healthconnect.storage.utils.WhereClauses.LogicalOperator.AND;
+import static com.android.server.healthconnect.storage.utils.WhereClauses.LogicalOperator.OR;
 
 import android.annotation.NonNull;
 import android.content.ContentValues;
@@ -40,6 +43,7 @@
 import android.database.sqlite.SQLiteDatabase;
 import android.health.connect.AggregateResult;
 import android.health.connect.aidl.ReadRecordsRequestParcel;
+import android.health.connect.aidl.RecordIdFiltersParcel;
 import android.health.connect.datatypes.AggregationType;
 import android.health.connect.datatypes.RecordTypeIdentifier;
 import android.health.connect.internal.datatypes.RecordInternal;
@@ -57,6 +61,8 @@
 import com.android.server.healthconnect.storage.request.ReadTableRequest;
 import com.android.server.healthconnect.storage.request.UpsertTableRequest;
 import com.android.server.healthconnect.storage.utils.OrderByClause;
+import com.android.server.healthconnect.storage.utils.PageTokenUtil;
+import com.android.server.healthconnect.storage.utils.PageTokenWrapper;
 import com.android.server.healthconnect.storage.utils.SqlJoin;
 import com.android.server.healthconnect.storage.utils.StorageUtils;
 import com.android.server.healthconnect.storage.utils.WhereClauses;
@@ -71,7 +77,6 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.UUID;
-import java.util.stream.Collectors;
 
 /**
  * Parent class for all the helper classes for all the records
@@ -128,38 +133,55 @@
      */
     public final AggregateTableRequest getAggregateTableRequest(
             AggregationType<?> aggregationType,
-            List<String> packageFilter,
+            String callingPackage,
+            List<String> packageFilters,
             long startTime,
             long endTime,
             long startDateAccess,
             boolean useLocalTime) {
+        AppInfoHelper appInfoHelper = AppInfoHelper.getInstance();
         AggregateParams params = getAggregateParams(aggregationType);
-        String startTimeColumnName = getStartTimeColumnName();
-        params.setPhysicalTimeColumnName(startTimeColumnName);
-        params.setTimeColumnName(
-                useLocalTime ? getLocalStartTimeColumnName() : startTimeColumnName);
-        params.setExtraTimeColumn(
-                useLocalTime ? getLocalEndTimeColumnName() : getEndTimeColumnName());
+        String physicalTimeColumnName = getStartTimeColumnName();
+        String startTimeColumnName =
+                useLocalTime ? getLocalStartTimeColumnName() : physicalTimeColumnName;
+        String endTimeColumnName =
+                useLocalTime ? getLocalEndTimeColumnName() : getEndTimeColumnName();
+        params.setTimeColumnName(startTimeColumnName);
+        params.setExtraTimeColumn(endTimeColumnName);
         params.setOffsetColumnToFetch(getZoneOffsetColumnName());
 
         if (supportsPriority(mRecordIdentifier, aggregationType.getAggregateOperationType())) {
             List<String> columns =
                     Arrays.asList(
-                            startTimeColumnName,
+                            physicalTimeColumnName,
                             END_TIME_COLUMN_NAME,
                             APP_INFO_ID_COLUMN_NAME,
                             LAST_MODIFIED_TIME_COLUMN_NAME);
             params.appendAdditionalColumns(columns);
         }
         if (StorageUtils.isDerivedType(mRecordIdentifier)) {
-            params.appendAdditionalColumns(Collections.singletonList(startTimeColumnName));
+            params.appendAdditionalColumns(Collections.singletonList(physicalTimeColumnName));
         }
 
-        return new AggregateTableRequest(
-                        params, aggregationType, this, startDateAccess, useLocalTime)
-                .setPackageFilter(
-                        AppInfoHelper.getInstance().getAppInfoIds(packageFilter),
-                        APP_INFO_ID_COLUMN_NAME)
+        WhereClauses whereClauses = new WhereClauses(AND);
+        // filters by package names
+        whereClauses.addWhereInLongsClause(
+                APP_INFO_ID_COLUMN_NAME, appInfoHelper.getAppInfoIds(packageFilters));
+        // filter by start date access
+        whereClauses.addNestedWhereClauses(
+                getFilterByStartAccessDateWhereClauses(
+                        appInfoHelper.getAppInfoId(callingPackage), startDateAccess));
+        // start/end time filter
+        whereClauses.addWhereLessThanClause(startTimeColumnName, endTime);
+        if (endTimeColumnName != null) {
+            // for IntervalRecord, filters by overlapping
+            whereClauses.addWhereGreaterThanOrEqualClause(endTimeColumnName, startTime);
+        } else {
+            // for InstantRecord, filters by whether time falls into [startTime, endTime)
+            whereClauses.addWhereGreaterThanOrEqualClause(startTimeColumnName, startTime);
+        }
+
+        return new AggregateTableRequest(params, aggregationType, this, whereClauses, useLocalTime)
                 .setTimeFilter(startTime, endTime);
     }
 
@@ -305,7 +327,7 @@
     /** Returns ReadSingleTableRequest for {@code request} and package name {@code packageName} */
     public ReadTableRequest getReadTableRequest(
             ReadRecordsRequestParcel request,
-            String packageName,
+            String callingPackageName,
             boolean enforceSelfRead,
             long startDateAccess,
             Map<String, Boolean> extraPermsState) {
@@ -313,13 +335,13 @@
                 .setJoinClause(getJoinForReadRequest())
                 .setWhereClause(
                         getReadTableWhereClause(
-                                request, packageName, enforceSelfRead, startDateAccess))
+                                request, callingPackageName, enforceSelfRead, startDateAccess))
                 .setOrderBy(getOrderByClause(request))
                 .setLimit(getLimitSize(request))
                 .setRecordHelper(this)
                 .setExtraReadRequests(
                         getExtraDataReadRequests(
-                                request, packageName, startDateAccess, extraPermsState));
+                                request, callingPackageName, startDateAccess, extraPermsState));
     }
 
     /**
@@ -345,17 +367,23 @@
     }
 
     /** Returns ReadTableRequest for {@code uuids} */
-    public ReadTableRequest getReadTableRequest(List<UUID> uuids, long startDateAccess) {
+    public ReadTableRequest getReadTableRequest(
+            String packageName,
+            List<UUID> uuids,
+            long startDateAccess,
+            Map<String, Boolean> extraPermsState) {
         return new ReadTableRequest(getMainTableName())
                 .setJoinClause(getJoinForReadRequest())
                 .setWhereClause(
-                        new WhereClauses()
+                        new WhereClauses(AND)
                                 .addWhereInClauseWithoutQuotes(
                                         UUID_COLUMN_NAME, StorageUtils.getListOfHexString(uuids))
                                 .addWhereLaterThanTimeClause(
                                         getStartTimeColumnName(), startDateAccess))
                 .setRecordHelper(this)
-                .setExtraReadRequests(getExtraDataReadRequests(uuids, startDateAccess));
+                .setExtraReadRequests(
+                        getExtraDataReadRequests(
+                                packageName, uuids, startDateAccess, extraPermsState));
     }
 
     /**
@@ -374,7 +402,11 @@
      * Returns list if ReadSingleTableRequest for {@code uuids} to populate extra data. Called in
      * change logs read requests.
      */
-    List<ReadTableRequest> getExtraDataReadRequests(List<UUID> uuids, long startDateAccess) {
+    List<ReadTableRequest> getExtraDataReadRequests(
+            String packageName,
+            List<UUID> uuids,
+            long startDateAccess,
+            Map<String, Boolean> extraPermsState) {
         return Collections.emptyList();
     }
 
@@ -388,115 +420,150 @@
                 .setDistinctClause(true);
     }
 
-    /** Returns List of Internal records from the cursor */
-    @SuppressWarnings("unchecked")
-    public List<RecordInternal<?>> getInternalRecords(Cursor cursor, int requestSize) {
-        return getInternalRecords(cursor, requestSize, null);
-    }
-
-    /** Returns List of Internal records from the cursor */
-    @SuppressWarnings("unchecked")
-    public List<RecordInternal<?>> getInternalRecords(
-            Cursor cursor, int requestSize, Map<Long, String> packageNamesByAppIds) {
+    /**
+     * Returns List of Internal records from the cursor. If the cursor contains more than {@link
+     * MAXIMUM_ALLOWED_CURSOR_COUNT} records, it throws {@link IllegalArgumentException}.
+     */
+    public List<RecordInternal<?>> getInternalRecords(Cursor cursor) {
+        if (cursor.getCount() > MAXIMUM_ALLOWED_CURSOR_COUNT) {
+            throw new IllegalArgumentException(
+                    "Too many records in the cursor. Max allowed: " + MAXIMUM_ALLOWED_CURSOR_COUNT);
+        }
         Trace.traceBegin(TRACE_TAG_RECORD_HELPER, TAG_RECORD_HELPER.concat("GetInternalRecords"));
+
         List<RecordInternal<?>> recordInternalList = new ArrayList<>();
-
-        int count = 0;
-        long prevStartTime = DEFAULT_LONG;
-        long currentStartTime = DEFAULT_LONG;
-        int tempCount = 0;
-        List<RecordInternal<?>> tempList = new ArrayList<>();
         while (cursor.moveToNext()) {
-            try {
-                T record =
-                        (T)
-                                RecordMapper.getInstance()
-                                        .getRecordIdToInternalRecordClassMap()
-                                        .get(getRecordIdentifier())
-                                        .getConstructor()
-                                        .newInstance();
-                record.setUuid(getCursorUUID(cursor, UUID_COLUMN_NAME));
-                record.setLastModifiedTime(getCursorLong(cursor, LAST_MODIFIED_TIME_COLUMN_NAME));
-                record.setClientRecordId(getCursorString(cursor, CLIENT_RECORD_ID_COLUMN_NAME));
-                record.setClientRecordVersion(
-                        getCursorLong(cursor, CLIENT_RECORD_VERSION_COLUMN_NAME));
-                record.setRecordingMethod(getCursorInt(cursor, RECORDING_METHOD_COLUMN_NAME));
-                record.setRowId(getCursorInt(cursor, PRIMARY_COLUMN_NAME));
-                long deviceInfoId = getCursorLong(cursor, DEVICE_INFO_ID_COLUMN_NAME);
-                DeviceInfoHelper.getInstance().populateRecordWithValue(deviceInfoId, record);
-                long appInfoId = getCursorLong(cursor, APP_INFO_ID_COLUMN_NAME);
-                AppInfoHelper.getInstance()
-                        .populateRecordWithValue(appInfoId, record, packageNamesByAppIds);
-                populateRecordValue(cursor, record);
+            recordInternalList.add(getRecord(cursor, /* packageNamesByAppIds= */ null));
+        }
 
-                prevStartTime = currentStartTime;
-                currentStartTime = getCursorLong(cursor, getStartTimeColumnName());
-                if (prevStartTime == DEFAULT_LONG || prevStartTime == currentStartTime) {
-                    // Fetch and add records with same startTime to tempList
-                    tempList.add(record);
-                    tempCount++;
-                } else {
-                    if (count == 0) {
-                        // items in tempList having startTime same as the first record from cursor
-                        // is added to final list.
-                        // This makes sure that we return at least 1 record if the count of
-                        // records with startTime same as second record exceeds requestSize.
-                        recordInternalList.addAll(tempList);
-                        count = tempCount;
-                        tempList.clear();
-                        tempCount = 0;
-                        if (count >= requestSize) {
-                            // startTime of current record should be fetched for pageToken
-                            cursor.moveToPrevious();
-                            break;
-                        }
-                        tempList.add(record);
-                        tempCount = 1;
-                    } else if (tempCount + count <= requestSize) {
-                        // Makes sure after adding records in tempList with same starTime
-                        // the count does not exceed requestSize
-                        recordInternalList.addAll(tempList);
-                        count += tempCount;
-                        tempList.clear();
-                        tempCount = 0;
-                        if (count >= requestSize) {
-                            // After adding records if count is equal to requestSize then startTime
-                            // of current fetched record should be the next page token.
-                            cursor.moveToPrevious();
-                            break;
-                        }
-                        tempList.add(record);
-                        tempCount = 1;
-                    } else {
-                        // If adding records in tempList makes count > requestSize, then ignore temp
-                        // list and startTime of records in temp list should be the next page token.
-                        tempList.clear();
-                        int lastposition = cursor.getPosition();
-                        cursor.moveToPosition(lastposition - 2);
-                        break;
-                    }
-                }
-            } catch (InstantiationException
-                    | IllegalAccessException
-                    | NoSuchMethodException
-                    | InvocationTargetException exception) {
-                throw new IllegalArgumentException(exception);
-            }
-        }
-        if (!tempList.isEmpty()) {
-            if (tempCount + count <= requestSize) {
-                // If reached end of cursor while fetching records then add it to final list
-                recordInternalList.addAll(tempList);
-            } else {
-                // If reached end of cursor while fetching and adding it will exceed requestSize
-                // then ignore them,startTime of the last record will be pageToken for next read.
-                cursor.moveToPosition(cursor.getCount() - 2);
-            }
-        }
         Trace.traceEnd(TRACE_TAG_RECORD_HELPER);
         return recordInternalList;
     }
 
+    /**
+     * Returns a list of Internal records from the cursor up to the requested size, with pagination
+     * handled.
+     *
+     * @see #getNextInternalRecordsPageAndToken(Cursor, int, PageTokenWrapper, Map)
+     */
+    public Pair<List<RecordInternal<?>>, Long> getNextInternalRecordsPageAndToken(
+            Cursor cursor, int requestSize, PageTokenWrapper pageToken) {
+        return getNextInternalRecordsPageAndToken(
+                cursor, requestSize, pageToken, /* packageNamesByAppIds= */ null);
+    }
+
+    /**
+     * Returns List of Internal records from the cursor up to the requested size, with pagination
+     * handled.
+     *
+     * <p>Note that the cursor limit is set to {@code requestSize + offset + 1},
+     * <li>+ offset: {@code offset} records has already been returned in previous page(s). See
+     *     go/hc-page-token for details.
+     * <li>+ 1: if number of records queried is more than pageSize we know there are more records
+     *     available to return for the next read.
+     *
+     *     <p>Note that the cursor may contain more records that we need to return. Cursor limit set
+     *     to sum of the following:
+     * <li>offset: {@code offset} records have already been returned in previous page(s), and should
+     *     be skipped from this current page. In rare occasions (e.g. records deleted in between two
+     *     reads), there are less than {@code offset} records, an empty list is returned, with no
+     *     page token.
+     * <li>requestSize: {@code requestSize} records to return in the response.
+     * <li>one extra record: If there are more records than (offset+requestSize), a page token is
+     *     returned for the next page. If not, then a default token is returned.
+     *
+     * @see #getLimitSize(ReadRecordsRequestParcel)
+     */
+    public Pair<List<RecordInternal<?>>, Long> getNextInternalRecordsPageAndToken(
+            Cursor cursor,
+            int requestSize,
+            PageTokenWrapper prevPageToken,
+            @Nullable Map<Long, String> packageNamesByAppIds) {
+        Trace.traceBegin(
+                TRACE_TAG_RECORD_HELPER,
+                TAG_RECORD_HELPER.concat("getNextInternalRecordsPageAndToken"));
+
+        // Ignore <offset> records of the same start time, because it was returned in previous
+        // page(s).
+        // If the offset is greater than number of records in the cursor, it'll move to the last
+        // index and will not enter the while loop below.
+        long prevStartTime;
+        long currentStartTime = DEFAULT_LONG;
+        for (int i = 0; i < prevPageToken.offset(); i++) {
+            if (!cursor.moveToNext()) {
+                break;
+            }
+            prevStartTime = currentStartTime;
+            currentStartTime = getCursorLong(cursor, getStartTimeColumnName());
+            if (prevStartTime != DEFAULT_LONG && prevStartTime != currentStartTime) {
+                // The current record should not be skipped
+                cursor.moveToPrevious();
+                break;
+            }
+        }
+
+        currentStartTime = DEFAULT_LONG;
+        int offset = 0;
+        List<RecordInternal<?>> recordInternalList = new ArrayList<>();
+        long nextToken = DEFAULT_LONG;
+        while (cursor.moveToNext()) {
+            prevStartTime = currentStartTime;
+            currentStartTime = getCursorLong(cursor, getStartTimeColumnName());
+            if (currentStartTime != prevStartTime) {
+                offset = 0;
+            }
+
+            if (recordInternalList.size() >= requestSize) {
+                PageTokenWrapper nextPageToken =
+                        PageTokenWrapper.of(prevPageToken.isAscending(), currentStartTime, offset);
+                nextToken = PageTokenUtil.encode(nextPageToken);
+                break;
+            } else {
+                T record = getRecord(cursor, packageNamesByAppIds);
+                recordInternalList.add(record);
+                offset++;
+            }
+        }
+
+        Trace.traceEnd(TRACE_TAG_RECORD_HELPER);
+        return Pair.create(recordInternalList, nextToken);
+    }
+
+    @SuppressWarnings("unchecked") // uncheck cast to T
+    private T getRecord(Cursor cursor, @Nullable Map<Long, String> packageNamesByAppIds) {
+        try {
+            T record =
+                    (T)
+                            RecordMapper.getInstance()
+                                    .getRecordIdToInternalRecordClassMap()
+                                    .get(getRecordIdentifier())
+                                    .getConstructor()
+                                    .newInstance();
+            record.setUuid(getCursorUUID(cursor, UUID_COLUMN_NAME));
+            record.setLastModifiedTime(getCursorLong(cursor, LAST_MODIFIED_TIME_COLUMN_NAME));
+            record.setClientRecordId(getCursorString(cursor, CLIENT_RECORD_ID_COLUMN_NAME));
+            record.setClientRecordVersion(getCursorLong(cursor, CLIENT_RECORD_VERSION_COLUMN_NAME));
+            record.setRecordingMethod(getCursorInt(cursor, RECORDING_METHOD_COLUMN_NAME));
+            record.setRowId(getCursorInt(cursor, PRIMARY_COLUMN_NAME));
+            long deviceInfoId = getCursorLong(cursor, DEVICE_INFO_ID_COLUMN_NAME);
+            DeviceInfoHelper.getInstance().populateRecordWithValue(deviceInfoId, record);
+            long appInfoId = getCursorLong(cursor, APP_INFO_ID_COLUMN_NAME);
+            String packageName =
+                    packageNamesByAppIds != null
+                            ? packageNamesByAppIds.get(appInfoId)
+                            : AppInfoHelper.getInstance().getPackageName(appInfoId);
+            record.setPackageName(packageName);
+            populateRecordValue(cursor, record);
+
+            return record;
+        } catch (InstantiationException
+                | IllegalAccessException
+                | NoSuchMethodException
+                | InvocationTargetException exception) {
+            throw new IllegalArgumentException(exception);
+        }
+    }
+
     /** Returns is the read of this record type is enabled */
     public boolean isRecordOperationsEnabled() {
         return true;
@@ -600,95 +667,139 @@
         return null;
     }
 
-    private int getLimitSize(ReadRecordsRequestParcel request) {
+    private static int getLimitSize(ReadRecordsRequestParcel request) {
+        // Querying extra records on top of page size
+        // + pageOffset: <pageOffset> records has already been returned in previous page(s). See
+        //               go/hc-page-token for details.
+        // + 1: if number of records queried is more than pageSize we know there are more records
+        //      available to return for the next read.
         if (request.getRecordIdFiltersParcel() == null) {
-            return request.getPageSize();
+            int pageOffset =
+                    PageTokenUtil.decode(request.getPageToken(), request.isAscending()).offset();
+            return request.getPageSize() + pageOffset + 1;
         } else {
             return MAXIMUM_PAGE_SIZE;
         }
     }
 
-    WhereClauses getReadTableWhereClause(
+    final WhereClauses getReadTableWhereClause(
             ReadRecordsRequestParcel request,
-            String packageName,
+            String callingPackageName,
             boolean enforceSelfRead,
-            long startDateAccess) {
-        if (request.getRecordIdFiltersParcel() == null) {
-            List<Long> appIds =
-                    AppInfoHelper.getInstance().getAppInfoIds(request.getPackageFilters()).stream()
+            long startDateAccessMillis) {
+        AppInfoHelper appInfoHelper = AppInfoHelper.getInstance();
+        long callingAppInfoId = appInfoHelper.getAppInfoId(callingPackageName);
+
+        RecordIdFiltersParcel recordIdFiltersParcel = request.getRecordIdFiltersParcel();
+        if (recordIdFiltersParcel == null) {
+            List<Long> appInfoIds =
+                    appInfoHelper.getAppInfoIds(request.getPackageFilters()).stream()
                             .distinct()
-                            .collect(Collectors.toList());
+                            .toList();
             if (enforceSelfRead) {
-                appIds =
-                        AppInfoHelper.getInstance()
-                                .getAppInfoIds(Collections.singletonList(packageName));
+                appInfoIds = Collections.singletonList(callingAppInfoId);
             }
-            if (appIds.size() == 1 && appIds.get(0) == DEFAULT_INT) {
+            if (appInfoIds.size() == 1 && appInfoIds.get(0) == DEFAULT_INT) {
                 throw new TypeNotPresentException(TYPE_NOT_PRESENT_PACKAGE_NAME, new Throwable());
             }
 
-            WhereClauses clauses =
-                    new WhereClauses().addWhereInLongsClause(APP_INFO_ID_COLUMN_NAME, appIds);
+            WhereClauses clauses = new WhereClauses(AND);
 
-            if (request.getPageToken() != DEFAULT_LONG) {
-                // Since pageToken passed contains detail of sort order. Actual token value for read
-                // is calculated back from the requested pageToken based on sort order.
-                if (request.isAscending()) {
-                    clauses.addWhereGreaterThanOrEqualClause(
-                            getStartTimeColumnName(), request.getPageToken() / 2);
+            // package names filter
+            clauses.addWhereInLongsClause(APP_INFO_ID_COLUMN_NAME, appInfoIds);
+
+            // page token filter
+            PageTokenWrapper pageToken =
+                    PageTokenUtil.decode(request.getPageToken(), request.isAscending());
+            if (pageToken.isTimestampSet()) {
+                long timestamp = pageToken.timeMillis();
+                if (pageToken.isAscending()) {
+                    clauses.addWhereGreaterThanOrEqualClause(getStartTimeColumnName(), timestamp);
                 } else {
-                    clauses.addWhereLessThanOrEqualClause(
-                            getStartTimeColumnName(), (request.getPageToken() - 1) / 2);
+                    clauses.addWhereLessThanOrEqualClause(getStartTimeColumnName(), timestamp);
                 }
             }
 
-            if (request.usesLocalTimeFilter()) {
-                clauses.addWhereGreaterThanOrEqualClause(getStartTimeColumnName(), startDateAccess);
-                clauses.addWhereBetweenClause(
-                        getLocalStartTimeColumnName(),
-                        request.getStartTime(),
-                        request.getEndTime());
-            } else {
-                clauses.addWhereBetweenTimeClause(
-                        getStartTimeColumnName(), startDateAccess, request.getEndTime());
+            // start/end time filter
+            String timeColumnName =
+                    request.usesLocalTimeFilter()
+                            ? getLocalStartTimeColumnName()
+                            : getStartTimeColumnName();
+            long startTimeMillis = request.getStartTime();
+            long endTimeMillis = request.getEndTime();
+            if (startTimeMillis != DEFAULT_LONG) {
+                clauses.addWhereGreaterThanOrEqualClause(timeColumnName, startTimeMillis);
             }
+            if (endTimeMillis != DEFAULT_LONG) {
+                clauses.addWhereLessThanClause(timeColumnName, endTimeMillis);
+            }
+
+            // start date access
+            clauses.addNestedWhereClauses(
+                    getFilterByStartAccessDateWhereClauses(
+                            callingAppInfoId, startDateAccessMillis));
 
             return clauses;
         }
 
         // Since for now we don't support mixing IDs and filters, we need to look for IDs now
         List<UUID> ids =
-                request.getRecordIdFiltersParcel().getRecordIdFilters().stream()
+                recordIdFiltersParcel.getRecordIdFilters().stream()
                         .map(
                                 (recordIdFilter) ->
-                                        StorageUtils.getUUIDFor(recordIdFilter, packageName))
-                        .collect(Collectors.toList());
-        WhereClauses whereClauses =
-                new WhereClauses()
+                                        StorageUtils.getUUIDFor(recordIdFilter, callingPackageName))
+                        .toList();
+        WhereClauses filterByIdsWhereClauses =
+                new WhereClauses(AND)
                         .addWhereInClauseWithoutQuotes(
                                 UUID_COLUMN_NAME, StorageUtils.getListOfHexString(ids));
 
         if (enforceSelfRead) {
-            long id = AppInfoHelper.getInstance().getAppInfoId(packageName);
-            if (id == DEFAULT_LONG) {
+            if (callingAppInfoId == DEFAULT_LONG) {
                 throw new TypeNotPresentException(TYPE_NOT_PRESENT_PACKAGE_NAME, new Throwable());
             }
-            whereClauses.addWhereInLongsClause(
-                    APP_INFO_ID_COLUMN_NAME, Collections.singletonList(id));
-            return whereClauses.addWhereLaterThanTimeClause(
-                    getStartTimeColumnName(), startDateAccess);
+            // if self read is enforced, startDateAccess must not be applied.
+            return filterByIdsWhereClauses.addWhereInLongsClause(
+                    APP_INFO_ID_COLUMN_NAME, Collections.singletonList(callingAppInfoId));
+        } else {
+            return filterByIdsWhereClauses.addNestedWhereClauses(
+                    getFilterByStartAccessDateWhereClauses(
+                            callingAppInfoId, startDateAccessMillis));
         }
-        return whereClauses;
+    }
+
+    /**
+     * Returns a {@link WhereClauses} that takes in to account start date access date & reading own
+     * data.
+     */
+    private WhereClauses getFilterByStartAccessDateWhereClauses(
+            long callingAppInfoId, long startDateAccessMillis) {
+        WhereClauses resultWhereClauses = new WhereClauses(OR);
+
+        // if the data point belongs to the calling app, then we should not enforce startDateAccess
+        resultWhereClauses.addWhereEqualsClause(
+                APP_INFO_ID_COLUMN_NAME, String.valueOf(callingAppInfoId));
+
+        // Otherwise, we should enforce startDateAccess. Also we must use physical time column
+        // regardless whether local time filter is used or not.
+        String physicalTimeColumn = getStartTimeColumnName();
+        resultWhereClauses.addWhereGreaterThanOrEqualClause(
+                physicalTimeColumn, startDateAccessMillis);
+
+        return resultWhereClauses;
     }
 
     abstract String getZoneOffsetColumnName();
 
     private OrderByClause getOrderByClause(ReadRecordsRequestParcel request) {
-        OrderByClause orderByClause = new OrderByClause();
-        if (request.getRecordIdFiltersParcel() == null) {
-            orderByClause.addOrderByClause(getStartTimeColumnName(), request.isAscending());
+        if (request.getRecordIdFiltersParcel() != null) {
+            return new OrderByClause();
         }
-        return orderByClause;
+        PageTokenWrapper pageToken =
+                PageTokenUtil.decode(request.getPageToken(), request.isAscending());
+        return new OrderByClause()
+                .addOrderByClause(getStartTimeColumnName(), pageToken.isAscending())
+                .addOrderByClause(PRIMARY_COLUMN_NAME, /* isAscending= */ true);
     }
 
     @NonNull
diff --git a/service/java/com/android/server/healthconnect/storage/datatypehelpers/SleepStageRecordHelper.java b/service/java/com/android/server/healthconnect/storage/datatypehelpers/SleepStageRecordHelper.java
index b254c55..1e05ac4 100644
--- a/service/java/com/android/server/healthconnect/storage/datatypehelpers/SleepStageRecordHelper.java
+++ b/service/java/com/android/server/healthconnect/storage/datatypehelpers/SleepStageRecordHelper.java
@@ -24,6 +24,7 @@
 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorInt;
 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorLong;
 import static com.android.server.healthconnect.storage.utils.StorageUtils.isNullValue;
+import static com.android.server.healthconnect.storage.utils.WhereClauses.LogicalOperator.AND;
 
 import android.annotation.NonNull;
 import android.content.ContentValues;
@@ -64,7 +65,7 @@
     /** Returns sql join needed for calculating sleep duration */
     public static SqlJoin getJoinForDurationAggregation(String parentTableName) {
         SqlJoin join = getJoinReadRequest(parentTableName);
-        WhereClauses filterAwakes = new WhereClauses();
+        WhereClauses filterAwakes = new WhereClauses(AND);
         filterAwakes.addWhereInIntsClause(SLEEP_STAGE_TYPE, DURATION_EXCLUDE_TYPES);
         join.setSecondTableWhereClause(filterAwakes);
         return join;
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 1fb3ba0..2e68637 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
@@ -31,7 +31,10 @@
 import android.util.ArrayMap;
 import android.util.Slog;
 
+import androidx.annotation.Nullable;
+
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.healthconnect.HealthConnectDeviceConfigManager;
 import com.android.server.healthconnect.storage.request.AggregateParams;
 
 import java.time.ZoneOffset;
@@ -143,11 +146,13 @@
         }
 
         while (mLatestPopulatedStart <= expansionBorder && cursor.moveToNext()) {
-            AggregationRecordData data = readNewDataAndAddToBuffer(cursor);
-            mLatestPopulatedStart = data.getStartTime();
+            AggregationRecordData data = readNewDataAndMaybeAddToBuffer(cursor);
+            if (data != null) {
+                mLatestPopulatedStart = data.getStartTime();
 
-            if (Constants.DEBUG) {
-                Slog.d(TAG, "Updated buffer with : " + data);
+                if (Constants.DEBUG) {
+                    Slog.d(TAG, "Updated buffer with : " + data);
+                }
             }
         }
 
@@ -162,8 +167,11 @@
                     new AggregationTimestamp(AggregationTimestamp.GROUP_BORDER, groupSplit));
         }
 
-        if (cursor.moveToNext()) {
-            readNewDataAndAddToBuffer(cursor);
+        while (cursor.moveToNext()) {
+            AggregationRecordData data = readNewDataAndMaybeAddToBuffer(cursor);
+            if (data != null) {
+                break;
+            }
         }
 
         if (Constants.DEBUG) {
@@ -171,8 +179,17 @@
         }
     }
 
-    private AggregationRecordData readNewDataAndAddToBuffer(Cursor cursor) {
+    @Nullable
+    private AggregationRecordData readNewDataAndMaybeAddToBuffer(Cursor cursor) {
         AggregationRecordData data = readNewData(cursor);
+        int priority = data.getPriority();
+
+        if (HealthConnectDeviceConfigManager.getInitialisedInstance()
+                        .isAggregationSourceControlsEnabled()
+                && priority == Integer.MIN_VALUE) {
+            return null;
+        }
+
         mTimestampsBuffer.add(data.getStartTimestamp());
         mTimestampsBuffer.add(data.getEndTimestamp());
         return data;
diff --git a/service/java/com/android/server/healthconnect/storage/request/AggregateParams.java b/service/java/com/android/server/healthconnect/storage/request/AggregateParams.java
index 00532f9..c136295 100644
--- a/service/java/com/android/server/healthconnect/storage/request/AggregateParams.java
+++ b/service/java/com/android/server/healthconnect/storage/request/AggregateParams.java
@@ -35,17 +35,6 @@
 public final class AggregateParams {
     private final String mTableName;
 
-    /**
-     * Physical time column name (i.e. {@link
-     * com.android.server.healthconnect.storage.datatypehelpers.RecordHelper#getStartTimeColumnName()}).
-     *
-     * <p>This is used to compare against start date access when local time is used, when local time
-     * is NOT used, this has the same value as {@link #mTimeColumnName}.
-     *
-     * <p>Same as {@link #mTimeColumnName}, this is start time for interval records.
-     */
-    private String mPhysicalTimeColumnName;
-
     /** Column used for time filtering. Start time for interval records. */
     private String mTimeColumnName;
 
@@ -87,10 +76,6 @@
         return mTimeColumnName;
     }
 
-    public String getPhysicalTimeColumnName() {
-        return mPhysicalTimeColumnName;
-    }
-
     public String getExtraTimeColumnName() {
         return mExtraTimeColumnName;
     }
@@ -114,10 +99,6 @@
         return this;
     }
 
-    public void setPhysicalTimeColumnName(String physicalTimeColumnName) {
-        this.mPhysicalTimeColumnName = physicalTimeColumnName;
-    }
-
     /** Appends additional columns to fetch. */
     public AggregateParams appendAdditionalColumns(List<String> additionColumns) {
         mColumnsToFetch.addAll(additionColumns);
diff --git a/service/java/com/android/server/healthconnect/storage/request/AggregateTableRequest.java b/service/java/com/android/server/healthconnect/storage/request/AggregateTableRequest.java
index ad6a031..feb77b3 100644
--- a/service/java/com/android/server/healthconnect/storage/request/AggregateTableRequest.java
+++ b/service/java/com/android/server/healthconnect/storage/request/AggregateTableRequest.java
@@ -63,23 +63,23 @@
 
     private static final int MAX_NUMBER_OF_GROUPS = Constants.MAXIMUM_PAGE_SIZE;
 
-    private final long DEFAULT_TIME = -1;
     private final String mTableName;
     private final List<String> mColumnNamesToAggregate;
     private final AggregationType<?> mAggregationType;
     private final RecordHelper<?> mRecordHelper;
     private final Map<Integer, AggregateResult<?>> mAggregateResults = new ArrayMap<>();
-    private final String mPhysicalTimeColumnName;
+
+    /**
+     * Represents "start time" for interval record, and "time" for instant record.
+     *
+     * <p>{@link #mUseLocalTime} is already taken into account when this field is set, meaning if
+     * {@link #mUseLocalTime} is {@code true}, then this field represent local time, otherwise
+     * physical time.
+     */
     private final String mTimeColumnName;
-    // Additional column used for time filtering. End time for interval records,
-    // null for other records.
-    private final String mEndTimeColumnName;
-    private final long mStartDateAccess;
+
+    private final WhereClauses mWhereClauses;
     private final SqlJoin mSqlJoin;
-    private List<Long> mPackageFilters;
-    private long mStartTime = DEFAULT_TIME;
-    private long mEndTime = DEFAULT_TIME;
-    private String mPackageColumnName;
     private String mGroupByColumnName;
     private int mGroupBySize = 1;
     private final List<String> mAdditionalColumnsToFetch;
@@ -91,24 +91,23 @@
             AggregateParams params,
             AggregationType<?> aggregationType,
             RecordHelper<?> recordHelper,
-            long startDateAccess,
+            WhereClauses whereClauses,
             boolean useLocalTime) {
         mTableName = params.getTableName();
         mColumnNamesToAggregate = params.getColumnsToFetch();
-        mPhysicalTimeColumnName = params.getPhysicalTimeColumnName();
         mTimeColumnName = params.getTimeColumnName();
         mAggregationType = aggregationType;
         mRecordHelper = recordHelper;
         mSqlJoin = params.getJoin();
         mPriorityParams = params.getPriorityAggregationExtraParams();
-        mEndTimeColumnName = params.getExtraTimeColumnName();
+        mWhereClauses = whereClauses;
         mAdditionalColumnsToFetch = new ArrayList<>();
         mAdditionalColumnsToFetch.add(params.getTimeOffsetColumnName());
         mAdditionalColumnsToFetch.add(mTimeColumnName);
-        if (mEndTimeColumnName != null) {
-            mAdditionalColumnsToFetch.add(mEndTimeColumnName);
+        String endTimeColumnName = params.getExtraTimeColumnName();
+        if (endTimeColumnName != null) {
+            mAdditionalColumnsToFetch.add(endTimeColumnName);
         }
-        mStartDateAccess = startDateAccess;
         mUseLocalTime = useLocalTime;
     }
 
@@ -194,14 +193,6 @@
         return appendAggregateCommand(builder, usingPriority);
     }
 
-    public AggregateTableRequest setPackageFilter(
-            List<Long> packageFilters, String packageColumnName) {
-        mPackageFilters = packageFilters;
-        mPackageColumnName = packageColumnName;
-
-        return this;
-    }
-
     /** Sets time filter for table request. */
     public AggregateTableRequest setTimeFilter(long startTime, long endTime) {
         // Return if the params will result in no impact on the query
@@ -209,9 +200,7 @@
             return this;
         }
 
-        mStartTime = startTime;
-        mEndTime = endTime;
-        mTimeSplits = List.of(mStartTime, mEndTime);
+        mTimeSplits = List.of(startTime, endTime);
         return this;
     }
 
@@ -333,7 +322,7 @@
             builder.append(mSqlJoin.getJoinCommand());
         }
 
-        builder.append(buildAggregationWhereCondition());
+        builder.append(mWhereClauses.get(/* withWhereKeyword= */ true));
 
         if (useGroupBy) {
             builder.append(" GROUP BY " + GROUP_BY_COLUMN_NAME);
@@ -350,36 +339,6 @@
         return builder.toString();
     }
 
-    private String buildAggregationWhereCondition() {
-        WhereClauses whereClauses = new WhereClauses();
-        whereClauses.addWhereInLongsClause(mPackageColumnName, mPackageFilters);
-
-        // Take start access date into account
-        long startTime = mStartTime;
-        // This is an optimization to avoid unnecessary WHERE clause.
-        // - if local time is used, we'll compare the physical time field with mStartDateAccess
-        // - otherwise we'll just update the startTime to mStartDateAccess if it is greater than
-        //   startTime because we'll need to WHERE with that anyway.
-        // This is similar to what's been done in RecordHelper#getReadTableWhereClause()
-        if (mUseLocalTime) {
-            whereClauses.addWhereGreaterThanOrEqualClause(
-                    mPhysicalTimeColumnName, mStartDateAccess);
-        } else {
-            startTime = Math.max(mStartDateAccess, mStartTime);
-        }
-
-        if (mEndTimeColumnName != null) {
-            // Filter all records which overlap with time filter interval:
-            // recordStartTime < filterEndTime and recordEndTime >= filterStartTime
-            whereClauses.addWhereGreaterThanOrEqualClause(mEndTimeColumnName, startTime);
-        } else {
-            whereClauses.addWhereGreaterThanOrEqualClause(mTimeColumnName, startTime);
-        }
-        whereClauses.addWhereLessThanClause(mTimeColumnName, mEndTime);
-
-        return whereClauses.get(/* withWhereKeyword= */ true);
-    }
-
     private void updateResultWithDataOriginPackageNames(Cursor metaDataCursor) {
         List<Long> packageIds = new ArrayList<>();
         while (metaDataCursor.moveToNext()) {
diff --git a/service/java/com/android/server/healthconnect/storage/request/AggregateTransactionRequest.java b/service/java/com/android/server/healthconnect/storage/request/AggregateTransactionRequest.java
index 66d97cf..ba3b8c0 100644
--- a/service/java/com/android/server/healthconnect/storage/request/AggregateTransactionRequest.java
+++ b/service/java/com/android/server/healthconnect/storage/request/AggregateTransactionRequest.java
@@ -72,6 +72,7 @@
                 AggregateTableRequest aggregateTableRequest =
                         recordHelper.getAggregateTableRequest(
                                 aggregationType,
+                                packageName,
                                 request.getPackageFilters(),
                                 request.getStartTime(),
                                 request.getEndTime(),
diff --git a/service/java/com/android/server/healthconnect/storage/request/DeleteTableRequest.java b/service/java/com/android/server/healthconnect/storage/request/DeleteTableRequest.java
index c1be2b1..c7b839c 100644
--- a/service/java/com/android/server/healthconnect/storage/request/DeleteTableRequest.java
+++ b/service/java/com/android/server/healthconnect/storage/request/DeleteTableRequest.java
@@ -19,6 +19,8 @@
 import static android.health.connect.Constants.DEFAULT_LONG;
 import static android.health.connect.datatypes.RecordTypeIdentifier.RECORD_TYPE_UNKNOWN;
 
+import static com.android.server.healthconnect.storage.utils.WhereClauses.LogicalOperator.AND;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.health.connect.Constants;
@@ -129,6 +131,11 @@
         return mIdColumnName;
     }
 
+    @Nullable
+    public List<String> getIds() {
+        return mIds;
+    }
+
     @NonNull
     public String getTableName() {
         return mTableName;
@@ -160,7 +167,7 @@
 
     public String getWhereCommand() {
         WhereClauses whereClauses =
-                Objects.isNull(mCustomWhereClauses) ? new WhereClauses() : mCustomWhereClauses;
+                Objects.isNull(mCustomWhereClauses) ? new WhereClauses(AND) : mCustomWhereClauses;
         whereClauses.addWhereInLongsClause(mPackageColumnName, mPackageFilters);
         whereClauses.addWhereBetweenTimeClause(mTimeColumnName, mStartTime, mEndTime);
         whereClauses.addWhereInClauseWithoutQuotes(mIdColumnName, mIds);
diff --git a/service/java/com/android/server/healthconnect/storage/request/ReadTableRequest.java b/service/java/com/android/server/healthconnect/storage/request/ReadTableRequest.java
index bda8284..ed03727 100644
--- a/service/java/com/android/server/healthconnect/storage/request/ReadTableRequest.java
+++ b/service/java/com/android/server/healthconnect/storage/request/ReadTableRequest.java
@@ -16,10 +16,9 @@
 
 package com.android.server.healthconnect.storage.request;
 
-import static android.health.connect.Constants.DEFAULT_PAGE_SIZE;
-
 import static com.android.server.healthconnect.storage.utils.StorageUtils.DELIMITER;
 import static com.android.server.healthconnect.storage.utils.StorageUtils.LIMIT_SIZE;
+import static com.android.server.healthconnect.storage.utils.WhereClauses.LogicalOperator.AND;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -49,11 +48,10 @@
     private RecordHelper<?> mRecordHelper;
     private List<String> mColumnNames;
     private SqlJoin mJoinClause;
-    private WhereClauses mWhereClauses = new WhereClauses();
+    private WhereClauses mWhereClauses = new WhereClauses(AND);
     private boolean mDistinct = false;
     private OrderByClause mOrderByClause = new OrderByClause();
     private String mLimitClause = "";
-    private int mPageSize = DEFAULT_PAGE_SIZE;
     private List<ReadTableRequest> mExtraReadRequests;
     private List<ReadTableRequest> mUnionReadRequests;
 
@@ -172,20 +170,11 @@
 
     /** Sets LIMIT size for the read query */
     @NonNull
-    public ReadTableRequest setLimit(int pageSize) {
-        mPageSize = pageSize;
-        // We set limit size to requested pageSize + 1,so that if number of records queried is more
-        // than pageSize we know there are more records available to return for the next read.
-        pageSize += 1;
-        mLimitClause = LIMIT_SIZE + pageSize;
+    public ReadTableRequest setLimit(int limit) {
+        mLimitClause = LIMIT_SIZE + limit;
         return this;
     }
 
-    /** Returns page size of the read request */
-    public int getPageSize() {
-        return mPageSize;
-    }
-
     private String getColumnsToFetch() {
         if (mColumnNames == null || mColumnNames.isEmpty()) {
             return "*";
diff --git a/service/java/com/android/server/healthconnect/storage/request/ReadTransactionRequest.java b/service/java/com/android/server/healthconnect/storage/request/ReadTransactionRequest.java
index a6fb737..a38413d 100644
--- a/service/java/com/android/server/healthconnect/storage/request/ReadTransactionRequest.java
+++ b/service/java/com/android/server/healthconnect/storage/request/ReadTransactionRequest.java
@@ -16,16 +16,22 @@
 
 package com.android.server.healthconnect.storage.request;
 
+import static android.health.connect.Constants.DEFAULT_INT;
+
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.health.connect.aidl.ReadRecordsRequestParcel;
 
 import com.android.server.healthconnect.storage.datatypehelpers.RecordHelper;
+import com.android.server.healthconnect.storage.utils.PageTokenUtil;
+import com.android.server.healthconnect.storage.utils.PageTokenWrapper;
 import com.android.server.healthconnect.storage.utils.RecordHelperProvider;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.UUID;
 
 /**
@@ -36,41 +42,77 @@
  *
  * @hide
  */
+// TODO(b/308158714): Separate two types of requests: read by id and read by filter.
 public class ReadTransactionRequest {
     public static final String TYPE_NOT_PRESENT_PACKAGE_NAME = "package_name";
     private final List<ReadTableRequest> mReadTableRequests;
+    @Nullable // page token is null for read by id requests
+    private final PageTokenWrapper mPageToken;
+    private final int mPageSize;
 
     public ReadTransactionRequest(
-            String packageName,
+            String callingPackageName,
             ReadRecordsRequestParcel request,
-            long startDateAccess,
+            long startDateAccessMillis,
             boolean enforceSelfRead,
-            Map<String, Boolean> extraReadPermsMapping) {
+            Map<String, Boolean> extraPermsState) {
         RecordHelper<?> recordHelper =
                 RecordHelperProvider.getInstance().getRecordHelper(request.getRecordType());
         mReadTableRequests =
                 Collections.singletonList(
                         recordHelper.getReadTableRequest(
                                 request,
-                                packageName,
+                                callingPackageName,
                                 enforceSelfRead,
-                                startDateAccess,
-                                extraReadPermsMapping));
+                                startDateAccessMillis,
+                                extraPermsState));
+        if (request.getRecordIdFiltersParcel() == null) {
+            mPageToken = PageTokenUtil.decode(request.getPageToken(), request.isAscending());
+            mPageSize = request.getPageSize();
+        } else {
+            mPageSize = DEFAULT_INT;
+            mPageToken = null;
+        }
     }
 
     public ReadTransactionRequest(
-            Map<Integer, List<UUID>> recordTypeToUuids, long startDateAccess) {
+            String packageName,
+            Map<Integer, List<UUID>> recordTypeToUuids,
+            long startDateAccess,
+            Map<String, Boolean> extraPermsState) {
         mReadTableRequests = new ArrayList<>();
         recordTypeToUuids.forEach(
                 (recordType, uuids) ->
                         mReadTableRequests.add(
                                 RecordHelperProvider.getInstance()
                                         .getRecordHelper(recordType)
-                                        .getReadTableRequest(uuids, startDateAccess)));
+                                        .getReadTableRequest(
+                                                packageName,
+                                                uuids,
+                                                startDateAccess,
+                                                extraPermsState)));
+        mPageSize = DEFAULT_INT;
+        mPageToken = null;
     }
 
     @NonNull
     public List<ReadTableRequest> getReadRequests() {
         return mReadTableRequests;
     }
+
+    @Nullable
+    public PageTokenWrapper getPageToken() {
+        return mPageToken;
+    }
+
+    /**
+     * Returns optional of page size in the {@link android.health.connect.ReadRecordsRequest}
+     * refined by this {@link ReadTransactionRequest}.
+     *
+     * <p>For {@link android.health.connect.ReadRecordsRequestUsingIds} requests, page size is
+     * {@code Optional.empty}.
+     */
+    public Optional<Integer> getPageSize() {
+        return mPageSize == DEFAULT_INT ? Optional.empty() : Optional.of(mPageSize);
+    }
 }
diff --git a/service/java/com/android/server/healthconnect/storage/request/UpsertTableRequest.java b/service/java/com/android/server/healthconnect/storage/request/UpsertTableRequest.java
index eaf4939..199e957 100644
--- a/service/java/com/android/server/healthconnect/storage/request/UpsertTableRequest.java
+++ b/service/java/com/android/server/healthconnect/storage/request/UpsertTableRequest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.healthconnect.storage.request;
 
+import static com.android.server.healthconnect.storage.utils.WhereClauses.LogicalOperator.OR;
+
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -158,7 +160,7 @@
 
     @NonNull
     private WhereClauses getReadWhereClauses() {
-        WhereClauses readWhereClause = new WhereClauses().setUseOr(true);
+        WhereClauses readWhereClause = new WhereClauses(OR);
 
         for (Pair<String, Integer> uniqueColumn : mUniqueColumns) {
             switch (uniqueColumn.second) {
diff --git a/service/java/com/android/server/healthconnect/storage/request/UpsertTransactionRequest.java b/service/java/com/android/server/healthconnect/storage/request/UpsertTransactionRequest.java
index 7cb7ff9..dadcb6a 100644
--- a/service/java/com/android/server/healthconnect/storage/request/UpsertTransactionRequest.java
+++ b/service/java/com/android/server/healthconnect/storage/request/UpsertTransactionRequest.java
@@ -18,6 +18,8 @@
 
 import static android.health.connect.Constants.UPSERT;
 
+import static com.android.server.healthconnect.storage.utils.WhereClauses.LogicalOperator.AND;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
@@ -183,7 +185,7 @@
     }
 
     private WhereClauses generateWhereClausesForUpdate(@NonNull RecordInternal<?> recordInternal) {
-        WhereClauses whereClauseForUpdateRequest = new WhereClauses();
+        WhereClauses whereClauseForUpdateRequest = new WhereClauses(AND);
         whereClauseForUpdateRequest.addWhereEqualsClause(
                 RecordHelper.UUID_COLUMN_NAME, StorageUtils.getHexString(recordInternal.getUuid()));
         whereClauseForUpdateRequest.addWhereEqualsClause(
diff --git a/service/java/com/android/server/healthconnect/storage/utils/PageTokenUtil.java b/service/java/com/android/server/healthconnect/storage/utils/PageTokenUtil.java
new file mode 100644
index 0000000..8221461
--- /dev/null
+++ b/service/java/com/android/server/healthconnect/storage/utils/PageTokenUtil.java
@@ -0,0 +1,93 @@
+/*
+ * 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.server.healthconnect.storage.utils;
+
+import static android.health.connect.Constants.DEFAULT_LONG;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+
+/**
+ * A util class handles encoding and decoding page token.
+ *
+ * @hide
+ */
+// TODO(b/296846629): Move this util to under framework/, so we can use this on client side, and use
+//  this in {@link ReadRecordsRequestUsingFilters}
+public final class PageTokenUtil {
+    static final long MAX_ALLOWED_TIME_MILLIS = (1L << 44) - 1;
+    static final long MAX_ALLOWED_OFFSET = (1 << 18) - 1;
+
+    private static final int OFFSET_START_BIT = 45;
+    private static final int TIMESTAMP_START_BIT = 1;
+
+    /**
+     * Encodes a {@link PageTokenWrapper} to page token.
+     *
+     * <p>Page token is structured as following from right (least significant bit) to left (most
+     * significant bit):
+     * <li>Least significant bit: 0 = isAscending true, 1 = isAscending false
+     * <li>Next 44 bits: timestamp, represents epoch time millis
+     * <li>Next 18 bits: offset, represents number of records processed in the previous page
+     * <li>Sign bit: not used for encoding, page token is a signed long
+     */
+    public static long encode(PageTokenWrapper wrapper) {
+        return ((long) wrapper.offset() << OFFSET_START_BIT)
+                | (wrapper.timeMillis() << TIMESTAMP_START_BIT)
+                | (wrapper.isAscending() ? 0 : 1);
+    }
+
+    /**
+     * Decodes a {@code pageToken} to {@link PageTokenWrapper}.
+     *
+     * <p>When {@code pageToken} is not set, in which case we can not get {@code isAscending} from
+     * the token, it falls back to {@code defaultIsAscending}.
+     *
+     * <p>{@code pageToken} must be a non-negative long number (except for using the sentinel value
+     * {@code DEFAULT_LONG}, whose current value is {@code -1}, which represents page token not set)
+     */
+    public static PageTokenWrapper decode(long pageToken, boolean defaultIsAscending) {
+        if (pageToken == DEFAULT_LONG) {
+            return PageTokenWrapper.ofAscending(defaultIsAscending);
+        }
+        checkArgument(pageToken >= 0, "pageToken cannot be negative");
+        return PageTokenWrapper.of(
+                getIsAscending(pageToken), getTimestamp(pageToken), getOffset(pageToken));
+    }
+
+    /**
+     * Take the least significant bit in the given {@code pageToken} to retrieve isAscending
+     * information.
+     *
+     * <p>If the last bit of the token is 1, isAscending is false; otherwise isAscending is true.
+     */
+    private static boolean getIsAscending(long pageToken) {
+        return (pageToken & 1) == 0;
+    }
+
+    /** Shifts bits in the given {@code pageToken} to retrieve timestamp information. */
+    private static long getTimestamp(long pageToken) {
+        long mask = MAX_ALLOWED_TIME_MILLIS << TIMESTAMP_START_BIT;
+        return (pageToken & mask) >> TIMESTAMP_START_BIT;
+    }
+
+    /** Shifts bits in the given {@code pageToken} to retrieve offset information. */
+    private static int getOffset(long pageToken) {
+        return (int) (pageToken >> OFFSET_START_BIT);
+    }
+
+    private PageTokenUtil() {}
+}
diff --git a/service/java/com/android/server/healthconnect/storage/utils/PageTokenWrapper.java b/service/java/com/android/server/healthconnect/storage/utils/PageTokenWrapper.java
new file mode 100644
index 0000000..812d63f
--- /dev/null
+++ b/service/java/com/android/server/healthconnect/storage/utils/PageTokenWrapper.java
@@ -0,0 +1,118 @@
+/*
+ * 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.server.healthconnect.storage.utils;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+import static com.android.server.healthconnect.storage.utils.PageTokenUtil.MAX_ALLOWED_OFFSET;
+import static com.android.server.healthconnect.storage.utils.PageTokenUtil.MAX_ALLOWED_TIME_MILLIS;
+
+import static java.lang.Integer.min;
+
+import java.util.Objects;
+
+/**
+ * A wrapper object contains information encoded in the {@code long} page token.
+ *
+ * @hide
+ */
+public final class PageTokenWrapper {
+    private final boolean mIsAscending;
+    private final long mTimeMillis;
+    private final int mOffset;
+    private final boolean mIsTimestampSet;
+
+    /** isAscending stored in the page token. */
+    public boolean isAscending() {
+        return mIsAscending;
+    }
+
+    /** Timestamp stored in the page token. */
+    public long timeMillis() {
+        return mTimeMillis;
+    }
+
+    /** Offset stored in the page token. */
+    public int offset() {
+        return mOffset;
+    }
+
+    /** Whether or not the timestamp is set. */
+    public boolean isTimestampSet() {
+        return mIsTimestampSet;
+    }
+
+    /**
+     * Both {@code timeMillis} and {@code offset} have to be non-negative; {@code timeMillis} cannot
+     * exceed 2^44-1.
+     *
+     * <p>Note that due to space constraints, {@code offset} cannot exceed 2^18-1 (262143). If the
+     * {@code offset} parameter exceeds the maximum allowed value, it'll fallback to the max value.
+     *
+     * <p>More details see go/hc-page-token
+     */
+    public static PageTokenWrapper of(boolean isAscending, long timeMillis, int offset) {
+        checkArgument(timeMillis >= 0, "timestamp can not be negative");
+        checkArgument(timeMillis <= MAX_ALLOWED_TIME_MILLIS, "timestamp too large");
+        checkArgument(offset >= 0, "offset can not be negative");
+        int boundedOffset = min((int) MAX_ALLOWED_OFFSET, offset);
+        return new PageTokenWrapper(
+                isAscending, timeMillis, boundedOffset, /* isTimestampSet= */ true);
+    }
+
+    /**
+     * Generate a page token that contains only {@code isAscending} information. Timestamp and
+     * offset are not set.
+     */
+    public static PageTokenWrapper ofAscending(boolean isAscending) {
+        return new PageTokenWrapper(
+                isAscending, /* timeMillis= */ 0, /* offset= */ 0, /* isTimestampSet= */ false);
+    }
+
+    @Override
+    public String toString() {
+        return "PageTokenWrapper{"
+                + "isAscending = "
+                + mIsAscending
+                + ", timeMillis = "
+                + mTimeMillis
+                + ", offset = "
+                + mOffset
+                + "}";
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof PageTokenWrapper that)) return false;
+        return mIsAscending == that.mIsAscending
+                && mTimeMillis == that.mTimeMillis
+                && mOffset == that.mOffset;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mIsAscending, mOffset, mTimeMillis);
+    }
+
+    private PageTokenWrapper(
+            boolean isAscending, long timeMillis, int offset, boolean isTimestampSet) {
+        this.mIsAscending = isAscending;
+        this.mTimeMillis = timeMillis;
+        this.mOffset = offset;
+        this.mIsTimestampSet = isTimestampSet;
+    }
+}
diff --git a/service/java/com/android/server/healthconnect/storage/utils/WhereClauses.java b/service/java/com/android/server/healthconnect/storage/utils/WhereClauses.java
index 37179f6..a517409 100644
--- a/service/java/com/android/server/healthconnect/storage/utils/WhereClauses.java
+++ b/service/java/com/android/server/healthconnect/storage/utils/WhereClauses.java
@@ -19,13 +19,29 @@
 import com.android.server.healthconnect.storage.request.ReadTableRequest;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 import java.util.stream.Collectors;
 
 /** @hide */
 public final class WhereClauses {
+    public enum LogicalOperator {
+        AND(" AND "),
+        OR(" OR ");
+
+        private final String opKeyword;
+
+        LogicalOperator(String opKeyword) {
+            this.opKeyword = opKeyword;
+        }
+    }
+
     private final List<String> mClauses = new ArrayList<>();
-    private boolean mUseOr = false;
+    private final LogicalOperator mLogicalOperator;
+
+    public WhereClauses(LogicalOperator logicalOperator) {
+        mLogicalOperator = logicalOperator;
+    }
 
     public WhereClauses addWhereBetweenClause(String columnName, long start, long end) {
         mClauses.add(columnName + " BETWEEN " + start + " AND " + end);
@@ -126,18 +142,21 @@
     }
 
     /**
-     * Adds where in condition for the column
+     * Adds where in condition for the column.
      *
      * @param columnName Column name on which where condition to be applied
      * @param values to check in the where condition
      */
-    public WhereClauses addWhereInLongsClause(String columnName, List<Long> values) {
+    public WhereClauses addWhereInLongsClause(String columnName, Collection<Long> values) {
         if (values == null || values.isEmpty()) return this;
 
         mClauses.add(
                 columnName
                         + " IN ("
-                        + values.stream().map(String::valueOf).collect(Collectors.joining(", "))
+                        + values.stream()
+                                .distinct()
+                                .map(String::valueOf)
+                                .collect(Collectors.joining(", "))
                         + ")");
 
         return this;
@@ -153,6 +172,20 @@
         return this;
     }
 
+    /** Adds other {@link WhereClauses} as conditions of this where clause. */
+    public WhereClauses addNestedWhereClauses(WhereClauses... otherWhereClauses) {
+        for (WhereClauses whereClauses : otherWhereClauses) {
+            if (whereClauses.mClauses.isEmpty()) {
+                // skip empty WhereClauses so we don't add empty pairs of parentheses "()" to the
+                // final SQL statement
+                continue;
+            }
+            mClauses.add("(" + whereClauses.get(/* withWhereKeyword= */ false) + ")");
+        }
+
+        return this;
+    }
+
     /**
      * Returns where clauses joined by 'AND', if the input parameter isIncludeWHEREinClauses is true
      * then the clauses are preceded by 'WHERE'.
@@ -162,16 +195,7 @@
             return "";
         }
 
-        return (withWhereKeyword ? " WHERE " : "") + String.join(getJoinClause(), mClauses);
-    }
-
-    private String getJoinClause() {
-        return mUseOr ? " OR " : " AND ";
-    }
-
-    public WhereClauses setUseOr(boolean useOr) {
-        mUseOr = useOr;
-
-        return this;
+        return (withWhereKeyword ? " WHERE " : "")
+                + String.join(mLogicalOperator.opKeyword, mClauses);
     }
 }
diff --git a/testapps/toolbox/AndroidManifest.xml b/testapps/toolbox/AndroidManifest.xml
index 9b9b275..f9c1c06 100644
--- a/testapps/toolbox/AndroidManifest.xml
+++ b/testapps/toolbox/AndroidManifest.xml
@@ -31,6 +31,7 @@
   <uses-permission android:name="android.permission.health.READ_DISTANCE" />
   <uses-permission android:name="android.permission.health.READ_ELEVATION_GAINED" />
   <uses-permission android:name="android.permission.health.READ_EXERCISE" />
+  <uses-permission android:name="android.permission.health.READ_EXERCISE_ROUTES_ALL" />
   <uses-permission android:name="android.permission.health.READ_FLOORS_CLIMBED" />
   <uses-permission android:name="android.permission.health.READ_HEART_RATE" />
   <uses-permission android:name="android.permission.health.READ_HEART_RATE_VARIABILITY" />
diff --git a/testapps/toolbox/src/com/android/healthconnect/testapps/toolbox/Constants.kt b/testapps/toolbox/src/com/android/healthconnect/testapps/toolbox/Constants.kt
index 94703c4..9cc6f93 100644
--- a/testapps/toolbox/src/com/android/healthconnect/testapps/toolbox/Constants.kt
+++ b/testapps/toolbox/src/com/android/healthconnect/testapps/toolbox/Constants.kt
@@ -82,6 +82,7 @@
             "android.permission.health.READ_DISTANCE",
             "android.permission.health.READ_ELEVATION_GAINED",
             "android.permission.health.READ_EXERCISE",
+            "android.permission.health.READ_EXERCISE_ROUTES_ALL",
             "android.permission.health.READ_FLOORS_CLIMBED",
             "android.permission.health.READ_HEART_RATE",
             "android.permission.health.READ_HEART_RATE_VARIABILITY",
diff --git a/tests/cts/Android.bp b/tests/cts/Android.bp
index 76e53b1..e0b35de 100644
--- a/tests/cts/Android.bp
+++ b/tests/cts/Android.bp
@@ -28,6 +28,7 @@
     ],
     srcs: [
         ":healthfitness-cts-testapp-srcs",
+        ":healthfitness-permissions-testapp-srcs",
         "src/android/healthconnect/cts/*.java",
     ],
     // Tag this module as a cts test artifact
@@ -42,14 +43,16 @@
         "compatibility-device-util-axt",
         "ctstestrunner-axt",
         "cts-wm-util",
-	"modules-utils-build",
+        "modules-utils-build",
         "testng",
+        "cts-healthconnect-utils",
     ],
     min_sdk_version: "34",
     sdk_version: "module_current",
     data: [
         ":HealthFitnessCtsTestApp",
         ":HealthFitnessCtsTestApp2",
+        ":HealthFitnessPermsTestApp",
     ],
 }
 
@@ -63,7 +66,6 @@
     ],
     srcs: [
         "src/android/healthconnect/cts/nopermission/*.java",
-        "src/android/healthconnect/cts/TestUtils.java",
     ],
     // Tag this module as a cts test artifact
     test_suites: [
@@ -78,6 +80,7 @@
         "ctstestrunner-axt",
         "cts-wm-util",
         "testng",
+        "cts-healthconnect-utils",
     ],
     min_sdk_version: "34",
     sdk_version: "module_current",
@@ -97,7 +100,6 @@
     ],
     srcs: [
         "src/android/healthconnect/cts/ratelimiter/*.java",
-        "src/android/healthconnect/cts/TestUtils.java",
     ],
     // Tag this module as a cts test artifact
     test_suites: [
@@ -113,6 +115,7 @@
         "cts-wm-util",
 	"modules-utils-build",
         "testng",
+        "cts-healthconnect-utils",
     ],
     min_sdk_version: "34",
     sdk_version: "module_current",
@@ -132,7 +135,6 @@
         ":healthfitness-cts-testapp-srcs",
         ":healthfitness-cts-testapp2-srcs",
         "src/android/healthconnect/cts/ui/**/*.kt",
-        "src/android/healthconnect/cts/TestUtils.java",
         "src/com/android/cts/install/lib/*.java",
     ],
     // Tag this module as a cts test artifact
@@ -149,7 +151,8 @@
         "cts-wm-util",
         "testng",
         "cts-healthconnect-lib",
-        "cts-install-lib"
+        "cts-install-lib",
+        "cts-healthconnect-utils",
     ],
     min_sdk_version: "34",
     sdk_version: "module_current",
@@ -174,7 +177,6 @@
     ],
     srcs: [
         "src/android/healthconnect/cts/showmigrationinfointent/ShowMigrationInfoIntentAbsentTest.java",
-        "src/android/healthconnect/cts/TestUtils.java",
     ],
     // Tag this module as a cts test artifact
     test_suites: [
@@ -190,6 +192,7 @@
         "cts-wm-util",
 	"modules-utils-build",
         "testng",
+        "cts-healthconnect-utils",
     ],
     min_sdk_version: "34",
     sdk_version: "module_current",
diff --git a/tests/cts/AndroidManifest.xml b/tests/cts/AndroidManifest.xml
index 8f5e129..3d847ec 100644
--- a/tests/cts/AndroidManifest.xml
+++ b/tests/cts/AndroidManifest.xml
@@ -34,6 +34,9 @@
               <action android:name="android.health.connect.action.SHOW_MIGRATION_INFO"/>
           </intent-filter>
       </activity>
+
+      <receiver android:name="android.healthconnect.cts.utils.TestReceiver"
+          android:exported="true"/>
     </application>
 
     <!-- To get visibility of the app with health permissions definitions,
@@ -48,6 +51,7 @@
     <queries>
         <package android:name="android.healthconnect.cts.app"/>
         <package android:name="android.healthconnect.cts.app2"/>
+        <package android:name="android.healthconnect.test.app"/>
     </queries>
 
     <uses-permission android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED"/>
diff --git a/tests/cts/AndroidTest.xml b/tests/cts/AndroidTest.xml
index ec53d5a..16e75e1 100644
--- a/tests/cts/AndroidTest.xml
+++ b/tests/cts/AndroidTest.xml
@@ -25,6 +25,7 @@
         <option name="test-file-name" value="CtsHealthFitnessDeviceTestCases.apk"/>
         <option name="test-file-name" value="HealthFitnessCtsTestApp.apk"/>
         <option name="test-file-name" value="HealthFitnessCtsTestApp2.apk"/>
+        <option name="test-file-name" value="HealthFitnessPermsTestApp.apk"/>
     </target_preparer>
 
     <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
diff --git a/tests/cts/hostsidetests/healthconnect/Android.bp b/tests/cts/hostsidetests/healthconnect/Android.bp
index d9c0208..a276766 100644
--- a/tests/cts/hostsidetests/healthconnect/Android.bp
+++ b/tests/cts/hostsidetests/healthconnect/Android.bp
@@ -97,6 +97,7 @@
         "ctstestrunner-axt",
         "cts-wm-util",
         "testng",
+        "cts-healthconnect-utils",
     ],
     test_suites: [
         "cts",
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 be390d4..b882b89 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
@@ -16,33 +16,43 @@
 
 package android.healthconnect.cts.testhelper;
 
-import static android.healthconnect.cts.lib.TestUtils.APP_PKG_NAME_USED_IN_DATA_ORIGIN;
-import static android.healthconnect.cts.lib.TestUtils.CHANGE_LOGS_RESPONSE;
-import static android.healthconnect.cts.lib.TestUtils.CHANGE_LOG_TOKEN;
-import static android.healthconnect.cts.lib.TestUtils.CLIENT_ID;
-import static android.healthconnect.cts.lib.TestUtils.DELETE_RECORDS_QUERY;
-import static android.healthconnect.cts.lib.TestUtils.GET_CHANGE_LOG_TOKEN_QUERY;
-import static android.healthconnect.cts.lib.TestUtils.INSERT_RECORD_QUERY;
-import static android.healthconnect.cts.lib.TestUtils.INTENT_EXCEPTION;
-import static android.healthconnect.cts.lib.TestUtils.QUERY_TYPE;
-import static android.healthconnect.cts.lib.TestUtils.READ_CHANGE_LOGS_QUERY;
-import static android.healthconnect.cts.lib.TestUtils.READ_RECORDS_QUERY;
-import static android.healthconnect.cts.lib.TestUtils.READ_RECORDS_SIZE;
-import static android.healthconnect.cts.lib.TestUtils.READ_RECORD_CLASS_NAME;
-import static android.healthconnect.cts.lib.TestUtils.READ_USING_DATA_ORIGIN_FILTERS;
-import static android.healthconnect.cts.lib.TestUtils.RECORD_IDS;
-import static android.healthconnect.cts.lib.TestUtils.SUCCESS;
-import static android.healthconnect.cts.lib.TestUtils.UPDATE_EXERCISE_ROUTE;
-import static android.healthconnect.cts.lib.TestUtils.UPDATE_RECORDS_QUERY;
-import static android.healthconnect.cts.lib.TestUtils.UPSERT_EXERCISE_ROUTE;
-import static android.healthconnect.cts.lib.TestUtils.getChangeLogs;
-import static android.healthconnect.cts.lib.TestUtils.getExerciseSessionRecord;
-import static android.healthconnect.cts.lib.TestUtils.getTestRecords;
-import static android.healthconnect.cts.lib.TestUtils.insertRecords;
-import static android.healthconnect.cts.lib.TestUtils.insertRecordsAndGetIds;
-import static android.healthconnect.cts.lib.TestUtils.readRecords;
-import static android.healthconnect.cts.lib.TestUtils.updateRecords;
-import static android.healthconnect.cts.lib.TestUtils.verifyDeleteRecords;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.APP_PKG_NAME_USED_IN_DATA_ORIGIN;
+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.DELETE_RECORDS_QUERY;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.END_TIME;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.EXERCISE_SESSION;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.GET_CHANGE_LOG_TOKEN_QUERY;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.INSERT_RECORD_QUERY;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.INTENT_EXCEPTION;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.PAUSE_END;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.PAUSE_START;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.QUERY_TYPE;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.READ_CHANGE_LOGS_QUERY;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.READ_RECORDS_QUERY;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.READ_RECORDS_SIZE;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.READ_RECORD_CLASS_NAME;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.READ_USING_DATA_ORIGIN_FILTERS;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.RECORD_IDS;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.RECORD_TYPE;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.START_TIME;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.STEPS_COUNT;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.STEPS_RECORD;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.SUCCESS;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.UPDATE_EXERCISE_ROUTE;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.UPDATE_RECORDS_QUERY;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.UPSERT_EXERCISE_ROUTE;
+import static android.healthconnect.cts.utils.TestUtils.buildExerciseSession;
+import static android.healthconnect.cts.utils.TestUtils.buildStepsRecord;
+import static android.healthconnect.cts.utils.TestUtils.getChangeLogs;
+import static android.healthconnect.cts.utils.TestUtils.getExerciseSessionRecord;
+import static android.healthconnect.cts.utils.TestUtils.getTestRecords;
+import static android.healthconnect.cts.utils.TestUtils.insertRecords;
+import static android.healthconnect.cts.utils.TestUtils.insertRecordsAndGetIds;
+import static android.healthconnect.cts.utils.TestUtils.readRecords;
+import static android.healthconnect.cts.utils.TestUtils.updateRecords;
+import static android.healthconnect.cts.utils.TestUtils.verifyDeleteRecords;
 
 import android.app.Activity;
 import android.content.Context;
@@ -56,11 +66,12 @@
 import android.health.connect.datatypes.DataOrigin;
 import android.health.connect.datatypes.ExerciseSessionRecord;
 import android.health.connect.datatypes.Record;
-import android.healthconnect.cts.lib.TestUtils;
+import android.healthconnect.cts.utils.TestUtils;
 import android.os.Bundle;
 
 import java.io.Serializable;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 public class HealthConnectTestHelper extends Activity {
@@ -90,6 +101,28 @@
                                         queryType, bundle.getDouble(CLIENT_ID), context);
                         break;
                     }
+                    if (bundle.containsKey(RECORD_TYPE)) {
+                        if (bundle.getString(RECORD_TYPE).equals(STEPS_RECORD)) {
+                            returnIntent =
+                                    insertStepsRecord(
+                                            queryType,
+                                            bundle.getString(START_TIME),
+                                            bundle.getString(END_TIME),
+                                            bundle.getInt(STEPS_COUNT),
+                                            context);
+                            break;
+                        } else if (bundle.getString(RECORD_TYPE).equals(EXERCISE_SESSION)) {
+                            returnIntent =
+                                    insertExerciseSession(
+                                            queryType,
+                                            bundle.getString(START_TIME),
+                                            bundle.getString(END_TIME),
+                                            bundle.getString(PAUSE_START),
+                                            bundle.getString(PAUSE_END),
+                                            context);
+                            break;
+                        }
+                    }
                     returnIntent = insertRecord(queryType, context);
                     break;
                 case DELETE_RECORDS_QUERY:
@@ -135,6 +168,15 @@
                                     queryType, bundle.getString(CHANGE_LOG_TOKEN), context);
                     break;
                 case GET_CHANGE_LOG_TOKEN_QUERY:
+                    if (bundle.containsKey(READ_RECORD_CLASS_NAME)) {
+                        returnIntent =
+                                getChangeLogToken(
+                                        queryType,
+                                        bundle.getString(APP_PKG_NAME_USED_IN_DATA_ORIGIN),
+                                        bundle.getStringArrayList(READ_RECORD_CLASS_NAME),
+                                        context);
+                        break;
+                    }
                     returnIntent =
                             getChangeLogToken(
                                     queryType,
@@ -496,4 +538,106 @@
         intent.putExtra(CHANGE_LOG_TOKEN, tokenResponse.getToken());
         return intent;
     }
+
+    /**
+     * Method to get changeLogToken for an app
+     *
+     * @param queryType - specifies the action, here it should be GET_CHANGE_LOG_TOKEN_QUERY
+     * @param pkgName - pkgName of the app whose changeLogs we have to read using the returned token
+     * @param recordClassesToRead - Record Classes whose changeLogs to be read using the returned
+     *     token
+     * @param context - application context
+     * @return - Intent to send back to the main app which is running the tests
+     */
+    private Intent getChangeLogToken(
+            String queryType,
+            String pkgName,
+            ArrayList<String> recordClassesToRead,
+            Context context)
+            throws Exception {
+        final Intent intent = new Intent(queryType);
+
+        ChangeLogTokenRequest.Builder changeLogTokenRequestBuilder =
+                new ChangeLogTokenRequest.Builder()
+                        .addDataOriginFilter(
+                                new DataOrigin.Builder().setPackageName(pkgName).build());
+        for (String recordClass : recordClassesToRead) {
+            changeLogTokenRequestBuilder.addRecordType(
+                    (Class<? extends Record>) Class.forName(recordClass));
+        }
+        ChangeLogTokenResponse tokenResponse =
+                TestUtils.getChangeLogToken(changeLogTokenRequestBuilder.build(), context);
+
+        intent.putExtra(CHANGE_LOG_TOKEN, tokenResponse.getToken());
+        return intent;
+    }
+
+    /**
+     * Method to build steps record and insert them
+     *
+     * @param queryType - specifies the action, here it should be INSERT_RECORDS_QUERY
+     * @param startTime - start time for the steps record to build
+     * @param endTime - end time for the steps record to build
+     * @param stepsCount - number of steps to be added in the steps record
+     * @param context - application context
+     * @return Intent to send back to the main app which is running the tests
+     */
+    private Intent insertStepsRecord(
+            String queryType, String startTime, String endTime, int stepsCount, Context context) {
+        final Intent intent = new Intent(queryType);
+        try {
+            List<Record> recordToInsert =
+                    Arrays.asList(
+                            buildStepsRecord(
+                                    startTime, endTime, stepsCount, context.getPackageName()));
+            insertRecords(recordToInsert, context);
+            intent.putExtra(SUCCESS, true);
+        } catch (Exception e) {
+            intent.putExtra(SUCCESS, false);
+        }
+        return intent;
+    }
+
+    /**
+     * Method to build Exercise Session records and insert them
+     *
+     * @param queryType - specifies the action, here it should be INSERT_RECORDS_QUERY
+     * @param sessionStartTime - start time of the exercise session to build
+     * @param sessionEndTime - end time of the exercise session t build
+     * @param pauseStart - start time of the pause segment in the exercise session
+     * @param pauseEnd - end time of the pause segment in the exercise session
+     * @param context - application context
+     * @return Intent to send back to the main app which is running the tests
+     */
+    private Intent insertExerciseSession(
+            String queryType,
+            String sessionStartTime,
+            String sessionEndTime,
+            String pauseStart,
+            String pauseEnd,
+            Context context) {
+        final Intent intent = new Intent(queryType);
+        try {
+            List<Record> recordToInsert;
+            if (pauseStart == null) {
+                recordToInsert =
+                        Arrays.asList(
+                                buildExerciseSession(sessionStartTime, sessionEndTime, context));
+            } else {
+                recordToInsert =
+                        Arrays.asList(
+                                buildExerciseSession(
+                                        sessionStartTime,
+                                        sessionEndTime,
+                                        pauseStart,
+                                        pauseEnd,
+                                        context));
+            }
+            insertRecords(recordToInsert, context);
+            intent.putExtra(SUCCESS, true);
+        } catch (Exception e) {
+            intent.putExtra(SUCCESS, false);
+        }
+        return intent;
+    }
 }
diff --git a/tests/cts/hostsidetests/healthconnect/device/src/android/healthconnect/cts/device/ExerciseRouteAccessTest.java b/tests/cts/hostsidetests/healthconnect/device/src/android/healthconnect/cts/device/ExerciseRouteAccessTest.java
index f99d54f..fae49f2 100644
--- a/tests/cts/hostsidetests/healthconnect/device/src/android/healthconnect/cts/device/ExerciseRouteAccessTest.java
+++ b/tests/cts/hostsidetests/healthconnect/device/src/android/healthconnect/cts/device/ExerciseRouteAccessTest.java
@@ -18,15 +18,23 @@
 
 import static android.health.connect.HealthPermissions.WRITE_EXERCISE_ROUTE;
 import static android.healthconnect.cts.device.HealthConnectDeviceTest.APP_A_WITH_READ_WRITE_PERMS;
-import static android.healthconnect.cts.lib.TestUtils.READ_RECORDS_SIZE;
-import static android.healthconnect.cts.lib.TestUtils.SUCCESS;
-import static android.healthconnect.cts.lib.TestUtils.deleteAllStagedRemoteData;
-import static android.healthconnect.cts.lib.TestUtils.deleteTestData;
-import static android.healthconnect.cts.lib.TestUtils.insertRecordAs;
-import static android.healthconnect.cts.lib.TestUtils.insertSessionNoRouteAs;
-import static android.healthconnect.cts.lib.TestUtils.readRecords;
-import static android.healthconnect.cts.lib.TestUtils.readRecordsAs;
-import static android.healthconnect.cts.lib.TestUtils.updateRouteAs;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.READ_RECORDS_SIZE;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.RECORD_IDS;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.SUCCESS;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.insertRecordAs;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.insertSessionNoRouteAs;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.readRecordsAs;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.updateRouteAs;
+import static android.healthconnect.cts.utils.TestUtils.READ_EXERCISE_ROUTE_PERMISSION;
+import static android.healthconnect.cts.utils.TestUtils.deleteAllStagedRemoteData;
+import static android.healthconnect.cts.utils.TestUtils.deleteTestData;
+import static android.healthconnect.cts.utils.TestUtils.getChangeLogToken;
+import static android.healthconnect.cts.utils.TestUtils.getChangeLogs;
+import static android.healthconnect.cts.utils.TestUtils.getExerciseSessionRecord;
+import static android.healthconnect.cts.utils.TestUtils.insertRecords;
+import static android.healthconnect.cts.utils.TestUtils.readRecords;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
 
 import static com.android.compatibility.common.util.FeatureUtil.AUTOMOTIVE_FEATURE;
 import static com.android.compatibility.common.util.FeatureUtil.hasSystemFeature;
@@ -36,7 +44,12 @@
 import android.app.UiAutomation;
 import android.health.connect.HealthConnectException;
 import android.health.connect.ReadRecordsRequestUsingFilters;
+import android.health.connect.ReadRecordsRequestUsingIds;
+import android.health.connect.changelog.ChangeLogTokenRequest;
+import android.health.connect.changelog.ChangeLogsRequest;
+import android.health.connect.changelog.ChangeLogsResponse;
 import android.health.connect.datatypes.ExerciseSessionRecord;
+import android.healthconnect.cts.utils.TestUtils.RecordTypeAndRecordIds;
 import android.os.Bundle;
 
 import androidx.test.platform.app.InstrumentationRegistry;
@@ -48,7 +61,11 @@
 import org.junit.Test;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 public class ExerciseRouteAccessTest {
 
@@ -73,7 +90,7 @@
     }
 
     @Test
-    public void testRouteRead_cannotAccessOtherAppRoute() throws Exception {
+    public void readRecords_usingFilters_cannotAccessOtherAppRoute() throws Exception {
         assertThat(insertRecordAs(APP_A_WITH_READ_WRITE_PERMS).getBoolean(SUCCESS)).isTrue();
 
         List<ExerciseSessionRecord> records =
@@ -88,6 +105,290 @@
     }
 
     @Test
+    public void readRecords_usingFilters_withReadExerciseRoutePermission_canAccessOtherAppRoute()
+            throws Exception {
+        assertThat(insertRecordAs(APP_A_WITH_READ_WRITE_PERMS).getBoolean(SUCCESS)).isTrue();
+        mAutomation.adoptShellPermissionIdentity(READ_EXERCISE_ROUTE_PERMISSION);
+
+        List<ExerciseSessionRecord> records =
+                readRecords(
+                        new ReadRecordsRequestUsingFilters.Builder<>(ExerciseSessionRecord.class)
+                                .build());
+
+        assertThat(records).isNotNull();
+        assertThat(records).hasSize(1);
+        assertThat(records.get(0).hasRoute()).isTrue();
+        assertThat(records.get(0).getRoute()).isNotNull();
+    }
+
+    @Test
+    public void readRecords_usingFilters_canAccessOwnRoute() throws Exception {
+        ExerciseSessionRecord record =
+                getExerciseSessionRecord(
+                        getApplicationContext().getPackageName(), 0.0, /* withRoute= */ true);
+        insertRecords(List.of(record), getApplicationContext());
+
+        List<ExerciseSessionRecord> records =
+                readRecords(
+                        new ReadRecordsRequestUsingFilters.Builder<>(ExerciseSessionRecord.class)
+                                .build());
+
+        assertThat(records).isNotNull();
+        assertThat(records).hasSize(1);
+        assertThat(records.get(0).hasRoute()).isTrue();
+        assertThat(records.get(0).getRoute()).isEqualTo(record.getRoute());
+    }
+
+    @Test
+    public void readRecords_usingFilters_mixedOwnAndOtherAppSession() throws Exception {
+        Bundle bundle = insertRecordAs(APP_A_WITH_READ_WRITE_PERMS);
+        assertThat(bundle.getBoolean(SUCCESS)).isTrue();
+        String otherAppSessionId = getInsertedSessionId(bundle);
+        ExerciseSessionRecord ownSession =
+                getExerciseSessionRecord(
+                        getApplicationContext().getPackageName(), 0.0, /* withRoute= */ true);
+        String ownSessionId =
+                insertRecords(List.of(ownSession), getApplicationContext())
+                        .get(0)
+                        .getMetadata()
+                        .getId();
+
+        List<ExerciseSessionRecord> records =
+                readRecords(
+                        new ReadRecordsRequestUsingFilters.Builder<>(ExerciseSessionRecord.class)
+                                .build());
+
+        Map<String, ExerciseSessionRecord> idToRecordMap =
+                records.stream()
+                        .collect(
+                                Collectors.toMap(
+                                        record -> record.getMetadata().getId(),
+                                        Function.identity()));
+        assertThat(records).isNotNull();
+        assertThat(records).hasSize(2);
+        assertThat(idToRecordMap.get(otherAppSessionId).hasRoute()).isTrue();
+        assertThat(idToRecordMap.get(otherAppSessionId).getRoute()).isNull();
+        assertThat(idToRecordMap.get(ownSessionId).hasRoute()).isTrue();
+        assertThat(idToRecordMap.get(ownSessionId).getRoute()).isEqualTo(ownSession.getRoute());
+    }
+
+    @Test
+    public void readRecords_usingIds_cannotAccessOtherAppRoute() throws Exception {
+        Bundle bundle = insertRecordAs(APP_A_WITH_READ_WRITE_PERMS);
+        assertThat(bundle.getBoolean(SUCCESS)).isTrue();
+        String sessionId = getInsertedSessionId(bundle);
+
+        List<ExerciseSessionRecord> records =
+                readRecords(
+                        new ReadRecordsRequestUsingIds.Builder<>(ExerciseSessionRecord.class)
+                                .addId(sessionId)
+                                .build());
+
+        assertThat(records).isNotNull();
+        assertThat(records).hasSize(1);
+        assertThat(records.get(0).hasRoute()).isTrue();
+        assertThat(records.get(0).getRoute()).isNull();
+    }
+
+    @Test
+    public void readRecords_usingIds_withReadExerciseRoutePermission_canAccessOtherAppRoute()
+            throws Exception {
+        Bundle bundle = insertRecordAs(APP_A_WITH_READ_WRITE_PERMS);
+        assertThat(bundle.getBoolean(SUCCESS)).isTrue();
+        String sessionId = getInsertedSessionId(bundle);
+        mAutomation.adoptShellPermissionIdentity(READ_EXERCISE_ROUTE_PERMISSION);
+
+        List<ExerciseSessionRecord> records =
+                readRecords(
+                        new ReadRecordsRequestUsingIds.Builder<>(ExerciseSessionRecord.class)
+                                .addId(sessionId)
+                                .build());
+
+        assertThat(records).isNotNull();
+        assertThat(records).hasSize(1);
+        assertThat(records.get(0).hasRoute()).isTrue();
+        assertThat(records.get(0).getRoute()).isNotNull();
+    }
+
+    @Test
+    public void readRecords_usingIds_canAccessOwnRoute() throws Exception {
+        ExerciseSessionRecord record =
+                getExerciseSessionRecord(
+                        getApplicationContext().getPackageName(), 0.0, /* withRoute= */ true);
+        String sessionId =
+                insertRecords(List.of(record), getApplicationContext())
+                        .get(0)
+                        .getMetadata()
+                        .getId();
+
+        List<ExerciseSessionRecord> records =
+                readRecords(
+                        new ReadRecordsRequestUsingIds.Builder<>(ExerciseSessionRecord.class)
+                                .addId(sessionId)
+                                .build());
+
+        assertThat(records).isNotNull();
+        assertThat(records).hasSize(1);
+        assertThat(records.get(0).hasRoute()).isTrue();
+        assertThat(records.get(0).getRoute()).isEqualTo(record.getRoute());
+    }
+
+    @Test
+    public void readRecords_usingIds_mixedOwnAndOtherAppSession() throws Exception {
+        Bundle bundle = insertRecordAs(APP_A_WITH_READ_WRITE_PERMS);
+        assertThat(bundle.getBoolean(SUCCESS)).isTrue();
+        String otherAppSessionId = getInsertedSessionId(bundle);
+        ExerciseSessionRecord ownSession =
+                getExerciseSessionRecord(
+                        getApplicationContext().getPackageName(), 0.0, /* withRoute= */ true);
+        String ownSessionId =
+                insertRecords(List.of(ownSession), getApplicationContext())
+                        .get(0)
+                        .getMetadata()
+                        .getId();
+
+        List<ExerciseSessionRecord> records =
+                readRecords(
+                        new ReadRecordsRequestUsingIds.Builder<>(ExerciseSessionRecord.class)
+                                .addId(otherAppSessionId)
+                                .addId(ownSessionId)
+                                .build());
+
+        Map<String, ExerciseSessionRecord> idToRecordMap =
+                records.stream()
+                        .collect(
+                                Collectors.toMap(
+                                        record -> record.getMetadata().getId(),
+                                        Function.identity()));
+        assertThat(records).isNotNull();
+        assertThat(records).hasSize(2);
+        assertThat(idToRecordMap.get(otherAppSessionId).hasRoute()).isTrue();
+        assertThat(idToRecordMap.get(otherAppSessionId).getRoute()).isNull();
+        assertThat(idToRecordMap.get(ownSessionId).hasRoute()).isTrue();
+        assertThat(idToRecordMap.get(ownSessionId).getRoute()).isEqualTo(ownSession.getRoute());
+    }
+
+    @Test
+    public void getChangelogs_cannotAccessOtherAppRoute() throws Exception {
+        String token =
+                getChangeLogToken(
+                                new ChangeLogTokenRequest.Builder()
+                                        .addRecordType(ExerciseSessionRecord.class)
+                                        .build(),
+                                getApplicationContext())
+                        .getToken();
+        assertThat(insertRecordAs(APP_A_WITH_READ_WRITE_PERMS).getBoolean(SUCCESS)).isTrue();
+
+        ChangeLogsResponse response =
+                getChangeLogs(
+                        new ChangeLogsRequest.Builder(token).build(), getApplicationContext());
+
+        List<ExerciseSessionRecord> records =
+                response.getUpsertedRecords().stream()
+                        .map(ExerciseSessionRecord.class::cast)
+                        .toList();
+        assertThat(records).isNotNull();
+        assertThat(records).hasSize(1);
+        assertThat(records.get(0).hasRoute()).isTrue();
+        assertThat(records.get(0).getRoute()).isNull();
+    }
+
+    @Test
+    public void getChangelogs_withReadExerciseRoutePermission_canAccessOtherAppRoute()
+            throws Exception {
+        String token =
+                getChangeLogToken(
+                                new ChangeLogTokenRequest.Builder()
+                                        .addRecordType(ExerciseSessionRecord.class)
+                                        .build(),
+                                getApplicationContext())
+                        .getToken();
+        assertThat(insertRecordAs(APP_A_WITH_READ_WRITE_PERMS).getBoolean(SUCCESS)).isTrue();
+        mAutomation.adoptShellPermissionIdentity(READ_EXERCISE_ROUTE_PERMISSION);
+
+        ChangeLogsResponse response =
+                getChangeLogs(
+                        new ChangeLogsRequest.Builder(token).build(), getApplicationContext());
+
+        List<ExerciseSessionRecord> records =
+                response.getUpsertedRecords().stream()
+                        .map(ExerciseSessionRecord.class::cast)
+                        .toList();
+        assertThat(records).isNotNull();
+        assertThat(records).hasSize(1);
+        assertThat(records.get(0).hasRoute()).isTrue();
+        assertThat(records.get(0).getRoute()).isNotNull();
+    }
+
+    @Test
+    public void getChangelogs_canAccessOwnRoute() throws Exception {
+        String token =
+                getChangeLogToken(
+                                new ChangeLogTokenRequest.Builder()
+                                        .addRecordType(ExerciseSessionRecord.class)
+                                        .build(),
+                                getApplicationContext())
+                        .getToken();
+        ExerciseSessionRecord record =
+                getExerciseSessionRecord(
+                        getApplicationContext().getPackageName(), 0.0, /* withRoute= */ true);
+        insertRecords(List.of(record), getApplicationContext());
+
+        ChangeLogsResponse response =
+                getChangeLogs(
+                        new ChangeLogsRequest.Builder(token).build(), getApplicationContext());
+
+        List<ExerciseSessionRecord> records =
+                response.getUpsertedRecords().stream()
+                        .map(ExerciseSessionRecord.class::cast)
+                        .toList();
+        assertThat(records).isNotNull();
+        assertThat(records).hasSize(1);
+        assertThat(records.get(0).hasRoute()).isTrue();
+        assertThat(records.get(0).getRoute()).isEqualTo(record.getRoute());
+    }
+
+    @Test
+    public void getChangelogs_mixedOwnAndOtherAppSession() throws Exception {
+        String token =
+                getChangeLogToken(
+                                new ChangeLogTokenRequest.Builder()
+                                        .addRecordType(ExerciseSessionRecord.class)
+                                        .build(),
+                                getApplicationContext())
+                        .getToken();
+        Bundle bundle = insertRecordAs(APP_A_WITH_READ_WRITE_PERMS);
+        assertThat(bundle.getBoolean(SUCCESS)).isTrue();
+        String otherAppSessionId = getInsertedSessionId(bundle);
+        ExerciseSessionRecord ownSession =
+                getExerciseSessionRecord(
+                        getApplicationContext().getPackageName(), 0.0, /* withRoute= */ true);
+        String ownSessionId =
+                insertRecords(List.of(ownSession), getApplicationContext())
+                        .get(0)
+                        .getMetadata()
+                        .getId();
+
+        ChangeLogsResponse response =
+                getChangeLogs(
+                        new ChangeLogsRequest.Builder(token).build(), getApplicationContext());
+
+        Map<String, ExerciseSessionRecord> idToRecordMap =
+                response.getUpsertedRecords().stream()
+                        .map(ExerciseSessionRecord.class::cast)
+                        .collect(
+                                Collectors.toMap(
+                                        record -> record.getMetadata().getId(),
+                                        Function.identity()));
+        assertThat(response.getUpsertedRecords()).isNotNull();
+        assertThat(response.getUpsertedRecords()).hasSize(2);
+        assertThat(idToRecordMap.get(otherAppSessionId).hasRoute()).isTrue();
+        assertThat(idToRecordMap.get(otherAppSessionId).getRoute()).isNull();
+        assertThat(idToRecordMap.get(ownSessionId).hasRoute()).isTrue();
+        assertThat(idToRecordMap.get(ownSessionId).getRoute()).isEqualTo(ownSession.getRoute());
+    }
+
+    @Test
     public void testRouteInsert_cannotInsertRouteWithoutPerm() throws Exception {
         mAutomation.revokeRuntimePermission(
                 APP_A_WITH_READ_WRITE_PERMS.getPackageName(), WRITE_EXERCISE_ROUTE);
@@ -189,4 +490,21 @@
         assertThat(records).hasSize(1);
         assertThat(records.get(0).hasRoute()).isFalse();
     }
+
+    private static String getInsertedSessionId(Bundle bundle) {
+        List<String> ids =
+                ((List<RecordTypeAndRecordIds>) bundle.getSerializable(RECORD_IDS))
+                        .stream()
+                                .filter(
+                                        it ->
+                                                it.getRecordType()
+                                                        .equals(
+                                                                ExerciseSessionRecord.class
+                                                                        .getName()))
+                                .map(RecordTypeAndRecordIds::getRecordIds)
+                                .flatMap(Collection::stream)
+                                .toList();
+        assertThat(ids).hasSize(1);
+        return ids.get(0);
+    }
 }
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 47517f5..691b44a 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
@@ -16,30 +16,39 @@
 
 package android.healthconnect.cts.device;
 
-import static android.healthconnect.cts.lib.TestUtils.CHANGE_LOGS_RESPONSE;
-import static android.healthconnect.cts.lib.TestUtils.CHANGE_LOG_TOKEN;
-import static android.healthconnect.cts.lib.TestUtils.READ_RECORDS_SIZE;
-import static android.healthconnect.cts.lib.TestUtils.RECORD_IDS;
-import static android.healthconnect.cts.lib.TestUtils.SUCCESS;
-import static android.healthconnect.cts.lib.TestUtils.deleteAllStagedRemoteData;
-import static android.healthconnect.cts.lib.TestUtils.deleteRecordsAs;
-import static android.healthconnect.cts.lib.TestUtils.deleteTestData;
-import static android.healthconnect.cts.lib.TestUtils.fetchDataOriginsPriorityOrder;
-import static android.healthconnect.cts.lib.TestUtils.getChangeLogTokenAs;
-import static android.healthconnect.cts.lib.TestUtils.getGrantedHealthPermissions;
-import static android.healthconnect.cts.lib.TestUtils.grantPermission;
-import static android.healthconnect.cts.lib.TestUtils.insertRecordAs;
-import static android.healthconnect.cts.lib.TestUtils.insertRecordWithAnotherAppPackageName;
-import static android.healthconnect.cts.lib.TestUtils.insertRecordWithGivenClientId;
-import static android.healthconnect.cts.lib.TestUtils.readChangeLogsUsingDataOriginFiltersAs;
-import static android.healthconnect.cts.lib.TestUtils.readRecords;
-import static android.healthconnect.cts.lib.TestUtils.readRecordsAs;
-import static android.healthconnect.cts.lib.TestUtils.readRecordsUsingDataOriginFiltersAs;
-import static android.healthconnect.cts.lib.TestUtils.revokeHealthPermissions;
-import static android.healthconnect.cts.lib.TestUtils.revokePermission;
-import static android.healthconnect.cts.lib.TestUtils.updateDataOriginPriorityOrder;
-import static android.healthconnect.cts.lib.TestUtils.updateRecordsAs;
-import static android.healthconnect.cts.lib.TestUtils.verifyDeleteRecords;
+import static android.health.connect.datatypes.ExerciseSessionRecord.EXERCISE_DURATION_TOTAL;
+import static android.health.connect.datatypes.StepsRecord.STEPS_COUNT_TOTAL;
+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.READ_RECORDS_SIZE;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.RECORD_IDS;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.SUCCESS;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.deleteRecordsAs;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.getChangeLogTokenAs;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.getDataOriginPriorityOrder;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.insertExerciseSessionAs;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.insertRecordAs;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.insertRecordWithAnotherAppPackageName;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.insertRecordWithGivenClientId;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.insertStepsRecordAs;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.readChangeLogsUsingDataOriginFiltersAs;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.readRecordsAs;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.readRecordsUsingDataOriginFiltersAs;
+import static android.healthconnect.cts.lib.MultiAppTestUtils.updateRecordsAs;
+import static android.healthconnect.cts.utils.TestUtils.deleteAllStagedRemoteData;
+import static android.healthconnect.cts.utils.TestUtils.deleteTestData;
+import static android.healthconnect.cts.utils.TestUtils.fetchDataOriginsPriorityOrder;
+import static android.healthconnect.cts.utils.TestUtils.getAggregateResponse;
+import static android.healthconnect.cts.utils.TestUtils.getApplicationInfo;
+import static android.healthconnect.cts.utils.TestUtils.getGrantedHealthPermissions;
+import static android.healthconnect.cts.utils.TestUtils.getInstantTime;
+import static android.healthconnect.cts.utils.TestUtils.grantPermission;
+import static android.healthconnect.cts.utils.TestUtils.readRecords;
+import static android.healthconnect.cts.utils.TestUtils.revokeAndThenGrantHealthPermissions;
+import static android.healthconnect.cts.utils.TestUtils.revokeHealthPermissions;
+import static android.healthconnect.cts.utils.TestUtils.revokePermission;
+import static android.healthconnect.cts.utils.TestUtils.updateDataOriginPriorityOrder;
+import static android.healthconnect.cts.utils.TestUtils.verifyDeleteRecords;
 
 import static com.android.compatibility.common.util.FeatureUtil.AUTOMOTIVE_FEATURE;
 import static com.android.compatibility.common.util.FeatureUtil.hasSystemFeature;
@@ -47,17 +56,22 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import android.app.UiAutomation;
+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.RecordIdFilter;
+import android.health.connect.TimeInstantRangeFilter;
 import android.health.connect.UpdateDataOriginPriorityOrderRequest;
 import android.health.connect.changelog.ChangeLogsResponse;
 import android.health.connect.datatypes.DataOrigin;
+import android.health.connect.datatypes.HeartRateRecord;
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
-import android.healthconnect.cts.lib.TestUtils;
+import android.health.connect.datatypes.StepsRecord;
+import android.healthconnect.cts.utils.TestUtils;
 import android.os.Bundle;
 
 import androidx.test.core.app.ApplicationProvider;
@@ -84,6 +98,8 @@
     static final String TAG = "HealthConnectDeviceTest";
     public static final String MANAGE_HEALTH_DATA = HealthPermissions.MANAGE_HEALTH_DATA_PERMISSION;
     static final long VERSION_CODE = 1;
+    private static final int ASYNC_RETRIES = 3;
+    private static final int ASYNC_RETRY_DELAY_MILLIS = 500;
 
     static final TestApp APP_A_WITH_READ_WRITE_PERMS =
             new TestApp(
@@ -303,12 +319,16 @@
     public void testAppCanReadChangeLogsUsingDataOriginFilters() throws Exception {
         Bundle bundle =
                 getChangeLogTokenAs(
-                        APP_B_WITH_READ_WRITE_PERMS, APP_A_WITH_READ_WRITE_PERMS.getPackageName());
+                        APP_B_WITH_READ_WRITE_PERMS,
+                        APP_A_WITH_READ_WRITE_PERMS.getPackageName(),
+                        null);
         String changeLogTokenForAppB = bundle.getString(CHANGE_LOG_TOKEN);
 
         bundle =
                 getChangeLogTokenAs(
-                        APP_A_WITH_READ_WRITE_PERMS, APP_B_WITH_READ_WRITE_PERMS.getPackageName());
+                        APP_A_WITH_READ_WRITE_PERMS,
+                        APP_B_WITH_READ_WRITE_PERMS.getPackageName(),
+                        null);
         String changeLogTokenForAppA = bundle.getString(CHANGE_LOG_TOKEN);
 
         bundle = insertRecordAs(APP_A_WITH_READ_WRITE_PERMS);
@@ -345,7 +365,7 @@
 
         ChangeLogsResponse response = bundle.getParcelable(CHANGE_LOGS_RESPONSE);
 
-        assertThat(response.getUpsertedRecords().size()).isEqualTo(noOfRecordsInsertedByAppA);
+        assertThat(response.getUpsertedRecords()).hasSize(noOfRecordsInsertedByAppA);
         assertThat(
                         response.getUpsertedRecords().stream()
                                 .map(Record::getMetadata)
@@ -353,7 +373,7 @@
                                 .toList())
                 .containsExactlyElementsIn(listOfRecordIdsInsertedByAppA);
 
-        assertThat(response.getDeletedLogs().size()).isEqualTo(0);
+        assertThat(response.getDeletedLogs()).isEmpty();
 
         bundle =
                 readChangeLogsUsingDataOriginFiltersAs(
@@ -361,8 +381,8 @@
 
         response = bundle.getParcelable(CHANGE_LOGS_RESPONSE);
 
-        assertThat(response.getUpsertedRecords().size()).isEqualTo(0);
-        assertThat(response.getDeletedLogs().size()).isEqualTo(noOfRecordsInsertedByAppB);
+        assertThat(response.getUpsertedRecords()).isEmpty();
+        assertThat(response.getDeletedLogs()).hasSize(noOfRecordsInsertedByAppB);
     }
 
     @Test
@@ -394,8 +414,8 @@
                         .map(dataOrigin -> dataOrigin.getPackageName())
                         .collect(Collectors.toList());
 
-        assertThat(newPriorityList.size()).isEqualTo(oldPriorityList.size() + 1);
-        assertThat(newPriorityList.contains(APP_A_WITH_READ_WRITE_PERMS.getPackageName())).isTrue();
+        assertThat(newPriorityList).hasSize(oldPriorityList.size() + 1);
+        assertThat(newPriorityList).contains(APP_A_WITH_READ_WRITE_PERMS.getPackageName());
     }
 
     @Test
@@ -420,7 +440,7 @@
                         .map(dataOrigin -> dataOrigin.getPackageName())
                         .collect(Collectors.toList());
 
-        assertThat(oldPriorityList.contains(APP_A_WITH_READ_WRITE_PERMS.getPackageName())).isTrue();
+        assertThat(oldPriorityList).contains(APP_A_WITH_READ_WRITE_PERMS.getPackageName());
 
         revokePermission(APP_A_WITH_READ_WRITE_PERMS.getPackageName(), healthPerms.get(0));
 
@@ -432,7 +452,7 @@
                         .map(dataOrigin -> dataOrigin.getPackageName())
                         .collect(Collectors.toList());
 
-        assertThat(newPriorityList.contains(APP_A_WITH_READ_WRITE_PERMS.getPackageName())).isTrue();
+        assertThat(newPriorityList).contains(APP_A_WITH_READ_WRITE_PERMS.getPackageName());
 
         grantPermission(APP_A_WITH_READ_WRITE_PERMS.getPackageName(), healthPerms.get(0));
     }
@@ -459,7 +479,7 @@
                         .map(dataOrigin -> dataOrigin.getPackageName())
                         .collect(Collectors.toList());
 
-        assertThat(oldPriorityList.contains(APP_A_WITH_READ_WRITE_PERMS.getPackageName())).isTrue();
+        assertThat(oldPriorityList).contains(APP_A_WITH_READ_WRITE_PERMS.getPackageName());
 
         for (String perm : healthPerms) {
             revokePermission(APP_A_WITH_READ_WRITE_PERMS.getPackageName(), perm);
@@ -595,4 +615,260 @@
                             + " app!");
         }
     }
+
+    @Test
+    public void testToVerifyGetContributorApplicationsInfo() throws Exception {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+
+        Bundle bundle = insertRecordAs(APP_A_WITH_READ_WRITE_PERMS);
+        assertThat(bundle.getBoolean(SUCCESS)).isTrue();
+
+        bundle = insertRecordAs(APP_B_WITH_READ_WRITE_PERMS);
+        assertThat(bundle.getBoolean(SUCCESS)).isTrue();
+
+        List<String> pkgNameList =
+                List.of(
+                        APP_A_WITH_READ_WRITE_PERMS.getPackageName(),
+                        APP_B_WITH_READ_WRITE_PERMS.getPackageName());
+
+        uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA);
+
+        // Contributor information is updated asynchronously, so retry with delay until the update
+        // finishes (or we run out of retries).
+        for (int i = 1; i <= ASYNC_RETRIES; i++) {
+            List<String> appInfoList =
+                    getApplicationInfo().stream()
+                            .map(appInfo -> appInfo.getPackageName())
+                            .collect(Collectors.toList());
+
+            try {
+                assertThat(appInfoList).containsAtLeastElementsIn(pkgNameList);
+            } catch (AssertionError e) {
+                if (i < ASYNC_RETRIES) {
+                    Thread.sleep(ASYNC_RETRY_DELAY_MILLIS);
+                    continue;
+                }
+
+                throw new AssertionError(e);
+            }
+        }
+    }
+
+    @Test
+    public void testAggregationOutputForTotalStepsCountWithDataFromTwoAppsHavingDifferentPriority()
+            throws Exception {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+
+        revokeAndThenGrantHealthPermissions(APP_A_WITH_READ_WRITE_PERMS);
+        revokeAndThenGrantHealthPermissions(APP_B_WITH_READ_WRITE_PERMS);
+
+        List<DataOrigin> dataOriginPrioOrder =
+                getDataOriginPriorityOrder(
+                        APP_A_WITH_READ_WRITE_PERMS, APP_B_WITH_READ_WRITE_PERMS);
+
+        uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA);
+        List<String> priorityList =
+                fetchDataOriginsPriorityOrder(HealthDataCategory.ACTIVITY)
+                        .getDataOriginsPriorityOrder()
+                        .stream()
+                        .map(dataOrigin -> dataOrigin.getPackageName())
+                        .collect(Collectors.toList());
+
+        assertThat(
+                        priorityList.equals(
+                                dataOriginPrioOrder.stream()
+                                        .map(dataOrigin -> dataOrigin.getPackageName())
+                                        .collect(Collectors.toList())))
+                .isTrue();
+
+        Bundle bundle =
+                insertStepsRecordAs(APP_A_WITH_READ_WRITE_PERMS, "01:00 PM", "03:00 PM", 1000);
+        assertThat(bundle.getBoolean(SUCCESS)).isTrue();
+        bundle = insertStepsRecordAs(APP_B_WITH_READ_WRITE_PERMS, "02:00 PM", "04:00 PM", 2000);
+        assertThat(bundle.getBoolean(SUCCESS)).isTrue();
+
+        AggregateRecordsRequest<Long> aggregateRecordsRequest =
+                new AggregateRecordsRequest.Builder<Long>(
+                                new TimeInstantRangeFilter.Builder()
+                                        .setStartTime(getInstantTime("01:00 PM"))
+                                        .setEndTime(getInstantTime("04:00 PM"))
+                                        .build())
+                        .addAggregationType(STEPS_COUNT_TOTAL)
+                        .build();
+        assertThat(aggregateRecordsRequest.getAggregationTypes()).isNotNull();
+        assertThat(aggregateRecordsRequest.getTimeRangeFilter()).isNotNull();
+        assertThat(aggregateRecordsRequest.getDataOriginsFilters()).isNotNull();
+
+        AggregateRecordsResponse<Long> oldResponse = getAggregateResponse(aggregateRecordsRequest);
+        assertThat(oldResponse.get(STEPS_COUNT_TOTAL)).isNotNull();
+        assertThat(oldResponse.get(STEPS_COUNT_TOTAL)).isEqualTo(2000);
+
+        dataOriginPrioOrder =
+                getDataOriginPriorityOrder(
+                        APP_B_WITH_READ_WRITE_PERMS, APP_A_WITH_READ_WRITE_PERMS);
+
+        uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA);
+        updateDataOriginPriorityOrder(
+                new UpdateDataOriginPriorityOrderRequest(
+                        dataOriginPrioOrder, HealthDataCategory.ACTIVITY));
+
+        uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA);
+        priorityList =
+                fetchDataOriginsPriorityOrder(HealthDataCategory.ACTIVITY)
+                        .getDataOriginsPriorityOrder()
+                        .stream()
+                        .map(dataOrigin -> dataOrigin.getPackageName())
+                        .collect(Collectors.toList());
+
+        assertThat(
+                        priorityList.equals(
+                                dataOriginPrioOrder.stream()
+                                        .map(dataOrigin -> dataOrigin.getPackageName())
+                                        .collect(Collectors.toList())))
+                .isTrue();
+
+        AggregateRecordsResponse<Long> newResponse = getAggregateResponse(aggregateRecordsRequest);
+        assertThat(newResponse.get(STEPS_COUNT_TOTAL)).isNotNull();
+        assertThat(newResponse.get(STEPS_COUNT_TOTAL)).isEqualTo(2500);
+    }
+
+    @Test
+    public void testAggregationOutputForExerciseSessionWithDataFromTwoAppsHavingDifferentPriority()
+            throws Exception {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+
+        revokeAndThenGrantHealthPermissions(APP_A_WITH_READ_WRITE_PERMS);
+        revokeAndThenGrantHealthPermissions(APP_B_WITH_READ_WRITE_PERMS);
+
+        List<DataOrigin> dataOriginPrioOrder =
+                getDataOriginPriorityOrder(
+                        APP_A_WITH_READ_WRITE_PERMS, APP_B_WITH_READ_WRITE_PERMS);
+
+        uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA);
+        List<String> priorityList =
+                fetchDataOriginsPriorityOrder(HealthDataCategory.ACTIVITY)
+                        .getDataOriginsPriorityOrder()
+                        .stream()
+                        .map(dataOrigin -> dataOrigin.getPackageName())
+                        .collect(Collectors.toList());
+
+        assertThat(
+                        priorityList.equals(
+                                dataOriginPrioOrder.stream()
+                                        .map(dataOrigin -> dataOrigin.getPackageName())
+                                        .collect(Collectors.toList())))
+                .isTrue();
+
+        Bundle bundle =
+                insertExerciseSessionAs(
+                        APP_A_WITH_READ_WRITE_PERMS,
+                        "01:00 PM",
+                        "03:00 PM",
+                        "02:00 PM",
+                        "03:00 PM");
+        assertThat(bundle.getBoolean(SUCCESS)).isTrue();
+        bundle =
+                insertExerciseSessionAs(
+                        APP_B_WITH_READ_WRITE_PERMS, "02:00 PM", "03:00 PM", null, null);
+        assertThat(bundle.getBoolean(SUCCESS)).isTrue();
+
+        AggregateRecordsRequest<Long> aggregateRecordsRequest =
+                new AggregateRecordsRequest.Builder<Long>(
+                                new TimeInstantRangeFilter.Builder()
+                                        .setStartTime(getInstantTime("01:00 PM"))
+                                        .setEndTime(getInstantTime("03:00 PM"))
+                                        .build())
+                        .addAggregationType(EXERCISE_DURATION_TOTAL)
+                        .build();
+        assertThat(aggregateRecordsRequest.getAggregationTypes()).isNotNull();
+        assertThat(aggregateRecordsRequest.getTimeRangeFilter()).isNotNull();
+        assertThat(aggregateRecordsRequest.getDataOriginsFilters()).isNotNull();
+
+        AggregateRecordsResponse<Long> response = getAggregateResponse(aggregateRecordsRequest);
+        assertThat(response.get(EXERCISE_DURATION_TOTAL)).isNotNull();
+        assertThat(response.get(EXERCISE_DURATION_TOTAL))
+                .isEqualTo(
+                        (getInstantTime("03:00 PM").toEpochMilli()
+                                        - getInstantTime("01:00 PM").toEpochMilli())
+                                - (getInstantTime("03:00 PM").toEpochMilli()
+                                        - getInstantTime("02:00 PM").toEpochMilli()));
+
+        dataOriginPrioOrder =
+                getDataOriginPriorityOrder(
+                        APP_B_WITH_READ_WRITE_PERMS, APP_A_WITH_READ_WRITE_PERMS);
+
+        uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA);
+        updateDataOriginPriorityOrder(
+                new UpdateDataOriginPriorityOrderRequest(
+                        dataOriginPrioOrder, HealthDataCategory.ACTIVITY));
+
+        uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA);
+        priorityList =
+                fetchDataOriginsPriorityOrder(HealthDataCategory.ACTIVITY)
+                        .getDataOriginsPriorityOrder()
+                        .stream()
+                        .map(dataOrigin -> dataOrigin.getPackageName())
+                        .collect(Collectors.toList());
+
+        assertThat(
+                        priorityList.equals(
+                                dataOriginPrioOrder.stream()
+                                        .map(dataOrigin -> dataOrigin.getPackageName())
+                                        .collect(Collectors.toList())))
+                .isTrue();
+
+        AggregateRecordsResponse<Long> newResponse = getAggregateResponse(aggregateRecordsRequest);
+        assertThat(newResponse.get(EXERCISE_DURATION_TOTAL)).isNotNull();
+        assertThat(newResponse.get(EXERCISE_DURATION_TOTAL))
+                .isEqualTo(
+                        getInstantTime("03:00 PM").toEpochMilli()
+                                - getInstantTime("01:00 PM").toEpochMilli());
+    }
+
+    @Test
+    public void testToVerifyNoPermissionChangeLog() throws Exception {
+        ArrayList<String> recordClassesToRead = new ArrayList();
+        recordClassesToRead.add(HeartRateRecord.class.getName());
+        recordClassesToRead.add(StepsRecord.class.getName());
+
+        Bundle bundle =
+                getChangeLogTokenAs(
+                        APP_B_WITH_READ_WRITE_PERMS,
+                        APP_A_WITH_READ_WRITE_PERMS.getPackageName(),
+                        recordClassesToRead);
+        String changeLogTokenForAppB = bundle.getString(CHANGE_LOG_TOKEN);
+
+        bundle = insertRecordAs(APP_A_WITH_READ_WRITE_PERMS);
+        assertThat(bundle.getBoolean(SUCCESS)).isTrue();
+
+        List<String> healthPerms =
+                getGrantedHealthPermissions(APP_B_WITH_READ_WRITE_PERMS.getPackageName());
+
+        revokeHealthPermissions(APP_B_WITH_READ_WRITE_PERMS.getPackageName());
+
+        try {
+            readChangeLogsUsingDataOriginFiltersAs(
+                    APP_B_WITH_READ_WRITE_PERMS, changeLogTokenForAppB);
+            Assert.fail(
+                    "Should have thrown exception in reading changeLogs without read permissions!");
+        } catch (HealthConnectException e) {
+            assertThat(e.getErrorCode()).isEqualTo(HealthConnectException.ERROR_SECURITY);
+        }
+
+        try {
+            getChangeLogTokenAs(
+                    APP_B_WITH_READ_WRITE_PERMS,
+                    APP_A_WITH_READ_WRITE_PERMS.getPackageName(),
+                    recordClassesToRead);
+            Assert.fail(
+                    "Should have thrown exception in getting change log token without read "
+                            + "permission!");
+        } catch (HealthConnectException e) {
+            assertThat(e.getErrorCode()).isEqualTo(HealthConnectException.ERROR_SECURITY);
+        }
+
+        for (String perm : healthPerms) {
+            grantPermission(APP_B_WITH_READ_WRITE_PERMS.getPackageName(), perm);
+        }
+    }
 }
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 d73a13a..b27af1e 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
@@ -38,14 +38,22 @@
 
 import com.google.protobuf.ExtensionRegistry;
 
+import java.time.Duration;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Date;
 import java.util.List;
 
 public class HealthConnectDailyLogsStatsTests extends DeviceTestCase implements IBuildReceiver {
 
     public static final String TEST_APP_PKG_NAME = "android.healthconnect.cts.testhelper";
     private static final int NUMBER_OF_RETRIES = 10;
-
+    private static final String DAILY_LOG_TESTS_ACTIVITY = ".DailyLogsTests";
+    private static final String HEALTH_CONNECT_SERVICE_LOG_TESTS_ACTIVITY =
+            ".HealthConnectServiceLogsTests";
     private IBuildInfo mCtsBuild;
+    private Instant mTestStartTime;
+    private Instant mTestStartTimeOnDevice;
 
     @Override
     protected void setUp() throws Exception {
@@ -55,14 +63,21 @@
         super.setUp();
         assertThat(mCtsBuild).isNotNull();
         assertThat(isHardwareSupported(getDevice())).isTrue();
+        mTestStartTime = Instant.now();
+        mTestStartTimeOnDevice = Instant.ofEpochMilli(getDevice().getDeviceDate());
         ConfigUtils.removeConfig(getDevice());
         ReportUtils.clearReports(getDevice());
+        clearData();
+        // Doing this to avoid any access log entries which might make the test flaky.
+        increaseDeviceTimeByDays(/* numberOfDays= */ 31);
     }
 
     @Override
     protected void tearDown() throws Exception {
         ConfigUtils.removeConfig(getDevice());
         ReportUtils.clearReports(getDevice());
+        clearData();
+        resetTime();
         super.tearDown();
     }
 
@@ -80,11 +95,12 @@
                 TEST_APP_PKG_NAME,
                 new int[] {ApiExtensionAtoms.HEALTH_CONNECT_USAGE_STATS_FIELD_NUMBER});
 
-        List<StatsLog.EventMetricData> data = getEventMetricDataList(null, NUMBER_OF_RETRIES);
-
+        List<StatsLog.EventMetricData> data =
+                getEventMetricDataList(/* testName= */ null, NUMBER_OF_RETRIES);
         assertThat(data.size()).isAtLeast(1);
         HealthConnectUsageStats atom =
                 data.get(0).getAtom().getExtension(ApiExtensionAtoms.healthConnectUsageStats);
+
         assertThat(atom.getConnectedAppsCount()).isGreaterThan(0);
         assertThat(atom.getAvailableAppsCount()).isGreaterThan(0);
     }
@@ -93,27 +109,159 @@
         if (!isHardwareSupported(getDevice())) {
             return;
         }
-        // To clear the data before the test starts (as a safety measure)
-        DeviceUtils.runDeviceTests(
-                getDevice(), TEST_APP_PKG_NAME, ".DailyLogsTests", "deleteAllRecordsAddedForTest");
         ConfigUtils.uploadConfigForPushedAtoms(
                 getDevice(),
                 TEST_APP_PKG_NAME,
                 new int[] {ApiExtensionAtoms.HEALTH_CONNECT_STORAGE_STATS_FIELD_NUMBER});
+
         List<StatsLog.EventMetricData> data =
-                getEventMetricDataList("testHealthConnectDatabaseStats", NUMBER_OF_RETRIES);
+                getEventMetricDataList(
+                        /* testName= */ "testHealthConnectDatabaseStats", NUMBER_OF_RETRIES);
         assertThat(data.size()).isAtLeast(1);
         HealthConnectStorageStats atom =
                 data.get(0).getAtom().getExtension(ApiExtensionAtoms.healthConnectStorageStats);
+
         assertThat(atom.getDatabaseSize()).isGreaterThan(0);
         assertThat(atom.getInstantDataCount()).isEqualTo(1);
         assertThat(atom.getIntervalDataCount()).isEqualTo(1);
         assertThat(atom.getSeriesDataCount()).isEqualTo(1);
         assertThat(atom.getChangelogCount()).isGreaterThan(2);
+    }
 
-        // To clear the data once the database stats have been verified
-        DeviceUtils.runDeviceTests(
-                getDevice(), TEST_APP_PKG_NAME, ".DailyLogsTests", "deleteAllRecordsAddedForTest");
+    public void testIsUserActive_insertRecordPreviousDay_userMonthlyActive() throws Exception {
+        if (!isHardwareSupported(getDevice())) {
+            return;
+        }
+
+        ConfigUtils.uploadConfigForPushedAtoms(
+                getDevice(),
+                TEST_APP_PKG_NAME,
+                new int[] {ApiExtensionAtoms.HEALTH_CONNECT_USAGE_STATS_FIELD_NUMBER});
+        triggerTestInTestApp(
+                HEALTH_CONNECT_SERVICE_LOG_TESTS_ACTIVITY, "testHealthConnectInsertRecords");
+        increaseDeviceTimeByDays(/* numberOfDays= */ 1);
+
+        List<StatsLog.EventMetricData> data =
+                getEventMetricDataList(/* testName= */ null, NUMBER_OF_RETRIES);
+        assertThat(data.size()).isAtLeast(1);
+        HealthConnectUsageStats atom =
+                data.get(0).getAtom().getExtension(ApiExtensionAtoms.healthConnectUsageStats);
+
+        assertThat(atom.getIsMonthlyActiveUser()).isTrue();
+    }
+
+    public void testIsUserActive_insertRecordBetween7To30days_userMonthlyActive() throws Exception {
+        if (!isHardwareSupported(getDevice())) {
+            return;
+        }
+
+        ConfigUtils.uploadConfigForPushedAtoms(
+                getDevice(),
+                TEST_APP_PKG_NAME,
+                new int[] {ApiExtensionAtoms.HEALTH_CONNECT_USAGE_STATS_FIELD_NUMBER});
+        triggerTestInTestApp(
+                HEALTH_CONNECT_SERVICE_LOG_TESTS_ACTIVITY, "testHealthConnectInsertRecords");
+        increaseDeviceTimeByDays(/* numberOfDays= */ 10);
+
+        List<StatsLog.EventMetricData> data =
+                getEventMetricDataList(/* testName= */ null, NUMBER_OF_RETRIES);
+        assertThat(data.size()).isAtLeast(1);
+        HealthConnectUsageStats atom =
+                data.get(0).getAtom().getExtension(ApiExtensionAtoms.healthConnectUsageStats);
+
+        assertThat(atom.getIsMonthlyActiveUser()).isTrue();
+    }
+
+    public void testIsUserActive_insertRecordBefore30Days_userNotMonthlyActive() throws Exception {
+        if (!isHardwareSupported(getDevice())) {
+            return;
+        }
+
+        ConfigUtils.uploadConfigForPushedAtoms(
+                getDevice(),
+                TEST_APP_PKG_NAME,
+                new int[] {ApiExtensionAtoms.HEALTH_CONNECT_USAGE_STATS_FIELD_NUMBER});
+        triggerTestInTestApp(
+                HEALTH_CONNECT_SERVICE_LOG_TESTS_ACTIVITY, "testHealthConnectInsertRecords");
+        increaseDeviceTimeByDays(/* numberOfDays= */ 35);
+        // To delete access logs older than 7 days.
+        triggerDailyJob();
+
+        List<StatsLog.EventMetricData> data =
+                getEventMetricDataList(/* testName= */ null, NUMBER_OF_RETRIES);
+        assertThat(data.size()).isAtLeast(1);
+        HealthConnectUsageStats atom =
+                data.get(0).getAtom().getExtension(ApiExtensionAtoms.healthConnectUsageStats);
+
+        assertThat(atom.getIsMonthlyActiveUser()).isFalse();
+    }
+
+    public void testIsUserActive_readRecordPreviousDay_userMonthlyActive() throws Exception {
+        if (!isHardwareSupported(getDevice())) {
+            return;
+        }
+
+        ConfigUtils.uploadConfigForPushedAtoms(
+                getDevice(),
+                TEST_APP_PKG_NAME,
+                new int[] {ApiExtensionAtoms.HEALTH_CONNECT_USAGE_STATS_FIELD_NUMBER});
+        triggerTestInTestApp(
+                HEALTH_CONNECT_SERVICE_LOG_TESTS_ACTIVITY, "testHealthConnectReadRecords");
+        increaseDeviceTimeByDays(/* numberOfDays= */ 1);
+
+        List<StatsLog.EventMetricData> data =
+                getEventMetricDataList(/* testName= */ null, NUMBER_OF_RETRIES);
+        assertThat(data.size()).isAtLeast(1);
+        HealthConnectUsageStats atom =
+                data.get(0).getAtom().getExtension(ApiExtensionAtoms.healthConnectUsageStats);
+
+        assertThat(atom.getIsMonthlyActiveUser()).isTrue();
+    }
+
+    public void testIsUserActive_readRecordBetween7To30days_userMonthlyActive() throws Exception {
+        if (!isHardwareSupported(getDevice())) {
+            return;
+        }
+
+        ConfigUtils.uploadConfigForPushedAtoms(
+                getDevice(),
+                TEST_APP_PKG_NAME,
+                new int[] {ApiExtensionAtoms.HEALTH_CONNECT_USAGE_STATS_FIELD_NUMBER});
+        triggerTestInTestApp(
+                HEALTH_CONNECT_SERVICE_LOG_TESTS_ACTIVITY, "testHealthConnectReadRecords");
+        increaseDeviceTimeByDays(/* numberOfDays= */ 10);
+        // To delete access logs older than 7 days.
+        triggerDailyJob();
+
+        List<StatsLog.EventMetricData> data =
+                getEventMetricDataList(/* testName= */ null, NUMBER_OF_RETRIES);
+        assertThat(data.size()).isAtLeast(1);
+        HealthConnectUsageStats atom =
+                data.get(0).getAtom().getExtension(ApiExtensionAtoms.healthConnectUsageStats);
+
+        assertThat(atom.getIsMonthlyActiveUser()).isTrue();
+    }
+
+    public void testIsUserActive_readRecordBefore30Days_userNotMonthlyActive() throws Exception {
+        if (!isHardwareSupported(getDevice())) {
+            return;
+        }
+
+        ConfigUtils.uploadConfigForPushedAtoms(
+                getDevice(),
+                TEST_APP_PKG_NAME,
+                new int[] {ApiExtensionAtoms.HEALTH_CONNECT_USAGE_STATS_FIELD_NUMBER});
+        triggerTestInTestApp(
+                HEALTH_CONNECT_SERVICE_LOG_TESTS_ACTIVITY, "testHealthConnectReadRecords");
+        increaseDeviceTimeByDays(/* numberOfDays= */ 35);
+
+        List<StatsLog.EventMetricData> data =
+                getEventMetricDataList(/* testName= */ null, NUMBER_OF_RETRIES);
+        assertThat(data.size()).isAtLeast(1);
+        HealthConnectUsageStats atom =
+                data.get(0).getAtom().getExtension(ApiExtensionAtoms.healthConnectUsageStats);
+
+        assertThat(atom.getIsMonthlyActiveUser()).isFalse();
     }
 
     private List<StatsLog.EventMetricData> getEventMetricDataList(String testName, int retryCount)
@@ -122,7 +270,11 @@
             throw new RuntimeException("Could not collect metrics.");
         }
 
-        List<StatsLog.EventMetricData> data = triggerDailyJob(triggerTestInTestApp(testName));
+        ExtensionRegistry extensionRegistry =
+                triggerTestInTestApp(DAILY_LOG_TESTS_ACTIVITY, testName);
+        triggerDailyJob(); // This will run the job which calls DailyLogger to log some metrics.
+        List<StatsLog.EventMetricData> data =
+                ReportUtils.getEventMetricDataList(getDevice(), extensionRegistry);
 
         if (data.size() == 0) {
             return getEventMetricDataList(testName, retryCount - 1);
@@ -130,10 +282,19 @@
         return data;
     }
 
-    private ExtensionRegistry triggerTestInTestApp(String testName) throws Exception {
+    private void clearData() throws Exception {
+        triggerTestInTestApp(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(10);
+        triggerDailyJob();
+    }
+
+    private ExtensionRegistry triggerTestInTestApp(String className, String testName)
+            throws Exception {
 
         if (testName != null) {
-            DeviceUtils.runDeviceTests(getDevice(), TEST_APP_PKG_NAME, ".DailyLogsTests", testName);
+            DeviceUtils.runDeviceTests(getDevice(), TEST_APP_PKG_NAME, className, testName);
         }
 
         ExtensionRegistry registry = ExtensionRegistry.newInstance();
@@ -141,8 +302,7 @@
         return registry;
     }
 
-    private List<StatsLog.EventMetricData> triggerDailyJob(ExtensionRegistry registry)
-            throws Exception {
+    private void triggerDailyJob() 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'.
@@ -157,7 +317,6 @@
 
         executeLoggingJob(jobExecutionCommand, NUMBER_OF_RETRIES);
         RunUtil.getDefault().sleep(AtomTestUtils.WAIT_TIME_LONG);
-        return ReportUtils.getEventMetricDataList(getDevice(), registry);
     }
 
     private void executeLoggingJob(String jobExecutionCommand, int retry)
@@ -170,4 +329,28 @@
             executeLoggingJob(jobExecutionCommand, retry - 1);
         }
     }
+
+    private void increaseDeviceTimeByDays(int numberOfDays) throws DeviceNotAvailableException {
+        Instant deviceDate = Instant.ofEpochMilli(getDevice().getDeviceDate());
+
+        getDevice().setDate(Date.from(deviceDate.plus(numberOfDays, ChronoUnit.DAYS)));
+        getDevice()
+                .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");
+
+        getDevice().executeShellCommand("am broadcast -a android.intent.action.TIME_SET");
+    }
+
+    private void resetTime() throws DeviceNotAvailableException {
+        long timeDiff = Duration.between(mTestStartTime, Instant.now()).toMillis();
+
+        getDevice()
+                .executeShellCommand(
+                        "cmd time_detector set_time_state_for_tests --unix_epoch_time "
+                                + mTestStartTimeOnDevice.plusMillis(timeDiff).toEpochMilli()
+                                + " --user_should_confirm_time false --elapsed_realtime 0");
+        getDevice().executeShellCommand("am broadcast -a android.intent.action.TIME_SET");
+    }
 }
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 0035eab..59b51e6 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
@@ -25,6 +25,7 @@
 import android.cts.statsdatom.lib.ReportUtils;
 import android.healthfitness.api.ApiMethod;
 import android.healthfitness.api.ApiStatus;
+import android.healthfitness.api.ForegroundState;
 import android.healthfitness.api.RateLimit;
 
 import com.android.os.StatsLog;
@@ -198,6 +199,7 @@
         assertThat(atom.getDurationMillis()).isAtLeast(0);
         assertThat(atom.getNumberOfRecords()).isEqualTo(1);
         assertThat(atom.getRateLimit()).isEqualTo(RateLimit.NOT_USED);
+        assertThat(atom.getCallerForegroundState()).isEqualTo(ForegroundState.FOREGROUND);
     }
 
     public void testReadRecordsError() throws Exception {
@@ -215,6 +217,7 @@
         assertThat(atom.getErrorCode()).isNotEqualTo(0);
         assertThat(atom.getDurationMillis()).isAtLeast(0);
         assertThat(atom.getRateLimit()).isEqualTo(RateLimit.NOT_USED);
+        assertThat(atom.getCallerForegroundState()).isEqualTo(ForegroundState.FOREGROUND);
     }
 
     public void testChangeLogTokenRequest() throws Exception {
@@ -269,6 +272,7 @@
         assertThat(atom.getDurationMillis()).isAtLeast(0);
         assertThat(atom.getNumberOfRecords()).isEqualTo(1);
         assertThat(atom.getRateLimit()).isEqualTo(RateLimit.NOT_USED);
+        assertThat(atom.getCallerForegroundState()).isEqualTo(ForegroundState.FOREGROUND);
     }
 
     public void testChangeLogsRequestError() throws Exception {
@@ -286,6 +290,7 @@
         assertThat(atom.getErrorCode()).isNotEqualTo(0);
         assertThat(atom.getDurationMillis()).isAtLeast(0);
         assertThat(atom.getRateLimit()).isEqualTo(RateLimit.NOT_USED);
+        assertThat(atom.getCallerForegroundState()).isEqualTo(ForegroundState.FOREGROUND);
     }
 
     public void testAggregatedDataRequest() throws Exception {
@@ -304,6 +309,7 @@
         assertThat(atom.getDurationMillis()).isAtLeast(0);
         assertThat(atom.getNumberOfRecords()).isEqualTo(1);
         assertThat(atom.getRateLimit()).isEqualTo(RateLimit.NOT_USED);
+        assertThat(atom.getCallerForegroundState()).isEqualTo(ForegroundState.FOREGROUND);
     }
 
     public void testAggregatedDataRequestError() throws Exception {
@@ -322,6 +328,7 @@
         assertThat(atom.getDurationMillis()).isAtLeast(0);
         assertThat(atom.getNumberOfRecords()).isEqualTo(1);
         assertThat(atom.getRateLimit()).isEqualTo(RateLimit.NOT_USED);
+        assertThat(atom.getCallerForegroundState()).isEqualTo(ForegroundState.FOREGROUND);
     }
 
     private List<StatsLog.EventMetricData> uploadAtomConfigAndTriggerTest(String testName)
diff --git a/tests/cts/hostsidetests/healthconnect/libs/HealthConnectTestLib/Android.bp b/tests/cts/hostsidetests/healthconnect/libs/HealthConnectTestLib/Android.bp
index 4572cdd..110b37d 100644
--- a/tests/cts/hostsidetests/healthconnect/libs/HealthConnectTestLib/Android.bp
+++ b/tests/cts/hostsidetests/healthconnect/libs/HealthConnectTestLib/Android.bp
@@ -27,6 +27,7 @@
                  "androidx.test.rules",
                   "cts-install-lib",
                   "platform-test-annotations",
+                  "cts-healthconnect-utils",
     ],
     sdk_version: "test_current"
 }
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
new file mode 100644
index 0000000..51903f3
--- /dev/null
+++ b/tests/cts/hostsidetests/healthconnect/libs/HealthConnectTestLib/src/android/healthconnect/cts/lib/MultiAppTestUtils.java
@@ -0,0 +1,274 @@
+/*
+ * 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.lib;
+
+import static androidx.test.InstrumentationRegistry.getContext;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.health.connect.datatypes.DataOrigin;
+import android.healthconnect.cts.utils.TestUtils;
+import android.os.Bundle;
+
+import com.android.cts.install.lib.TestApp;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class MultiAppTestUtils {
+    static final String TAG = "HealthConnectTest";
+    public static final String QUERY_TYPE = "android.healthconnect.cts.queryType";
+    public static final String INTENT_EXTRA_CALLING_PKG = "android.healthconnect.cts.calling_pkg";
+    public static final String APP_PKG_NAME_USED_IN_DATA_ORIGIN =
+            "android.healthconnect.cts.pkg.usedInDataOrigin";
+    public static final String INSERT_RECORD_QUERY = "android.healthconnect.cts.insertRecord";
+    public static final String READ_RECORDS_QUERY = "android.healthconnect.cts.readRecords";
+    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 READ_RECORD_CLASS_NAME =
+            "android.healthconnect.cts.readRecordsClass";
+    public static final String READ_CHANGE_LOGS_QUERY = "android.healthconnect.cts.readChangeLogs";
+    public static final String CHANGE_LOGS_RESPONSE =
+            "android.healthconnect.cts.changeLogsResponse";
+    public static final String CHANGE_LOG_TOKEN = "android.healthconnect.cts.changeLogToken";
+    public static final String SUCCESS = "android.healthconnect.cts.success";
+    public static final String CLIENT_ID = "android.healthconnect.cts.clientId";
+    public static final String RECORD_IDS = "android.healthconnect.cts.records";
+    public static final String DELETE_RECORDS_QUERY = "android.healthconnect.cts.deleteRecords";
+    public static final String UPDATE_RECORDS_QUERY = "android.healthconnect.cts.updateRecords";
+    public static final String UPDATE_EXERCISE_ROUTE = "android.healthconnect.cts.updateRoute";
+
+    public static final String UPSERT_EXERCISE_ROUTE = "android.healthconnect.cts.upsertRoute";
+    public static final String GET_CHANGE_LOG_TOKEN_QUERY =
+            "android.healthconnect.cts.getChangeLogToken";
+    public static final String RECORD_TYPE = "android.healthconnect.cts.recordType";
+    public static final String STEPS_RECORD = "android.healthconnect.cts.stepsRecord";
+    public static final String EXERCISE_SESSION = "android.healthconnect.cts.exerciseSession";
+    public static final String START_TIME = "android.healthconnect.cts.startTime";
+    public static final String END_TIME = "android.healthconnect.cts.endTime";
+    public static final String STEPS_COUNT = "android.healthconnect.cts.stepsCount";
+    public static final String PAUSE_START = "android.healthconnect.cts.pauseStart";
+    public static final String PAUSE_END = "android.healthconnect.cts.pauseEnd";
+    public static final String INTENT_EXCEPTION = "android.healthconnect.cts.exception";
+    private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(20);
+
+    public static Bundle insertRecordAs(TestApp testApp) throws Exception {
+        Bundle bundle = new Bundle();
+        bundle.putString(QUERY_TYPE, INSERT_RECORD_QUERY);
+
+        return getFromTestApp(testApp, bundle);
+    }
+
+    public static Bundle deleteRecordsAs(
+            TestApp testApp, List<TestUtils.RecordTypeAndRecordIds> listOfRecordIdsAndClass)
+            throws Exception {
+        Bundle bundle = new Bundle();
+        bundle.putString(QUERY_TYPE, DELETE_RECORDS_QUERY);
+        bundle.putSerializable(RECORD_IDS, (Serializable) listOfRecordIdsAndClass);
+
+        return getFromTestApp(testApp, bundle);
+    }
+
+    public static Bundle updateRecordsAs(
+            TestApp testAppToUpdateData,
+            List<TestUtils.RecordTypeAndRecordIds> listOfRecordIdsAndClass)
+            throws Exception {
+        Bundle bundle = new Bundle();
+        bundle.putString(QUERY_TYPE, UPDATE_RECORDS_QUERY);
+        bundle.putSerializable(RECORD_IDS, (Serializable) listOfRecordIdsAndClass);
+
+        return getFromTestApp(testAppToUpdateData, bundle);
+    }
+
+    public static Bundle updateRouteAs(TestApp testAppToUpdateData) throws Exception {
+        Bundle bundle = new Bundle();
+        bundle.putString(QUERY_TYPE, UPDATE_EXERCISE_ROUTE);
+        return getFromTestApp(testAppToUpdateData, bundle);
+    }
+
+    public static Bundle insertSessionNoRouteAs(TestApp testAppToUpdateData) throws Exception {
+        Bundle bundle = new Bundle();
+        bundle.putString(QUERY_TYPE, UPSERT_EXERCISE_ROUTE);
+        return getFromTestApp(testAppToUpdateData, bundle);
+    }
+
+    public static Bundle insertRecordWithAnotherAppPackageName(
+            TestApp testAppToInsertData, TestApp testAppPkgNameUsed) throws Exception {
+        Bundle bundle = new Bundle();
+        bundle.putString(QUERY_TYPE, INSERT_RECORD_QUERY);
+        bundle.putString(APP_PKG_NAME_USED_IN_DATA_ORIGIN, testAppPkgNameUsed.getPackageName());
+
+        return getFromTestApp(testAppToInsertData, bundle);
+    }
+
+    public static Bundle readRecordsAs(TestApp testApp, ArrayList<String> recordClassesToRead)
+            throws Exception {
+        Bundle bundle = new Bundle();
+        bundle.putString(QUERY_TYPE, READ_RECORDS_QUERY);
+        bundle.putStringArrayList(READ_RECORD_CLASS_NAME, recordClassesToRead);
+
+        return getFromTestApp(testApp, bundle);
+    }
+
+    public static Bundle insertRecordWithGivenClientId(TestApp testApp, double clientId)
+            throws Exception {
+        Bundle bundle = new Bundle();
+        bundle.putString(QUERY_TYPE, INSERT_RECORD_QUERY);
+        bundle.putDouble(CLIENT_ID, clientId);
+
+        return getFromTestApp(testApp, bundle);
+    }
+
+    public static Bundle readRecordsUsingDataOriginFiltersAs(
+            TestApp testApp, ArrayList<String> recordClassesToRead) throws Exception {
+        Bundle bundle = new Bundle();
+        bundle.putString(QUERY_TYPE, READ_RECORDS_QUERY);
+        bundle.putStringArrayList(READ_RECORD_CLASS_NAME, recordClassesToRead);
+        bundle.putBoolean(READ_USING_DATA_ORIGIN_FILTERS, true);
+
+        return getFromTestApp(testApp, bundle);
+    }
+
+    public static Bundle readChangeLogsUsingDataOriginFiltersAs(
+            TestApp testApp, String changeLogToken) throws Exception {
+        Bundle bundle = new Bundle();
+        bundle.putString(QUERY_TYPE, READ_CHANGE_LOGS_QUERY);
+        bundle.putString(CHANGE_LOG_TOKEN, changeLogToken);
+        bundle.putBoolean(READ_USING_DATA_ORIGIN_FILTERS, true);
+
+        return getFromTestApp(testApp, bundle);
+    }
+
+    public static Bundle getChangeLogTokenAs(
+            TestApp testApp, String pkgName, ArrayList<String> recordClassesToRead)
+            throws Exception {
+        Bundle bundle = new Bundle();
+        bundle.putString(QUERY_TYPE, GET_CHANGE_LOG_TOKEN_QUERY);
+        bundle.putString(APP_PKG_NAME_USED_IN_DATA_ORIGIN, pkgName);
+
+        if (recordClassesToRead != null) {
+            bundle.putStringArrayList(READ_RECORD_CLASS_NAME, recordClassesToRead);
+        }
+        return getFromTestApp(testApp, bundle);
+    }
+
+    public static Bundle insertStepsRecordAs(
+            TestApp testApp, String startTime, String endTime, int stepsCount) throws Exception {
+        Bundle bundle = new Bundle();
+        bundle.putString(QUERY_TYPE, INSERT_RECORD_QUERY);
+        bundle.putString(RECORD_TYPE, STEPS_RECORD);
+        bundle.putString(START_TIME, startTime);
+        bundle.putString(END_TIME, endTime);
+        bundle.putInt(STEPS_COUNT, stepsCount);
+
+        return getFromTestApp(testApp, bundle);
+    }
+
+    public static Bundle insertExerciseSessionAs(
+            TestApp testApp,
+            String sessionStartTime,
+            String sessionEndTime,
+            String pauseStart,
+            String pauseEnd)
+            throws Exception {
+        Bundle bundle = new Bundle();
+        bundle.putString(QUERY_TYPE, INSERT_RECORD_QUERY);
+        bundle.putString(RECORD_TYPE, EXERCISE_SESSION);
+        bundle.putString(START_TIME, sessionStartTime);
+        bundle.putString(END_TIME, sessionEndTime);
+        bundle.putString(PAUSE_START, pauseStart);
+        bundle.putString(PAUSE_END, pauseEnd);
+
+        return getFromTestApp(testApp, bundle);
+    }
+
+    private static Bundle getFromTestApp(TestApp testApp, Bundle bundleToCreateIntent)
+            throws Exception {
+        final CountDownLatch latch = new CountDownLatch(1);
+        AtomicReference<Bundle> response = new AtomicReference<>();
+        AtomicReference<Exception> exceptionAtomicReference = new AtomicReference<>();
+        BroadcastReceiver broadcastReceiver =
+                new BroadcastReceiver() {
+                    @Override
+                    public void onReceive(Context context, Intent intent) {
+                        if (intent.hasExtra(INTENT_EXCEPTION)) {
+                            exceptionAtomicReference.set(
+                                    (Exception) (intent.getSerializableExtra(INTENT_EXCEPTION)));
+                        } else {
+                            response.set(intent.getExtras());
+                        }
+                        latch.countDown();
+                    }
+                };
+
+        launchTestApp(testApp, bundleToCreateIntent, broadcastReceiver, latch);
+        if (exceptionAtomicReference.get() != null) {
+            throw exceptionAtomicReference.get();
+        }
+        return response.get();
+    }
+
+    private static void launchTestApp(
+            TestApp testApp,
+            Bundle bundleToCreateIntent,
+            BroadcastReceiver broadcastReceiver,
+            CountDownLatch latch)
+            throws Exception {
+
+        // Register broadcast receiver
+        final IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(bundleToCreateIntent.getString(QUERY_TYPE));
+        intentFilter.addCategory(Intent.CATEGORY_DEFAULT);
+        getContext().registerReceiver(broadcastReceiver, intentFilter, Context.RECEIVER_EXPORTED);
+
+        // Launch the test app.
+        final Intent intent = new Intent(Intent.ACTION_MAIN);
+        intent.setPackage(testApp.getPackageName());
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        intent.putExtra(INTENT_EXTRA_CALLING_PKG, getContext().getPackageName());
+        intent.putExtras(bundleToCreateIntent);
+        intent.addCategory(Intent.CATEGORY_LAUNCHER);
+        intent.putExtras(bundleToCreateIntent);
+
+        Thread.sleep(500);
+        getContext().startActivity(intent);
+        if (!latch.await(POLLING_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
+            final String errorMessage =
+                    "Timed out while waiting to receive "
+                            + bundleToCreateIntent.getString(QUERY_TYPE)
+                            + " intent from "
+                            + testApp.getPackageName();
+            throw new TimeoutException(errorMessage);
+        }
+        getContext().unregisterReceiver(broadcastReceiver);
+    }
+
+    public static List<DataOrigin> getDataOriginPriorityOrder(TestApp testAppA, TestApp testAppB) {
+        return List.of(
+                new DataOrigin.Builder().setPackageName(testAppA.getPackageName()).build(),
+                new DataOrigin.Builder().setPackageName(testAppB.getPackageName()).build());
+    }
+}
diff --git a/tests/cts/hostsidetests/healthconnect/libs/HealthConnectTestLib/src/android/healthconnect/cts/lib/TestUtils.java b/tests/cts/hostsidetests/healthconnect/libs/HealthConnectTestLib/src/android/healthconnect/cts/lib/TestUtils.java
deleted file mode 100644
index 02fb7af..0000000
--- a/tests/cts/hostsidetests/healthconnect/libs/HealthConnectTestLib/src/android/healthconnect/cts/lib/TestUtils.java
+++ /dev/null
@@ -1,862 +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.lib;
-
-import static android.content.pm.PackageManager.GET_PERMISSIONS;
-
-import static androidx.test.InstrumentationRegistry.getContext;
-
-import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.app.UiAutomation;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.health.connect.DeleteUsingFiltersRequest;
-import android.health.connect.FetchDataOriginsPriorityOrderResponse;
-import android.health.connect.HealthConnectException;
-import android.health.connect.HealthConnectManager;
-import android.health.connect.HealthPermissions;
-import android.health.connect.InsertRecordsResponse;
-import android.health.connect.ReadRecordsRequest;
-import android.health.connect.ReadRecordsResponse;
-import android.health.connect.RecordIdFilter;
-import android.health.connect.TimeInstantRangeFilter;
-import android.health.connect.UpdateDataOriginPriorityOrderRequest;
-import android.health.connect.changelog.ChangeLogTokenRequest;
-import android.health.connect.changelog.ChangeLogTokenResponse;
-import android.health.connect.changelog.ChangeLogsRequest;
-import android.health.connect.changelog.ChangeLogsResponse;
-import android.health.connect.datatypes.BasalMetabolicRateRecord;
-import android.health.connect.datatypes.DataOrigin;
-import android.health.connect.datatypes.Device;
-import android.health.connect.datatypes.ExerciseRoute;
-import android.health.connect.datatypes.ExerciseSessionRecord;
-import android.health.connect.datatypes.ExerciseSessionType;
-import android.health.connect.datatypes.HeartRateRecord;
-import android.health.connect.datatypes.Metadata;
-import android.health.connect.datatypes.Record;
-import android.health.connect.datatypes.StepsRecord;
-import android.health.connect.datatypes.units.Power;
-import android.os.Bundle;
-import android.os.OutcomeReceiver;
-import android.os.UserHandle;
-import android.util.Log;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.core.app.ApplicationProvider;
-
-import com.android.cts.install.lib.TestApp;
-
-import java.io.Serializable;
-import java.time.Instant;
-import java.time.ZoneOffset;
-import java.time.temporal.ChronoUnit;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicReference;
-
-public class TestUtils {
-    static final String TAG = "HealthConnectTest";
-    public static final String MANAGE_HEALTH_PERMISSION =
-            HealthPermissions.MANAGE_HEALTH_PERMISSIONS;
-    public static final String QUERY_TYPE = "android.healthconnect.cts.queryType";
-    public static final String INTENT_EXTRA_CALLING_PKG = "android.healthconnect.cts.calling_pkg";
-    public static final String APP_PKG_NAME_USED_IN_DATA_ORIGIN =
-            "android.healthconnect.cts.pkg.usedInDataOrigin";
-    public static final String INSERT_RECORD_QUERY = "android.healthconnect.cts.insertRecord";
-    public static final String READ_RECORDS_QUERY = "android.healthconnect.cts.readRecords";
-    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 READ_RECORD_CLASS_NAME =
-            "android.healthconnect.cts.readRecordsClass";
-    public static final String READ_CHANGE_LOGS_QUERY = "android.healthconnect.cts.readChangeLogs";
-    public static final String CHANGE_LOGS_RESPONSE =
-            "android.healthconnect.cts.changeLogsResponse";
-    public static final String CHANGE_LOG_TOKEN = "android.healthconnect.cts.changeLogToken";
-    public static final String SUCCESS = "android.healthconnect.cts.success";
-    public static final String CLIENT_ID = "android.healthconnect.cts.clientId";
-    public static final String RECORD_IDS = "android.healthconnect.cts.records";
-    public static final String DELETE_RECORDS_QUERY = "android.healthconnect.cts.deleteRecords";
-    public static final String UPDATE_RECORDS_QUERY = "android.healthconnect.cts.updateRecords";
-    public static final String UPDATE_EXERCISE_ROUTE = "android.healthconnect.cts.updateRoute";
-
-    public static final String UPSERT_EXERCISE_ROUTE = "android.healthconnect.cts.upsertRoute";
-    public static final String GET_CHANGE_LOG_TOKEN_QUERY =
-            "android.healthconnect.cts.getChangeLogToken";
-    public static final String INTENT_EXCEPTION = "android.healthconnect.cts.exception";
-    private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(20);
-    private static final String HEALTH_PERMISSION_PREFIX = "android.permission.health.";
-
-    public static class RecordTypeAndRecordIds implements Serializable {
-        private String mRecordType;
-        private List<String> mRecordIds;
-
-        public RecordTypeAndRecordIds(String recordType, List<String> ids) {
-            mRecordType = recordType;
-            mRecordIds = ids;
-        }
-
-        public String getRecordType() {
-            return mRecordType;
-        }
-
-        public List<String> getRecordIds() {
-            return mRecordIds;
-        }
-    }
-
-    public static Bundle insertRecordAs(TestApp testApp) throws Exception {
-        Bundle bundle = new Bundle();
-        bundle.putString(QUERY_TYPE, INSERT_RECORD_QUERY);
-
-        return getFromTestApp(testApp, bundle);
-    }
-
-    public static Bundle deleteRecordsAs(
-            TestApp testApp, List<RecordTypeAndRecordIds> listOfRecordIdsAndClass)
-            throws Exception {
-        Bundle bundle = new Bundle();
-        bundle.putString(QUERY_TYPE, DELETE_RECORDS_QUERY);
-        bundle.putSerializable(RECORD_IDS, (Serializable) listOfRecordIdsAndClass);
-
-        return getFromTestApp(testApp, bundle);
-    }
-
-    public static Bundle updateRecordsAs(
-            TestApp testAppToUpdateData, List<RecordTypeAndRecordIds> listOfRecordIdsAndClass)
-            throws Exception {
-        Bundle bundle = new Bundle();
-        bundle.putString(QUERY_TYPE, UPDATE_RECORDS_QUERY);
-        bundle.putSerializable(RECORD_IDS, (Serializable) listOfRecordIdsAndClass);
-
-        return getFromTestApp(testAppToUpdateData, bundle);
-    }
-
-    public static Bundle updateRouteAs(TestApp testAppToUpdateData) throws Exception {
-        Bundle bundle = new Bundle();
-        bundle.putString(QUERY_TYPE, UPDATE_EXERCISE_ROUTE);
-        return getFromTestApp(testAppToUpdateData, bundle);
-    }
-
-    public static Bundle insertSessionNoRouteAs(TestApp testAppToUpdateData) throws Exception {
-        Bundle bundle = new Bundle();
-        bundle.putString(QUERY_TYPE, UPSERT_EXERCISE_ROUTE);
-        return getFromTestApp(testAppToUpdateData, bundle);
-    }
-
-    public static Bundle insertRecordWithAnotherAppPackageName(
-            TestApp testAppToInsertData, TestApp testAppPkgNameUsed) throws Exception {
-        Bundle bundle = new Bundle();
-        bundle.putString(QUERY_TYPE, INSERT_RECORD_QUERY);
-        bundle.putString(APP_PKG_NAME_USED_IN_DATA_ORIGIN, testAppPkgNameUsed.getPackageName());
-
-        return getFromTestApp(testAppToInsertData, bundle);
-    }
-
-    public static Bundle readRecordsAs(TestApp testApp, ArrayList<String> recordClassesToRead)
-            throws Exception {
-        Bundle bundle = new Bundle();
-        bundle.putString(QUERY_TYPE, READ_RECORDS_QUERY);
-        bundle.putStringArrayList(READ_RECORD_CLASS_NAME, recordClassesToRead);
-
-        return getFromTestApp(testApp, bundle);
-    }
-
-    public static Bundle insertRecordWithGivenClientId(TestApp testApp, double clientId)
-            throws Exception {
-        Bundle bundle = new Bundle();
-        bundle.putString(QUERY_TYPE, INSERT_RECORD_QUERY);
-        bundle.putDouble(CLIENT_ID, clientId);
-
-        return getFromTestApp(testApp, bundle);
-    }
-
-    public static void verifyDeleteRecords(DeleteUsingFiltersRequest request)
-            throws InterruptedException {
-        UiAutomation uiAutomation =
-                androidx.test.platform.app.InstrumentationRegistry.getInstrumentation()
-                        .getUiAutomation();
-        uiAutomation.adoptShellPermissionIdentity("android.permission.MANAGE_HEALTH_DATA");
-        try {
-            Context context = ApplicationProvider.getApplicationContext();
-            HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-            CountDownLatch latch = new CountDownLatch(1);
-            AtomicReference<HealthConnectException> exceptionAtomicReference =
-                    new AtomicReference<>();
-            assertThat(service).isNotNull();
-            service.deleteRecords(
-                    request,
-                    Executors.newSingleThreadExecutor(),
-                    new OutcomeReceiver<>() {
-                        @Override
-                        public void onResult(Void result) {
-                            latch.countDown();
-                        }
-
-                        @Override
-                        public void onError(HealthConnectException healthConnectException) {
-                            exceptionAtomicReference.set(healthConnectException);
-                            latch.countDown();
-                        }
-                    });
-            assertThat(latch.await(3, TimeUnit.SECONDS)).isEqualTo(true);
-            if (exceptionAtomicReference.get() != null) {
-                throw exceptionAtomicReference.get();
-            }
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    public static Bundle readRecordsUsingDataOriginFiltersAs(
-            TestApp testApp, ArrayList<String> recordClassesToRead) throws Exception {
-        Bundle bundle = new Bundle();
-        bundle.putString(QUERY_TYPE, READ_RECORDS_QUERY);
-        bundle.putStringArrayList(READ_RECORD_CLASS_NAME, recordClassesToRead);
-        bundle.putBoolean(READ_USING_DATA_ORIGIN_FILTERS, true);
-
-        return getFromTestApp(testApp, bundle);
-    }
-
-    public static Bundle readChangeLogsUsingDataOriginFiltersAs(
-            TestApp testApp, String changeLogToken) throws Exception {
-        Bundle bundle = new Bundle();
-        bundle.putString(QUERY_TYPE, READ_CHANGE_LOGS_QUERY);
-        bundle.putString(CHANGE_LOG_TOKEN, changeLogToken);
-        bundle.putBoolean(READ_USING_DATA_ORIGIN_FILTERS, true);
-
-        return getFromTestApp(testApp, bundle);
-    }
-
-    public static Bundle getChangeLogTokenAs(TestApp testApp, String pkgName) throws Exception {
-        Bundle bundle = new Bundle();
-        bundle.putString(QUERY_TYPE, GET_CHANGE_LOG_TOKEN_QUERY);
-        bundle.putString(APP_PKG_NAME_USED_IN_DATA_ORIGIN, pkgName);
-
-        return getFromTestApp(testApp, bundle);
-    }
-
-    private static Bundle getFromTestApp(TestApp testApp, Bundle bundleToCreateIntent)
-            throws Exception {
-        final CountDownLatch latch = new CountDownLatch(1);
-        AtomicReference<Bundle> response = new AtomicReference<>();
-        AtomicReference<Exception> exceptionAtomicReference = new AtomicReference<>();
-        BroadcastReceiver broadcastReceiver =
-                new BroadcastReceiver() {
-                    @Override
-                    public void onReceive(Context context, Intent intent) {
-                        if (intent.hasExtra(INTENT_EXCEPTION)) {
-                            exceptionAtomicReference.set(
-                                    (Exception) (intent.getSerializableExtra(INTENT_EXCEPTION)));
-                        } else {
-                            response.set(intent.getExtras());
-                        }
-                        latch.countDown();
-                    }
-                };
-
-        launchTestApp(testApp, bundleToCreateIntent, broadcastReceiver, latch);
-        if (exceptionAtomicReference.get() != null) {
-            throw exceptionAtomicReference.get();
-        }
-        return response.get();
-    }
-
-    private static void launchTestApp(
-            TestApp testApp,
-            Bundle bundleToCreateIntent,
-            BroadcastReceiver broadcastReceiver,
-            CountDownLatch latch)
-            throws Exception {
-
-        // Register broadcast receiver
-        final IntentFilter intentFilter = new IntentFilter();
-        intentFilter.addAction(bundleToCreateIntent.getString(QUERY_TYPE));
-        intentFilter.addCategory(Intent.CATEGORY_DEFAULT);
-        getContext().registerReceiver(broadcastReceiver, intentFilter, Context.RECEIVER_EXPORTED);
-
-        // Launch the test app.
-        final Intent intent = new Intent(Intent.ACTION_MAIN);
-        intent.setPackage(testApp.getPackageName());
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
-        intent.putExtra(INTENT_EXTRA_CALLING_PKG, getContext().getPackageName());
-        intent.putExtras(bundleToCreateIntent);
-        intent.addCategory(Intent.CATEGORY_LAUNCHER);
-        intent.putExtras(bundleToCreateIntent);
-
-        Thread.sleep(500);
-        getContext().startActivity(intent);
-        if (!latch.await(POLLING_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
-            final String errorMessage =
-                    "Timed out while waiting to receive "
-                            + bundleToCreateIntent.getString(QUERY_TYPE)
-                            + " intent from "
-                            + testApp.getPackageName();
-            throw new TimeoutException(errorMessage);
-        }
-        getContext().unregisterReceiver(broadcastReceiver);
-    }
-
-    public static String insertRecordAndGetId(Record record, Context context)
-            throws InterruptedException {
-        return insertRecords(Collections.singletonList(record), context)
-                .get(0)
-                .getMetadata()
-                .getId();
-    }
-
-    public static List<RecordTypeAndRecordIds> insertRecordsAndGetIds(
-            List<Record> records, Context context) throws InterruptedException {
-        List<Record> insertedRecords = insertRecords(records, context);
-
-        Map<String, List<String>> recordTypeToRecordIdsMap = new HashMap<>();
-        for (Record record : insertedRecords) {
-            recordTypeToRecordIdsMap.putIfAbsent(record.getClass().getName(), new ArrayList<>());
-            recordTypeToRecordIdsMap
-                    .get(record.getClass().getName())
-                    .add(record.getMetadata().getId());
-        }
-
-        List<RecordTypeAndRecordIds> recordTypeAndRecordIdsList = new ArrayList<>();
-        for (String recordType : recordTypeToRecordIdsMap.keySet()) {
-            recordTypeAndRecordIdsList.add(
-                    new RecordTypeAndRecordIds(
-                            recordType, recordTypeToRecordIdsMap.get(recordType)));
-        }
-
-        return recordTypeAndRecordIdsList;
-    }
-
-    public static List<Record> insertRecords(List<Record> records, Context context)
-            throws InterruptedException {
-        CountDownLatch latch = new CountDownLatch(1);
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        assertThat(service).isNotNull();
-        AtomicReference<List<Record>> response = new AtomicReference<>();
-        AtomicReference<HealthConnectException> exceptionAtomicReference = new AtomicReference<>();
-        service.insertRecords(
-                records,
-                Executors.newSingleThreadExecutor(),
-                new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(InsertRecordsResponse result) {
-                        response.set(result.getRecords());
-                        latch.countDown();
-                    }
-
-                    @Override
-                    public void onError(HealthConnectException exception) {
-                        Log.e(TAG, exception.getMessage());
-                        exceptionAtomicReference.set(exception);
-                        latch.countDown();
-                    }
-                });
-        assertThat(latch.await(3, TimeUnit.SECONDS)).isTrue();
-        if (exceptionAtomicReference.get() != null) {
-            throw exceptionAtomicReference.get();
-        }
-        assertThat(response.get()).hasSize(records.size());
-
-        return response.get();
-    }
-
-    public static List<Record> getTestRecords(String packageName) {
-        double clientId = Math.random();
-        return getTestRecords(packageName, clientId);
-    }
-
-    public static List<Record> getTestRecords(String packageName, Double clientId) {
-        return Arrays.asList(
-                getExerciseSessionRecord(packageName, clientId, /* withRoute= */ true),
-                getStepsRecord(packageName, clientId),
-                getHeartRateRecord(packageName, clientId),
-                getBasalMetabolicRateRecord(packageName, clientId));
-    }
-
-    public static void deleteTestData() throws InterruptedException {
-        verifyDeleteRecords(
-                new DeleteUsingFiltersRequest.Builder()
-                        .setTimeRangeFilter(
-                                new TimeInstantRangeFilter.Builder()
-                                        .setStartTime(Instant.EPOCH)
-                                        .setEndTime(Instant.now().plus(10, ChronoUnit.DAYS))
-                                        .build())
-                        .addRecordType(ExerciseSessionRecord.class)
-                        .addRecordType(StepsRecord.class)
-                        .addRecordType(HeartRateRecord.class)
-                        .addRecordType(BasalMetabolicRateRecord.class)
-                        .build());
-    }
-
-    public static ExerciseSessionRecord getExerciseSessionRecord(
-            String packageName, double clientId, boolean withRoute) {
-        Instant startTime = Instant.now().minusSeconds(3000);
-        Instant endTime = Instant.now();
-        ExerciseSessionRecord.Builder builder =
-                new ExerciseSessionRecord.Builder(
-                                buildSessionMetadata(packageName, clientId),
-                                startTime,
-                                endTime,
-                                ExerciseSessionType.EXERCISE_SESSION_TYPE_OTHER_WORKOUT)
-                        .setEndZoneOffset(ZoneOffset.MAX)
-                        .setStartZoneOffset(ZoneOffset.MIN)
-                        .setNotes("notes")
-                        .setTitle("title");
-
-        if (withRoute) {
-            builder.setRoute(
-                    new ExerciseRoute(
-                            List.of(
-                                    new ExerciseRoute.Location.Builder(startTime, 50., 50.).build(),
-                                    new ExerciseRoute.Location.Builder(
-                                                    startTime.plusSeconds(2), 51., 51.)
-                                            .build())));
-        }
-        return builder.build();
-    }
-
-    public static StepsRecord getStepsRecord(String packageName, double clientId) {
-        Device device =
-                new Device.Builder().setManufacturer("google").setModel("Pixel").setType(1).build();
-        DataOrigin dataOrigin = new DataOrigin.Builder().setPackageName(packageName).build();
-        return new StepsRecord.Builder(
-                        new Metadata.Builder()
-                                .setDevice(device)
-                                .setDataOrigin(dataOrigin)
-                                .setClientRecordId("SR" + clientId)
-                                .build(),
-                        Instant.now(),
-                        Instant.now().plusMillis(1000),
-                        10)
-                .build();
-    }
-
-    public static HeartRateRecord getHeartRateRecord(String packageName, double clientId) {
-        HeartRateRecord.HeartRateSample heartRateSample =
-                new HeartRateRecord.HeartRateSample(72, Instant.now().plusMillis(100));
-        ArrayList<HeartRateRecord.HeartRateSample> heartRateSamples = new ArrayList<>();
-        heartRateSamples.add(heartRateSample);
-        heartRateSamples.add(heartRateSample);
-        Device device =
-                new Device.Builder().setManufacturer("google").setModel("Pixel").setType(1).build();
-        DataOrigin dataOrigin = new DataOrigin.Builder().setPackageName(packageName).build();
-
-        return new HeartRateRecord.Builder(
-                        new Metadata.Builder()
-                                .setDevice(device)
-                                .setDataOrigin(dataOrigin)
-                                .setClientRecordId("HR" + clientId)
-                                .build(),
-                        Instant.now(),
-                        Instant.now().plusMillis(500),
-                        heartRateSamples)
-                .build();
-    }
-
-    public static BasalMetabolicRateRecord getBasalMetabolicRateRecord(
-            String packageName, double clientId) {
-        Device device =
-                new Device.Builder()
-                        .setManufacturer("google")
-                        .setModel("Pixel4a")
-                        .setType(2)
-                        .build();
-        DataOrigin dataOrigin = new DataOrigin.Builder().setPackageName(packageName).build();
-        return new BasalMetabolicRateRecord.Builder(
-                        new Metadata.Builder()
-                                .setDevice(device)
-                                .setDataOrigin(dataOrigin)
-                                .setClientRecordId("BMR" + clientId)
-                                .build(),
-                        Instant.now(),
-                        Power.fromWatts(100.0))
-                .build();
-    }
-
-    public static void verifyDeleteRecords(List<RecordIdFilter> request, Context context)
-            throws InterruptedException {
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        CountDownLatch latch = new CountDownLatch(1);
-        AtomicReference<HealthConnectException> exceptionAtomicReference = new AtomicReference<>();
-        assertThat(service).isNotNull();
-        service.deleteRecords(
-                request,
-                Executors.newSingleThreadExecutor(),
-                new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(Void result) {
-                        latch.countDown();
-                    }
-
-                    @Override
-                    public void onError(HealthConnectException healthConnectException) {
-                        exceptionAtomicReference.set(healthConnectException);
-                        latch.countDown();
-                    }
-                });
-        assertThat(latch.await(3, TimeUnit.SECONDS)).isEqualTo(true);
-        if (exceptionAtomicReference.get() != null) {
-            throw exceptionAtomicReference.get();
-        }
-    }
-
-    public static <T extends Record> List<T> readRecords(
-            ReadRecordsRequest<T> request, Context context) throws InterruptedException {
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        CountDownLatch latch = new CountDownLatch(1);
-        assertThat(service).isNotNull();
-        assertThat(request.getRecordType()).isNotNull();
-        AtomicReference<List<T>> response = new AtomicReference<>();
-        AtomicReference<HealthConnectException> healthConnectExceptionAtomicReference =
-                new AtomicReference<>();
-        service.readRecords(
-                request,
-                Executors.newSingleThreadExecutor(),
-                new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(ReadRecordsResponse<T> result) {
-                        response.set(result.getRecords());
-                        latch.countDown();
-                    }
-
-                    @Override
-                    public void onError(HealthConnectException exception) {
-                        Log.e(TAG, exception.getMessage());
-                        healthConnectExceptionAtomicReference.set(exception);
-                        latch.countDown();
-                    }
-                });
-        assertThat(latch.await(3, TimeUnit.SECONDS)).isEqualTo(true);
-        if (healthConnectExceptionAtomicReference.get() != null) {
-            throw healthConnectExceptionAtomicReference.get();
-        }
-        return response.get();
-    }
-
-    public static void updateRecords(List<Record> records, Context context)
-            throws InterruptedException {
-        CountDownLatch latch = new CountDownLatch(1);
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        assertThat(service).isNotNull();
-        AtomicReference<HealthConnectException> exceptionAtomicReference = new AtomicReference<>();
-
-        service.updateRecords(
-                records,
-                Executors.newSingleThreadExecutor(),
-                new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(Void result) {
-                        latch.countDown();
-                    }
-
-                    @Override
-                    public void onError(HealthConnectException exception) {
-                        exceptionAtomicReference.set(exception);
-                        latch.countDown();
-                    }
-                });
-        assertThat(latch.await(3, TimeUnit.SECONDS)).isTrue();
-        if (exceptionAtomicReference.get() != null) {
-            throw exceptionAtomicReference.get();
-        }
-    }
-
-    public static <T extends Record> List<T> readRecords(ReadRecordsRequest<T> request)
-            throws InterruptedException {
-        Context context = ApplicationProvider.getApplicationContext();
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        CountDownLatch latch = new CountDownLatch(1);
-        assertThat(service).isNotNull();
-        assertThat(request.getRecordType()).isNotNull();
-        AtomicReference<List<T>> response = new AtomicReference<>();
-        AtomicReference<HealthConnectException> healthConnectExceptionAtomicReference =
-                new AtomicReference<>();
-        service.readRecords(
-                request,
-                Executors.newSingleThreadExecutor(),
-                new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(ReadRecordsResponse<T> result) {
-                        response.set(result.getRecords());
-                        latch.countDown();
-                    }
-
-                    @Override
-                    public void onError(HealthConnectException exception) {
-                        Log.e(TAG, exception.getMessage());
-                        healthConnectExceptionAtomicReference.set(exception);
-                        latch.countDown();
-                    }
-                });
-        assertThat(latch.await(3, TimeUnit.SECONDS)).isEqualTo(true);
-        if (healthConnectExceptionAtomicReference.get() != null) {
-            throw healthConnectExceptionAtomicReference.get();
-        }
-        return response.get();
-    }
-
-    public static ChangeLogTokenResponse getChangeLogToken(
-            ChangeLogTokenRequest request, Context context) throws InterruptedException {
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        assertThat(service).isNotNull();
-        CountDownLatch latch = new CountDownLatch(1);
-        AtomicReference<ChangeLogTokenResponse> response = new AtomicReference<>();
-        AtomicReference<HealthConnectException> exceptionAtomicReference = new AtomicReference<>();
-        service.getChangeLogToken(
-                request,
-                Executors.newSingleThreadExecutor(),
-                new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(ChangeLogTokenResponse result) {
-                        response.set(result);
-                        latch.countDown();
-                    }
-
-                    @Override
-                    public void onError(HealthConnectException exception) {
-                        Log.e(TAG, exception.getMessage());
-                        exceptionAtomicReference.set(exception);
-                        latch.countDown();
-                    }
-                });
-        assertThat(latch.await(3, TimeUnit.SECONDS)).isTrue();
-        if (exceptionAtomicReference.get() != null) {
-            throw exceptionAtomicReference.get();
-        }
-        return response.get();
-    }
-
-    public static ChangeLogsResponse getChangeLogs(
-            ChangeLogsRequest changeLogsRequest, Context context) throws InterruptedException {
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        assertThat(service).isNotNull();
-
-        CountDownLatch latch = new CountDownLatch(1);
-        AtomicReference<ChangeLogsResponse> response = new AtomicReference<>();
-        AtomicReference<HealthConnectException> healthConnectExceptionAtomicReference =
-                new AtomicReference<>();
-        service.getChangeLogs(
-                changeLogsRequest,
-                Executors.newSingleThreadExecutor(),
-                new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(ChangeLogsResponse result) {
-                        response.set(result);
-                        latch.countDown();
-                    }
-
-                    @Override
-                    public void onError(HealthConnectException exception) {
-                        healthConnectExceptionAtomicReference.set(exception);
-                        latch.countDown();
-                    }
-                });
-        assertThat(latch.await(3, TimeUnit.SECONDS)).isEqualTo(true);
-        if (healthConnectExceptionAtomicReference.get() != null) {
-            throw healthConnectExceptionAtomicReference.get();
-        }
-
-        return response.get();
-    }
-
-    public static FetchDataOriginsPriorityOrderResponse fetchDataOriginsPriorityOrder(
-            int dataCategory) throws InterruptedException {
-        Context context = ApplicationProvider.getApplicationContext();
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        assertThat(service).isNotNull();
-
-        CountDownLatch latch = new CountDownLatch(1);
-        AtomicReference<FetchDataOriginsPriorityOrderResponse> response = new AtomicReference<>();
-        AtomicReference<HealthConnectException> exceptionAtomicReference = new AtomicReference<>();
-        service.fetchDataOriginsPriorityOrder(
-                dataCategory,
-                Executors.newSingleThreadExecutor(),
-                new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(FetchDataOriginsPriorityOrderResponse result) {
-                        response.set(result);
-                        latch.countDown();
-                    }
-
-                    @Override
-                    public void onError(HealthConnectException exception) {
-                        exceptionAtomicReference.set(exception);
-                        latch.countDown();
-                    }
-                });
-        assertThat(latch.await(3, TimeUnit.SECONDS)).isTrue();
-        if (exceptionAtomicReference.get() != null) {
-            throw exceptionAtomicReference.get();
-        }
-        return response.get();
-    }
-
-    public static void updateDataOriginPriorityOrder(UpdateDataOriginPriorityOrderRequest request)
-            throws InterruptedException {
-        Context context = ApplicationProvider.getApplicationContext();
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        assertThat(service).isNotNull();
-
-        CountDownLatch latch = new CountDownLatch(1);
-        AtomicReference<HealthConnectException> exceptionAtomicReference = new AtomicReference<>();
-        service.updateDataOriginPriorityOrder(
-                request,
-                Executors.newSingleThreadExecutor(),
-                new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(Void result) {
-                        latch.countDown();
-                    }
-
-                    @Override
-                    public void onError(HealthConnectException healthConnectException) {
-                        exceptionAtomicReference.set(healthConnectException);
-                        latch.countDown();
-                    }
-                });
-        assertThat(latch.await(3, TimeUnit.SECONDS)).isEqualTo(true);
-        if (exceptionAtomicReference.get() != null) {
-            throw exceptionAtomicReference.get();
-        }
-    }
-
-    public static void deleteAllStagedRemoteData() {
-        Context context = ApplicationProvider.getApplicationContext();
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        assertThat(service).isNotNull();
-        runWithShellPermissionIdentity(
-                () ->
-                        // TODO(b/241542162): Avoid reflection once TestApi can be called from CTS
-                        service.getClass().getMethod("deleteAllStagedRemoteData").invoke(service),
-                "android.permission.DELETE_STAGED_HEALTH_CONNECT_REMOTE_DATA");
-    }
-
-    private static Metadata buildSessionMetadata(String packageName, double clientId) {
-        Device device =
-                new Device.Builder().setManufacturer("google").setModel("Pixel").setType(1).build();
-        DataOrigin dataOrigin = new DataOrigin.Builder().setPackageName(packageName).build();
-        return new Metadata.Builder()
-                .setDevice(device)
-                .setDataOrigin(dataOrigin)
-                .setClientRecordId(String.valueOf(clientId))
-                .build();
-    }
-
-    public static void grantPermission(String pkgName, String permission) {
-        Context context = ApplicationProvider.getApplicationContext();
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        assertThat(service).isNotNull();
-        runWithShellPermissionIdentity(
-                () ->
-                        service.getClass()
-                                .getMethod("grantHealthPermission", String.class, String.class)
-                                .invoke(service, pkgName, permission),
-                MANAGE_HEALTH_PERMISSION);
-    }
-
-    public static void revokePermission(String pkgName, String permission) {
-        Context context = ApplicationProvider.getApplicationContext();
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        assertThat(service).isNotNull();
-        runWithShellPermissionIdentity(
-                () ->
-                        service.getClass()
-                                .getMethod(
-                                        "revokeHealthPermission",
-                                        String.class,
-                                        String.class,
-                                        String.class)
-                                .invoke(service, pkgName, permission, null),
-                MANAGE_HEALTH_PERMISSION);
-    }
-
-    public static void revokeHealthPermissions(String packageName) {
-        runWithShellPermissionIdentity(() -> revokeHealthPermissionsPrivileged(packageName));
-    }
-
-    private static void revokeHealthPermissionsPrivileged(String packageName)
-            throws PackageManager.NameNotFoundException {
-        final Context targetContext = InstrumentationRegistry.getTargetContext();
-        final PackageManager packageManager = targetContext.getPackageManager();
-        final UserHandle user = targetContext.getUser();
-
-        final PackageInfo packageInfo =
-                packageManager.getPackageInfo(
-                        packageName,
-                        PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS));
-
-        final String[] permissions = packageInfo.requestedPermissions;
-        if (permissions == null) {
-            return;
-        }
-
-        for (String permission : permissions) {
-            if (permission.startsWith(HEALTH_PERMISSION_PREFIX)) {
-                packageManager.revokeRuntimePermission(packageName, permission, user);
-            }
-        }
-    }
-
-    public static List<String> getGrantedHealthPermissions(String pkgName) {
-        final PackageInfo pi = getAppPackageInfo(pkgName);
-        final String[] requestedPermissions = pi.requestedPermissions;
-        final int[] requestedPermissionsFlags = pi.requestedPermissionsFlags;
-
-        if (requestedPermissions == null) {
-            return List.of();
-        }
-
-        final List<String> permissions = new ArrayList<>();
-
-        for (int i = 0; i < requestedPermissions.length; i++) {
-            if ((requestedPermissionsFlags[i] & PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0) {
-                if (requestedPermissions[i].startsWith(HEALTH_PERMISSION_PREFIX)) {
-                    permissions.add(requestedPermissions[i]);
-                }
-            }
-        }
-
-        return permissions;
-    }
-
-    private static PackageInfo getAppPackageInfo(String pkgName) {
-        final Context targetContext = InstrumentationRegistry.getTargetContext();
-        return runWithShellPermissionIdentity(
-                () ->
-                        targetContext
-                                .getPackageManager()
-                                .getPackageInfo(
-                                        pkgName,
-                                        PackageManager.PackageInfoFlags.of(GET_PERMISSIONS)));
-    }
-}
diff --git a/tests/cts/src/android/healthconnect/cts/ActiveCaloriesBurnedRecordTest.java b/tests/cts/src/android/healthconnect/cts/ActiveCaloriesBurnedRecordTest.java
index 0c5f9ca..01a6b99 100644
--- a/tests/cts/src/android/healthconnect/cts/ActiveCaloriesBurnedRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/ActiveCaloriesBurnedRecordTest.java
@@ -39,6 +39,7 @@
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.units.Energy;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
diff --git a/tests/cts/src/android/healthconnect/cts/BasalBodyTemperatureRecordTest.java b/tests/cts/src/android/healthconnect/cts/BasalBodyTemperatureRecordTest.java
index 7837dc3..2aefe73 100644
--- a/tests/cts/src/android/healthconnect/cts/BasalBodyTemperatureRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/BasalBodyTemperatureRecordTest.java
@@ -35,6 +35,7 @@
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.units.Temperature;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
diff --git a/tests/cts/src/android/healthconnect/cts/BasalMetabolicRateRecordTest.java b/tests/cts/src/android/healthconnect/cts/BasalMetabolicRateRecordTest.java
index de7a085..da6a6e0 100644
--- a/tests/cts/src/android/healthconnect/cts/BasalMetabolicRateRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/BasalMetabolicRateRecordTest.java
@@ -42,6 +42,7 @@
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.units.Energy;
 import android.health.connect.datatypes.units.Power;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
@@ -516,6 +517,198 @@
     }
 
     @Test
+    public void testAggregation_basalCaloriesBurntTotal_onlyWeightBeforeInterval_usesProfile()
+            throws Exception {
+        Instant now = Instant.now();
+        List<Record> records =
+                List.of(
+                        WeightRecordTest.getBaseWeightRecord(
+                                now.minus(10, ChronoUnit.DAYS), /* weight= */ 40000));
+        AggregateRecordsResponse<Energy> response =
+                TestUtils.getAggregateResponse(
+                        new AggregateRecordsRequest.Builder<Energy>(
+                                        new TimeInstantRangeFilter.Builder()
+                                                .setStartTime(now.minus(2, ChronoUnit.DAYS))
+                                                .setEndTime(now)
+                                                .build())
+                                .addAggregationType(BASAL_CALORIES_TOTAL)
+                                .build(),
+                        records);
+        assertThat(response.get(BASAL_CALORIES_TOTAL)).isNotNull();
+        Energy energy = response.get(BASAL_CALORIES_TOTAL);
+        assertThat(energy.getInCalories()).isWithin(1).of(2469000);
+    }
+
+    @Test
+    public void testAggregation_basalCaloriesBurntTotal_HeightAndWeightBeforeInterval_usesProfile()
+            throws Exception {
+        Instant now = Instant.now();
+        List<Record> records =
+                List.of(
+                        WeightRecordTest.getBaseWeightRecord(
+                                now.minus(10, ChronoUnit.DAYS), /* weight= */ 40000),
+                        HeightRecordTest.getBaseHeightRecord(
+                                now.minus(9, ChronoUnit.DAYS), /* height= */ 2.0));
+        AggregateRecordsResponse<Energy> response =
+                TestUtils.getAggregateResponse(
+                        new AggregateRecordsRequest.Builder<Energy>(
+                                        new TimeInstantRangeFilter.Builder()
+                                                .setStartTime(now.minus(2, ChronoUnit.DAYS))
+                                                .setEndTime(now)
+                                                .build())
+                                .addAggregationType(BASAL_CALORIES_TOTAL)
+                                .build(),
+                        records);
+        assertThat(response.get(BASAL_CALORIES_TOTAL)).isNotNull();
+        Energy energy = response.get(BASAL_CALORIES_TOTAL);
+        assertThat(energy.getInCalories()).isWithin(1).of(2844000);
+    }
+
+    @Test
+    public void
+            testAggregation_basalCaloriesBurntTotal_HeightWeightBeforeAndAfterInterval_usesProfile()
+                    throws Exception {
+        Instant now = Instant.now();
+        List<Record> records =
+                List.of(
+                        WeightRecordTest.getBaseWeightRecord(
+                                now.minus(10, ChronoUnit.DAYS), /* weight= */ 40000),
+                        HeightRecordTest.getBaseHeightRecord(
+                                now.minus(9, ChronoUnit.DAYS), /* height= */ 1.8),
+                        WeightRecordTest.getBaseWeightRecord(
+                                now.minus(1, ChronoUnit.DAYS), /* weight= */ 60000),
+                        HeightRecordTest.getBaseHeightRecord(
+                                now.minus(1, ChronoUnit.DAYS), /* height= */ 1.9),
+                        HeightRecordTest.getBaseHeightRecord(now, /* height= */ 2.0),
+                        WeightRecordTest.getBaseWeightRecord(now, /* weight= */ 70000),
+                        HeightRecordTest.getBaseHeightRecord(now, /* height= */ 2.0));
+        AggregateRecordsResponse<Energy> response =
+                TestUtils.getAggregateResponse(
+                        new AggregateRecordsRequest.Builder<Energy>(
+                                        new TimeInstantRangeFilter.Builder()
+                                                .setStartTime(now.minus(3, ChronoUnit.DAYS))
+                                                .setEndTime(now.minus(1, ChronoUnit.DAYS))
+                                                .build())
+                                .addAggregationType(BASAL_CALORIES_TOTAL)
+                                .build(),
+                        records);
+        assertThat(response.get(BASAL_CALORIES_TOTAL)).isNotNull();
+        Energy energy = response.get(BASAL_CALORIES_TOTAL);
+        assertThat(energy.getInCalories()).isWithin(1).of(2594000);
+    }
+
+    @Test
+    public void testAggregation_basalCaloriesBurntTotal_onlyWeightDuringInterval_usesProfile()
+            throws Exception {
+        Instant now = Instant.now();
+        List<Record> records =
+                List.of(
+                        WeightRecordTest.getBaseWeightRecord(
+                                now.minus(1, ChronoUnit.DAYS), /* weight= */ 40000));
+        AggregateRecordsResponse<Energy> response =
+                TestUtils.getAggregateResponse(
+                        new AggregateRecordsRequest.Builder<Energy>(
+                                        new TimeInstantRangeFilter.Builder()
+                                                .setStartTime(now.minus(2, ChronoUnit.DAYS))
+                                                .setEndTime(now)
+                                                .build())
+                                .addAggregationType(BASAL_CALORIES_TOTAL)
+                                .build(),
+                        records);
+        assertThat(response.get(BASAL_CALORIES_TOTAL)).isNotNull();
+        Energy energy = response.get(BASAL_CALORIES_TOTAL);
+        assertThat(energy.getInCalories()).isWithin(1).of(2799000);
+    }
+
+    @Test
+    public void testAggregation_basalCaloriesBurntTotal_onlyWeightAfterInterval_usesDefaultProfile()
+            throws Exception {
+        Instant now = Instant.now();
+        List<Record> records =
+                List.of(WeightRecordTest.getBaseWeightRecord(now, /* weight= */ 40000));
+        AggregateRecordsResponse<Energy> response =
+                TestUtils.getAggregateResponse(
+                        new AggregateRecordsRequest.Builder<Energy>(
+                                        new TimeInstantRangeFilter.Builder()
+                                                .setStartTime(now.minus(2, ChronoUnit.DAYS))
+                                                .setEndTime(now.minus(1, ChronoUnit.DAYS))
+                                                .build())
+                                .addAggregationType(BASAL_CALORIES_TOTAL)
+                                .build(),
+                        records);
+        assertThat(response.get(BASAL_CALORIES_TOTAL)).isNotNull();
+        Energy energy = response.get(BASAL_CALORIES_TOTAL);
+        assertThat(energy.getInCalories()).isWithin(1).of(1564500);
+    }
+
+    @Test
+    public void testAggregation_basalCaloriesBurntTotal_onlyHeightBeforeInterval_usesProfile()
+            throws Exception {
+        Instant now = Instant.now();
+        List<Record> records =
+                List.of(
+                        HeightRecordTest.getBaseHeightRecord(
+                                now.minus(10, ChronoUnit.DAYS), /* height= */ 2.0));
+        AggregateRecordsResponse<Energy> response =
+                TestUtils.getAggregateResponse(
+                        new AggregateRecordsRequest.Builder<Energy>(
+                                        new TimeInstantRangeFilter.Builder()
+                                                .setStartTime(now.minus(2, ChronoUnit.DAYS))
+                                                .setEndTime(now)
+                                                .build())
+                                .addAggregationType(BASAL_CALORIES_TOTAL)
+                                .build(),
+                        records);
+        assertThat(response.get(BASAL_CALORIES_TOTAL)).isNotNull();
+        Energy energy = response.get(BASAL_CALORIES_TOTAL);
+        assertThat(energy.getInCalories()).isWithin(1).of(3504000);
+    }
+
+    @Test
+    public void testAggregation_basalCaloriesBurntTotal_onlyHeightDuringInterval_usesProfile()
+            throws Exception {
+        Instant now = Instant.now();
+        List<Record> records =
+                List.of(
+                        HeightRecordTest.getBaseHeightRecord(
+                                now.minus(1, ChronoUnit.DAYS), /* height= */ 2.0));
+        AggregateRecordsResponse<Energy> response =
+                TestUtils.getAggregateResponse(
+                        new AggregateRecordsRequest.Builder<Energy>(
+                                        new TimeInstantRangeFilter.Builder()
+                                                .setStartTime(now.minus(2, ChronoUnit.DAYS))
+                                                .setEndTime(now)
+                                                .build())
+                                .addAggregationType(BASAL_CALORIES_TOTAL)
+                                .build(),
+                        records);
+        assertThat(response.get(BASAL_CALORIES_TOTAL)).isNotNull();
+        Energy energy = response.get(BASAL_CALORIES_TOTAL);
+        assertThat(energy.getInCalories()).isWithin(1).of(3316500);
+    }
+
+    @Test
+    public void testAggregation_basalCaloriesBurntTotal_onlyHeightAfterInterval_usesDefaultProfile()
+            throws Exception {
+        Instant now = Instant.now();
+        List<Record> records =
+                List.of(HeightRecordTest.getBaseHeightRecord(now, /* height= */ 2.0));
+        AggregateRecordsResponse<Energy> response =
+                TestUtils.getAggregateResponse(
+                        new AggregateRecordsRequest.Builder<Energy>(
+                                        new TimeInstantRangeFilter.Builder()
+                                                .setStartTime(now.minus(2, ChronoUnit.DAYS))
+                                                .setEndTime(now.minus(1, ChronoUnit.DAYS))
+                                                .build())
+                                .addAggregationType(BASAL_CALORIES_TOTAL)
+                                .build(),
+                        records);
+        assertThat(response.get(BASAL_CALORIES_TOTAL)).isNotNull();
+        Energy energy = response.get(BASAL_CALORIES_TOTAL);
+        assertThat(energy.getInCalories()).isWithin(1).of(1564500);
+    }
+
+    @Test
     public void testAggregation_BasalCaloriesBurntTotal_groupByDuration_profileDerived()
             throws Exception {
         Instant now = Instant.now();
diff --git a/tests/cts/src/android/healthconnect/cts/BloodGlucoseRecordTest.java b/tests/cts/src/android/healthconnect/cts/BloodGlucoseRecordTest.java
index 3f68381..d4e58e0 100644
--- a/tests/cts/src/android/healthconnect/cts/BloodGlucoseRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/BloodGlucoseRecordTest.java
@@ -35,6 +35,7 @@
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.units.BloodGlucose;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
diff --git a/tests/cts/src/android/healthconnect/cts/BloodPressureRecordTest.java b/tests/cts/src/android/healthconnect/cts/BloodPressureRecordTest.java
index cd5a2ee..0c84587 100644
--- a/tests/cts/src/android/healthconnect/cts/BloodPressureRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/BloodPressureRecordTest.java
@@ -44,6 +44,7 @@
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.units.Pressure;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
diff --git a/tests/cts/src/android/healthconnect/cts/BodyFatRecordTest.java b/tests/cts/src/android/healthconnect/cts/BodyFatRecordTest.java
index d615882..978f13c 100644
--- a/tests/cts/src/android/healthconnect/cts/BodyFatRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/BodyFatRecordTest.java
@@ -35,6 +35,7 @@
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.units.Percentage;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
diff --git a/tests/cts/src/android/healthconnect/cts/BodyTemperatureRecordTest.java b/tests/cts/src/android/healthconnect/cts/BodyTemperatureRecordTest.java
index 6ef5212..5089e97 100644
--- a/tests/cts/src/android/healthconnect/cts/BodyTemperatureRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/BodyTemperatureRecordTest.java
@@ -35,6 +35,7 @@
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.units.Temperature;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
diff --git a/tests/cts/src/android/healthconnect/cts/BodyWaterMassRecordTest.java b/tests/cts/src/android/healthconnect/cts/BodyWaterMassRecordTest.java
index 60d02d8..12d3a6e 100644
--- a/tests/cts/src/android/healthconnect/cts/BodyWaterMassRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/BodyWaterMassRecordTest.java
@@ -35,6 +35,7 @@
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.units.Mass;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
diff --git a/tests/cts/src/android/healthconnect/cts/BoneMassRecordTest.java b/tests/cts/src/android/healthconnect/cts/BoneMassRecordTest.java
index 6fe76b1..50d9e47 100644
--- a/tests/cts/src/android/healthconnect/cts/BoneMassRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/BoneMassRecordTest.java
@@ -35,6 +35,7 @@
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.units.Mass;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
diff --git a/tests/cts/src/android/healthconnect/cts/CervicalMucusRecordTest.java b/tests/cts/src/android/healthconnect/cts/CervicalMucusRecordTest.java
index bdcd933..408a6e8 100644
--- a/tests/cts/src/android/healthconnect/cts/CervicalMucusRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/CervicalMucusRecordTest.java
@@ -34,6 +34,7 @@
 import android.health.connect.datatypes.Device;
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
diff --git a/tests/cts/src/android/healthconnect/cts/CyclingPedalingCadenceRecordTest.java b/tests/cts/src/android/healthconnect/cts/CyclingPedalingCadenceRecordTest.java
index 0b6ba2d..1bf7d37 100644
--- a/tests/cts/src/android/healthconnect/cts/CyclingPedalingCadenceRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/CyclingPedalingCadenceRecordTest.java
@@ -41,6 +41,7 @@
 import android.health.connect.datatypes.Device;
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
diff --git a/tests/cts/src/android/healthconnect/cts/DataMigrationTest.java b/tests/cts/src/android/healthconnect/cts/DataMigrationTest.java
index cac8620..2bdb49e 100644
--- a/tests/cts/src/android/healthconnect/cts/DataMigrationTest.java
+++ b/tests/cts/src/android/healthconnect/cts/DataMigrationTest.java
@@ -25,7 +25,6 @@
 import static android.health.connect.HealthPermissions.WRITE_HEIGHT;
 import static android.health.connect.datatypes.units.Length.fromMeters;
 import static android.health.connect.datatypes.units.Power.fromWatts;
-import static android.healthconnect.cts.TestUtils.runShellCommand;
 
 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
 
@@ -76,6 +75,7 @@
 import android.health.connect.migration.PermissionMigrationPayload;
 import android.health.connect.migration.PriorityMigrationPayload;
 import android.health.connect.migration.RecordMigrationPayload;
+import android.healthconnect.cts.utils.TestUtils;
 import android.os.Build;
 import android.os.OutcomeReceiver;
 import android.os.UserHandle;
@@ -900,7 +900,7 @@
                         + ".MigrationStateChangeJob@android/com\\.android\\.server\\"
                         + ".healthconnect\\.HealthConnectDailyService";
 
-        String commandOutput = runShellCommand("dumpsys jobscheduler");
+        String commandOutput = TestUtils.runShellCommand("dumpsys jobscheduler");
 
         Pattern regexPattern = Pattern.compile(scheduledStateChangeJobPattern);
         Matcher matcher = regexPattern.matcher(commandOutput);
@@ -921,7 +921,7 @@
 
     private String getHealthFitnessDeviceConfig(String key, String defaultValue)
             throws IOException {
-        String value = runShellCommand("device_config get health_fitness " + key);
+        String value = TestUtils.runShellCommand("device_config get health_fitness " + key);
         return value.isBlank() ? defaultValue : value.strip();
     }
 
@@ -958,7 +958,7 @@
     }
 
     private void setHealthFitnessDeviceConfig(String key, String value) throws IOException {
-        runShellCommand("device_config put health_fitness " + key + " " + value);
+        TestUtils.runShellCommand("device_config put health_fitness " + key + " " + value);
     }
 
     private void assertStateChangeJobExists() throws IOException {
@@ -1054,7 +1054,11 @@
                         .setTimeRangeFilter(
                                 new TimeInstantRangeFilter.Builder()
                                         .setStartTime(mStartTime)
-                                        .setEndTime(mEndTime)
+                                        // Some tests in this file are using mEndTime as `time` for
+                                        // instant data points, and HC reads filters data points by
+                                        // [inclusive, exclusive), hence we need to add a positive
+                                        // amount of ms.
+                                        .setEndTime(mEndTime.plusMillis(1))
                                         .build())
                         .build(),
                 mOutcomeExecutor,
diff --git a/tests/cts/src/android/healthconnect/cts/DistanceRecordTest.java b/tests/cts/src/android/healthconnect/cts/DistanceRecordTest.java
index d40325f..20ec960 100644
--- a/tests/cts/src/android/healthconnect/cts/DistanceRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/DistanceRecordTest.java
@@ -39,6 +39,7 @@
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.units.Length;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
diff --git a/tests/cts/src/android/healthconnect/cts/ElevationGainedRecordTest.java b/tests/cts/src/android/healthconnect/cts/ElevationGainedRecordTest.java
index f5ac2fc..20cc655 100644
--- a/tests/cts/src/android/healthconnect/cts/ElevationGainedRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/ElevationGainedRecordTest.java
@@ -39,6 +39,7 @@
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.units.Length;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
diff --git a/tests/cts/src/android/healthconnect/cts/ExerciseDurationAggregationTest.java b/tests/cts/src/android/healthconnect/cts/ExerciseDurationAggregationTest.java
index c424fab..6fb2d90 100644
--- a/tests/cts/src/android/healthconnect/cts/ExerciseDurationAggregationTest.java
+++ b/tests/cts/src/android/healthconnect/cts/ExerciseDurationAggregationTest.java
@@ -17,8 +17,8 @@
 package android.healthconnect.cts;
 
 import static android.health.connect.datatypes.ExerciseSessionRecord.EXERCISE_DURATION_TOTAL;
-import static android.healthconnect.cts.TestUtils.SESSION_END_TIME;
-import static android.healthconnect.cts.TestUtils.SESSION_START_TIME;
+import static android.healthconnect.cts.utils.TestUtils.SESSION_END_TIME;
+import static android.healthconnect.cts.utils.TestUtils.SESSION_START_TIME;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -31,6 +31,7 @@
 import android.health.connect.datatypes.ExerciseSegmentType;
 import android.health.connect.datatypes.ExerciseSessionRecord;
 import android.health.connect.datatypes.ExerciseSessionType;
+import android.healthconnect.cts.utils.TestUtils;
 
 import org.junit.After;
 import org.junit.Test;
diff --git a/tests/cts/src/android/healthconnect/cts/ExerciseLapTest.java b/tests/cts/src/android/healthconnect/cts/ExerciseLapTest.java
index a3854e3..819bf64 100644
--- a/tests/cts/src/android/healthconnect/cts/ExerciseLapTest.java
+++ b/tests/cts/src/android/healthconnect/cts/ExerciseLapTest.java
@@ -16,8 +16,8 @@
 
 package android.healthconnect.cts;
 
-import static android.healthconnect.cts.TestUtils.SESSION_END_TIME;
-import static android.healthconnect.cts.TestUtils.SESSION_START_TIME;
+import static android.healthconnect.cts.utils.TestUtils.SESSION_END_TIME;
+import static android.healthconnect.cts.utils.TestUtils.SESSION_START_TIME;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -25,6 +25,7 @@
 import android.health.connect.datatypes.ExerciseSessionRecord;
 import android.health.connect.datatypes.ExerciseSessionType;
 import android.health.connect.datatypes.units.Length;
+import android.healthconnect.cts.utils.TestUtils;
 
 import org.junit.Test;
 
diff --git a/tests/cts/src/android/healthconnect/cts/ExerciseRouteDisabledFeatureTest.java b/tests/cts/src/android/healthconnect/cts/ExerciseRouteDisabledFeatureTest.java
index db617cb..2c9045a 100644
--- a/tests/cts/src/android/healthconnect/cts/ExerciseRouteDisabledFeatureTest.java
+++ b/tests/cts/src/android/healthconnect/cts/ExerciseRouteDisabledFeatureTest.java
@@ -23,6 +23,7 @@
 import android.health.connect.ReadRecordsRequestUsingIds;
 import android.health.connect.datatypes.ExerciseSessionRecord;
 import android.health.connect.datatypes.Record;
+import android.healthconnect.cts.utils.TestUtils;
 import android.provider.DeviceConfig;
 
 import androidx.test.platform.app.InstrumentationRegistry;
diff --git a/tests/cts/src/android/healthconnect/cts/ExerciseRouteTest.java b/tests/cts/src/android/healthconnect/cts/ExerciseRouteTest.java
index cc2f0ea..cc07aee 100644
--- a/tests/cts/src/android/healthconnect/cts/ExerciseRouteTest.java
+++ b/tests/cts/src/android/healthconnect/cts/ExerciseRouteTest.java
@@ -22,6 +22,7 @@
 
 import android.health.connect.datatypes.ExerciseRoute;
 import android.health.connect.datatypes.units.Length;
+import android.healthconnect.cts.utils.TestUtils;
 import android.os.Parcel;
 
 import androidx.test.runner.AndroidJUnit4;
diff --git a/tests/cts/src/android/healthconnect/cts/ExerciseSegmentTest.java b/tests/cts/src/android/healthconnect/cts/ExerciseSegmentTest.java
index 339eb98..9536255 100644
--- a/tests/cts/src/android/healthconnect/cts/ExerciseSegmentTest.java
+++ b/tests/cts/src/android/healthconnect/cts/ExerciseSegmentTest.java
@@ -16,8 +16,8 @@
 
 package android.healthconnect.cts;
 
-import static android.healthconnect.cts.TestUtils.SESSION_END_TIME;
-import static android.healthconnect.cts.TestUtils.SESSION_START_TIME;
+import static android.healthconnect.cts.utils.TestUtils.SESSION_END_TIME;
+import static android.healthconnect.cts.utils.TestUtils.SESSION_START_TIME;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -25,6 +25,7 @@
 import android.health.connect.datatypes.ExerciseSegmentType;
 import android.health.connect.datatypes.ExerciseSessionRecord;
 import android.health.connect.datatypes.ExerciseSessionType;
+import android.healthconnect.cts.utils.TestUtils;
 
 import org.junit.Test;
 
diff --git a/tests/cts/src/android/healthconnect/cts/ExerciseSessionRecordTest.java b/tests/cts/src/android/healthconnect/cts/ExerciseSessionRecordTest.java
index 9d729bc..0a70837 100644
--- a/tests/cts/src/android/healthconnect/cts/ExerciseSessionRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/ExerciseSessionRecordTest.java
@@ -16,10 +16,10 @@
 
 package android.healthconnect.cts;
 
-import static android.healthconnect.cts.TestUtils.SESSION_END_TIME;
-import static android.healthconnect.cts.TestUtils.SESSION_START_TIME;
-import static android.healthconnect.cts.TestUtils.buildExerciseSession;
-import static android.healthconnect.cts.TestUtils.buildLocationTimePoint;
+import static android.healthconnect.cts.utils.TestUtils.SESSION_END_TIME;
+import static android.healthconnect.cts.utils.TestUtils.SESSION_START_TIME;
+import static android.healthconnect.cts.utils.TestUtils.buildExerciseSession;
+import static android.healthconnect.cts.utils.TestUtils.buildLocationTimePoint;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -43,6 +43,7 @@
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.units.Length;
+import android.healthconnect.cts.utils.TestUtils;
 
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.runner.AndroidJUnit4;
diff --git a/tests/cts/src/android/healthconnect/cts/FloorsClimbedRecordTest.java b/tests/cts/src/android/healthconnect/cts/FloorsClimbedRecordTest.java
index d1bf705..929ba4a 100644
--- a/tests/cts/src/android/healthconnect/cts/FloorsClimbedRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/FloorsClimbedRecordTest.java
@@ -37,6 +37,7 @@
 import android.health.connect.datatypes.FloorsClimbedRecord;
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
+import android.healthconnect.cts.utils.TestUtils;
 
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.runner.AndroidJUnit4;
diff --git a/tests/cts/src/android/healthconnect/cts/GetActivityDatesTest.java b/tests/cts/src/android/healthconnect/cts/GetActivityDatesTest.java
index b5d1117..ff009ba 100644
--- a/tests/cts/src/android/healthconnect/cts/GetActivityDatesTest.java
+++ b/tests/cts/src/android/healthconnect/cts/GetActivityDatesTest.java
@@ -28,6 +28,7 @@
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.StepsRecord;
+import android.healthconnect.cts.utils.TestUtils;
 
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.runner.AndroidJUnit4;
diff --git a/tests/cts/src/android/healthconnect/cts/GetApplicationInfoTest.java b/tests/cts/src/android/healthconnect/cts/GetApplicationInfoTest.java
index 506f57a..4e5a497 100644
--- a/tests/cts/src/android/healthconnect/cts/GetApplicationInfoTest.java
+++ b/tests/cts/src/android/healthconnect/cts/GetApplicationInfoTest.java
@@ -16,7 +16,7 @@
 
 package android.healthconnect.cts;
 
-import static android.healthconnect.cts.TestUtils.MANAGE_HEALTH_DATA;
+import static android.healthconnect.cts.utils.TestUtils.MANAGE_HEALTH_DATA;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -26,6 +26,7 @@
 import android.health.connect.HealthConnectException;
 import android.health.connect.HealthConnectManager;
 import android.health.connect.datatypes.AppInfo;
+import android.healthconnect.cts.utils.TestUtils;
 import android.os.OutcomeReceiver;
 import android.platform.test.annotations.AppModeFull;
 import android.util.Log;
diff --git a/tests/cts/src/android/healthconnect/cts/HealthConnectAccessLogsTest.java b/tests/cts/src/android/healthconnect/cts/HealthConnectAccessLogsTest.java
index 28ad0e6..d69d942 100644
--- a/tests/cts/src/android/healthconnect/cts/HealthConnectAccessLogsTest.java
+++ b/tests/cts/src/android/healthconnect/cts/HealthConnectAccessLogsTest.java
@@ -28,6 +28,7 @@
 import android.health.connect.datatypes.HeartRateRecord;
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.StepsRecord;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
diff --git a/tests/cts/src/android/healthconnect/cts/HealthConnectChangeLogsTests.java b/tests/cts/src/android/healthconnect/cts/HealthConnectChangeLogsTests.java
index fc053ec..33a3187 100644
--- a/tests/cts/src/android/healthconnect/cts/HealthConnectChangeLogsTests.java
+++ b/tests/cts/src/android/healthconnect/cts/HealthConnectChangeLogsTests.java
@@ -27,6 +27,7 @@
 import android.health.connect.datatypes.DataOrigin;
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.StepsRecord;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
diff --git a/tests/cts/src/android/healthconnect/cts/HealthConnectManagerTest.java b/tests/cts/src/android/healthconnect/cts/HealthConnectManagerTest.java
index 5f917a7..8953275 100644
--- a/tests/cts/src/android/healthconnect/cts/HealthConnectManagerTest.java
+++ b/tests/cts/src/android/healthconnect/cts/HealthConnectManagerTest.java
@@ -30,13 +30,16 @@
 import static android.health.connect.datatypes.RecordTypeIdentifier.RECORD_TYPE_HEART_RATE;
 import static android.health.connect.datatypes.RecordTypeIdentifier.RECORD_TYPE_STEPS;
 import static android.health.connect.datatypes.StepsRecord.STEPS_COUNT_TOTAL;
-import static android.healthconnect.cts.TestUtils.MANAGE_HEALTH_DATA;
-import static android.healthconnect.cts.TestUtils.getRecordById;
+import static android.healthconnect.cts.utils.TestUtils.MANAGE_HEALTH_DATA;
+import static android.healthconnect.cts.utils.TestUtils.getRecordById;
+import static android.healthconnect.cts.utils.TestUtils.insertRecords;
 
 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertThrows;
+
 import android.Manifest;
 import android.app.UiAutomation;
 import android.content.Context;
@@ -47,7 +50,10 @@
 import android.health.connect.HealthConnectManager;
 import android.health.connect.HealthPermissions;
 import android.health.connect.LocalTimeRangeFilter;
+import android.health.connect.ReadRecordsRequest;
+import android.health.connect.ReadRecordsRequestUsingFilters;
 import android.health.connect.ReadRecordsRequestUsingIds;
+import android.health.connect.ReadRecordsResponse;
 import android.health.connect.RecordTypeInfoResponse;
 import android.health.connect.TimeInstantRangeFilter;
 import android.health.connect.changelog.ChangeLogTokenRequest;
@@ -66,6 +72,7 @@
 import android.health.connect.datatypes.units.Power;
 import android.health.connect.datatypes.units.Volume;
 import android.health.connect.restore.StageRemoteDataException;
+import android.healthconnect.cts.utils.TestUtils;
 import android.os.OutcomeReceiver;
 import android.os.ParcelFileDescriptor;
 import android.platform.test.annotations.AppModeFull;
@@ -160,7 +167,7 @@
     }
 
     @Test
-    public void testIsHealthPermission_forHealthPermission_returnsTrue() throws Exception {
+    public void testIsHealthPermission_forHealthPermission_returnsTrue() {
         Context context = ApplicationProvider.getApplicationContext();
         assertThat(isHealthPermission(context, HealthPermissions.READ_ACTIVE_CALORIES_BURNED))
                 .isTrue();
@@ -169,7 +176,7 @@
     }
 
     @Test
-    public void testIsHealthPermission_forNonHealthGroupPermission_returnsFalse() throws Exception {
+    public void testIsHealthPermission_forNonHealthGroupPermission_returnsFalse() {
         Context context = ApplicationProvider.getApplicationContext();
         assertThat(isHealthPermission(context, HealthPermissions.MANAGE_HEALTH_PERMISSIONS))
                 .isFalse();
@@ -192,12 +199,7 @@
      * <p>Insert a sample record of each dataType, update them and check by reading them.
      */
     @Test
-    public void testUpdateRecords_validInput_dataBaseUpdatedSuccessfully()
-            throws InterruptedException,
-                    InvocationTargetException,
-                    InstantiationException,
-                    IllegalAccessException,
-                    NoSuchMethodException {
+    public void testUpdateRecords_validInput_dataBaseUpdatedSuccessfully() throws Exception {
 
         Context context = ApplicationProvider.getApplicationContext();
         CountDownLatch latch = new CountDownLatch(1);
@@ -262,12 +264,7 @@
      * valid inputs) should not be modified either.
      */
     @Test
-    public void testUpdateRecords_invalidInputRecords_noChangeInDataBase()
-            throws InterruptedException,
-                    InvocationTargetException,
-                    InstantiationException,
-                    IllegalAccessException,
-                    NoSuchMethodException {
+    public void testUpdateRecords_invalidInputRecords_noChangeInDataBase() throws Exception {
 
         Context context = ApplicationProvider.getApplicationContext();
         CountDownLatch latch = new CountDownLatch(1);
@@ -338,11 +335,7 @@
      */
     @Test
     public void testUpdateRecords_recordWithInvalidPackageName_noChangeInDataBase()
-            throws InterruptedException,
-                    InvocationTargetException,
-                    InstantiationException,
-                    IllegalAccessException,
-                    NoSuchMethodException {
+            throws Exception {
 
         Context context = ApplicationProvider.getApplicationContext();
         CountDownLatch latch = new CountDownLatch(1);
@@ -632,6 +625,93 @@
     }
 
     @Test
+    public void testReadRecords_readByIdMaxPageSizeExceeded_throws() {
+        int maxPageSize = 5000;
+        ReadRecordsRequestUsingIds.Builder<StepsRecord> request =
+                new ReadRecordsRequestUsingIds.Builder<>(StepsRecord.class);
+        for (int i = 0; i < maxPageSize; i++) {
+            request.addClientRecordId("client.id" + i);
+        }
+        Throwable thrown =
+                assertThrows(IllegalArgumentException.class, () -> request.addId("extra_id"));
+        assertThat(thrown.getMessage()).contains("Maximum allowed pageSize is 5000");
+        thrown =
+                assertThrows(
+                        IllegalArgumentException.class,
+                        () -> request.addClientRecordId("extra_client_id"));
+        assertThat(thrown.getMessage()).contains("Maximum allowed pageSize is 5000");
+    }
+
+    @Test
+    public void testReadRecords_readByFilterMaxPageSizeExceeded_throws() {
+        int maxPageSize = 5000;
+        ReadRecordsRequestUsingFilters.Builder<StepsRecord> request =
+                new ReadRecordsRequestUsingFilters.Builder<>(StepsRecord.class);
+        Throwable thrown =
+                assertThrows(
+                        IllegalArgumentException.class, () -> request.setPageSize(maxPageSize + 1));
+        assertThat(thrown.getMessage()).contains("Maximum allowed pageSize is 5000");
+    }
+
+    @Test
+    public void testReadRecords_multiplePagesSameStartTimeRecords_paginatedCorrectly()
+            throws Exception {
+        Instant startTime = Instant.now().minus(1, ChronoUnit.DAYS);
+
+        insertRecords(
+                List.of(
+                        getStepsRecord(
+                                "client.id1",
+                                "package.name",
+                                /* count= */ 100,
+                                startTime,
+                                startTime.plusSeconds(500)),
+                        getStepsRecord(
+                                "client.id2",
+                                "package.name",
+                                /* count= */ 100,
+                                startTime,
+                                startTime.plusSeconds(200)),
+                        getStepsRecord(
+                                "client.id3",
+                                "package.name",
+                                /* count= */ 100,
+                                startTime,
+                                startTime.plusSeconds(400)),
+                        getStepsRecord(
+                                "client.id4",
+                                "package.name",
+                                /* count= */ 100,
+                                startTime,
+                                startTime.plusSeconds(300))));
+
+        ReadRecordsRequest<StepsRecord> request1 =
+                new ReadRecordsRequestUsingFilters.Builder<>(StepsRecord.class)
+                        .setPageSize(2)
+                        .setAscending(false)
+                        .build();
+        ReadRecordsResponse<StepsRecord> result1 = TestUtils.readRecordsWithPagination(request1);
+        assertThat(result1.getRecords()).hasSize(2);
+        assertThat(result1.getRecords().get(0).getMetadata().getClientRecordId())
+                .isEqualTo("client.id1");
+        assertThat(result1.getRecords().get(1).getMetadata().getClientRecordId())
+                .isEqualTo("client.id2");
+
+        ReadRecordsRequest<StepsRecord> request2 =
+                new ReadRecordsRequestUsingFilters.Builder<>(StepsRecord.class)
+                        .setPageSize(2)
+                        .setPageToken(result1.getNextPageToken())
+                        .build();
+        ReadRecordsResponse<StepsRecord> result2 = TestUtils.readRecordsWithPagination(request2);
+        assertThat(result2.getRecords()).hasSize(2);
+        assertThat(result2.getRecords().get(0).getMetadata().getClientRecordId())
+                .isEqualTo("client.id3");
+        assertThat(result2.getRecords().get(1).getMetadata().getClientRecordId())
+                .isEqualTo("client.id4");
+        assertThat(result2.getNextPageToken()).isEqualTo(-1);
+    }
+
+    @Test
     public void testAutoDeleteApis() throws InterruptedException {
         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
 
@@ -1439,7 +1519,8 @@
                         @Override
                         public void onError(@NonNull HealthConnectException e) {
                             returnedException.set(e);
-                            latch.countDown();}
+                            latch.countDown();
+                        }
                     });
         } catch (Exception e) {
             throw new RuntimeException(e);
diff --git a/tests/cts/src/android/healthconnect/cts/HealthPermissionCategoryPriorityTests.java b/tests/cts/src/android/healthconnect/cts/HealthPermissionCategoryPriorityTests.java
index a24bad9..f776689 100644
--- a/tests/cts/src/android/healthconnect/cts/HealthPermissionCategoryPriorityTests.java
+++ b/tests/cts/src/android/healthconnect/cts/HealthPermissionCategoryPriorityTests.java
@@ -22,7 +22,7 @@
 import static android.health.connect.HealthDataCategory.NUTRITION;
 import static android.health.connect.HealthDataCategory.SLEEP;
 import static android.health.connect.HealthDataCategory.VITALS;
-import static android.healthconnect.cts.TestUtils.MANAGE_HEALTH_DATA;
+import static android.healthconnect.cts.utils.TestUtils.MANAGE_HEALTH_DATA;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -33,6 +33,7 @@
 import android.health.connect.HealthConnectManager;
 import android.health.connect.UpdateDataOriginPriorityOrderRequest;
 import android.health.connect.datatypes.DataOrigin;
+import android.healthconnect.cts.utils.TestUtils;
 import android.os.OutcomeReceiver;
 
 import androidx.test.InstrumentationRegistry;
diff --git a/tests/cts/src/android/healthconnect/cts/HealthPermissionsPresenceTest.java b/tests/cts/src/android/healthconnect/cts/HealthPermissionsPresenceTest.java
index 194de47..3bcab0a 100644
--- a/tests/cts/src/android/healthconnect/cts/HealthPermissionsPresenceTest.java
+++ b/tests/cts/src/android/healthconnect/cts/HealthPermissionsPresenceTest.java
@@ -113,6 +113,8 @@
 public class HealthPermissionsPresenceTest {
     private static final Set<String> HEALTH_PERMISSIONS =
             Set.of(
+                    // TODO(b/299897306): Replace with a constant when it is exposed
+                    "android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND",
                     READ_ACTIVE_CALORIES_BURNED,
                     READ_DISTANCE,
                     READ_ELEVATION_GAINED,
diff --git a/tests/cts/src/android/healthconnect/cts/HeartRateRecordTest.java b/tests/cts/src/android/healthconnect/cts/HeartRateRecordTest.java
index 7724c27..dd72ac8 100644
--- a/tests/cts/src/android/healthconnect/cts/HeartRateRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/HeartRateRecordTest.java
@@ -20,6 +20,7 @@
 import static android.health.connect.datatypes.HeartRateRecord.BPM_MAX;
 import static android.health.connect.datatypes.HeartRateRecord.BPM_MIN;
 import static android.health.connect.datatypes.HeartRateRecord.HEART_MEASUREMENTS_COUNT;
+import static android.healthconnect.cts.utils.TestUtils.readRecordsWithPagination;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -32,6 +33,7 @@
 import android.health.connect.LocalTimeRangeFilter;
 import android.health.connect.ReadRecordsRequestUsingFilters;
 import android.health.connect.ReadRecordsRequestUsingIds;
+import android.health.connect.ReadRecordsResponse;
 import android.health.connect.RecordIdFilter;
 import android.health.connect.TimeInstantRangeFilter;
 import android.health.connect.changelog.ChangeLogTokenRequest;
@@ -43,8 +45,8 @@
 import android.health.connect.datatypes.HeartRateRecord;
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
-import android.util.Pair;
 
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.runner.AndroidJUnit4;
@@ -216,12 +218,12 @@
                         TestUtils.getHeartRateRecord(72, Instant.now().minus(1, ChronoUnit.DAYS)),
                         TestUtils.getHeartRateRecord(72, Instant.now().minus(2, ChronoUnit.DAYS)));
         TestUtils.insertRecords(recordList);
-        Pair<List<HeartRateRecord>, Long> newHeartRecords =
-                TestUtils.readRecordsWithPagination(
+        ReadRecordsResponse<HeartRateRecord> newHeartRecords =
+                readRecordsWithPagination(
                         new ReadRecordsRequestUsingFilters.Builder<>(HeartRateRecord.class)
                                 .setPageSize(1)
                                 .build());
-        assertThat(newHeartRecords.first.size()).isEqualTo(1);
+        assertThat(newHeartRecords.getRecords()).hasSize(1);
     }
 
     @Test
@@ -233,21 +235,22 @@
                         TestUtils.getHeartRateRecord(72, Instant.now().minusMillis(3000)),
                         TestUtils.getHeartRateRecord(72, Instant.now().minusMillis(4000)));
         TestUtils.insertRecords(recordList);
-        Pair<List<HeartRateRecord>, Long> oldHeartRecords =
-                TestUtils.readRecordsWithPagination(
+        ReadRecordsResponse<HeartRateRecord> oldHeartRecords =
+                readRecordsWithPagination(
                         new ReadRecordsRequestUsingFilters.Builder<>(HeartRateRecord.class)
                                 .setPageSize(1)
                                 .setAscending(true)
                                 .build());
-        assertThat(oldHeartRecords.first.size()).isEqualTo(1);
-        Pair<List<HeartRateRecord>, Long> newHeartRecords =
-                TestUtils.readRecordsWithPagination(
+        assertThat(oldHeartRecords.getRecords()).hasSize(1);
+        ReadRecordsResponse<HeartRateRecord> newHeartRecords =
+                readRecordsWithPagination(
                         new ReadRecordsRequestUsingFilters.Builder<>(HeartRateRecord.class)
                                 .setPageSize(2)
-                                .setPageToken(oldHeartRecords.second)
+                                .setPageToken(oldHeartRecords.getNextPageToken())
                                 .build());
-        assertThat(newHeartRecords.first.size()).isEqualTo(2);
-        assertThat(newHeartRecords.second).isNotEqualTo(oldHeartRecords.second);
+        assertThat(newHeartRecords.getRecords()).hasSize(2);
+        assertThat(newHeartRecords.getNextPageToken())
+                .isNotEqualTo(oldHeartRecords.getNextPageToken());
     }
 
     @Test
@@ -255,23 +258,20 @@
         List<Record> recordList =
                 Arrays.asList(TestUtils.getHeartRateRecord(), TestUtils.getHeartRateRecord());
         TestUtils.insertRecords(recordList);
-        Pair<List<HeartRateRecord>, Long> oldHeartRecords =
-                TestUtils.readRecordsWithPagination(
+        ReadRecordsResponse<HeartRateRecord> oldHeartRecords =
+                readRecordsWithPagination(
                         new ReadRecordsRequestUsingFilters.Builder<>(HeartRateRecord.class)
                                 .build());
-        Pair<List<HeartRateRecord>, Long> newHeartRecords;
-        while (oldHeartRecords.second != -1) {
+        ReadRecordsResponse<HeartRateRecord> newHeartRecords;
+        while (oldHeartRecords.getNextPageToken() != -1) {
             newHeartRecords =
-                    TestUtils.readRecordsWithPagination(
+                    readRecordsWithPagination(
                             new ReadRecordsRequestUsingFilters.Builder<>(HeartRateRecord.class)
-                                    .setPageToken(oldHeartRecords.second)
+                                    .setPageToken(oldHeartRecords.getNextPageToken())
                                     .build());
-            if (newHeartRecords.second != -1) {
-                assertThat(newHeartRecords.second).isGreaterThan(oldHeartRecords.second);
-            }
             oldHeartRecords = newHeartRecords;
         }
-        assertThat(oldHeartRecords.second).isEqualTo(-1);
+        assertThat(oldHeartRecords.getNextPageToken()).isEqualTo(-1);
     }
 
     @Test
diff --git a/tests/cts/src/android/healthconnect/cts/HeartRateVariabilityRmssdRecordTest.java b/tests/cts/src/android/healthconnect/cts/HeartRateVariabilityRmssdRecordTest.java
index 60361c6..72a0fbb 100644
--- a/tests/cts/src/android/healthconnect/cts/HeartRateVariabilityRmssdRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/HeartRateVariabilityRmssdRecordTest.java
@@ -34,6 +34,7 @@
 import android.health.connect.datatypes.HeartRateVariabilityRmssdRecord;
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
diff --git a/tests/cts/src/android/healthconnect/cts/HeightRecordTest.java b/tests/cts/src/android/healthconnect/cts/HeightRecordTest.java
index ddf5a81..fa2356a 100644
--- a/tests/cts/src/android/healthconnect/cts/HeightRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/HeightRecordTest.java
@@ -41,6 +41,7 @@
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.units.Length;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
diff --git a/tests/cts/src/android/healthconnect/cts/HistoricAccessLimitTest.java b/tests/cts/src/android/healthconnect/cts/HistoricAccessLimitTest.java
new file mode 100644
index 0000000..0aa0f7a
--- /dev/null
+++ b/tests/cts/src/android/healthconnect/cts/HistoricAccessLimitTest.java
@@ -0,0 +1,270 @@
+package android.healthconnect.cts;
+
+import static android.health.connect.datatypes.StepsRecord.STEPS_COUNT_TOTAL;
+import static android.health.connect.datatypes.WeightRecord.WEIGHT_AVG;
+import static android.healthconnect.cts.utils.TestUtils.deleteAllStagedRemoteData;
+import static android.healthconnect.cts.utils.TestUtils.sendCommandToTestAppReceiver;
+import static android.healthconnect.test.app.TestAppReceiver.ACTION_INSERT_STEPS_RECORDS;
+import static android.healthconnect.test.app.TestAppReceiver.ACTION_INSERT_WEIGHT_RECORDS;
+import static android.healthconnect.test.app.TestAppReceiver.EXTRA_END_TIMES;
+import static android.healthconnect.test.app.TestAppReceiver.EXTRA_RECORD_IDS;
+import static android.healthconnect.test.app.TestAppReceiver.EXTRA_RECORD_VALUES;
+import static android.healthconnect.test.app.TestAppReceiver.EXTRA_TIMES;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.time.temporal.ChronoUnit.DAYS;
+
+import android.content.Context;
+import android.health.connect.AggregateRecordsRequest;
+import android.health.connect.AggregateRecordsResponse;
+import android.health.connect.ReadRecordsRequestUsingFilters;
+import android.health.connect.ReadRecordsRequestUsingIds;
+import android.health.connect.TimeInstantRangeFilter;
+import android.health.connect.datatypes.Metadata;
+import android.health.connect.datatypes.Record;
+import android.health.connect.datatypes.StepsRecord;
+import android.health.connect.datatypes.WeightRecord;
+import android.health.connect.datatypes.units.Mass;
+import android.healthconnect.cts.utils.TestReceiver;
+import android.healthconnect.cts.utils.TestUtils;
+import android.os.Bundle;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+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;
+
+@AppModeFull(reason = "HealthConnectManager is not accessible to instant apps")
+@RunWith(AndroidJUnit4.class)
+public class HistoricAccessLimitTest {
+    private Context mContext;
+    private Instant mNow;
+
+    @Before
+    public void setUp() throws InterruptedException {
+        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        mNow = Instant.now();
+        deleteAllStagedRemoteData();
+        TestReceiver.reset();
+    }
+
+    @Test
+    public void testReadIntervalRecordsByFilters_expectCorrectResponse()
+            throws InterruptedException {
+        String ownRecordId1 = insertStepsRecord(daysBeforeNow(10), daysBeforeNow(9), 10);
+        String ownRecordId2 = insertStepsRecord(daysBeforeNow(11), daysBeforeNow(10), 11);
+        String ownRecordId3 = insertStepsRecord(daysBeforeNow(50), daysBeforeNow(40), 12);
+        String otherAppsRecordIdAfterHistoricLimit =
+                insertStepsRecordViaTestApp(daysBeforeNow(2), daysBeforeNow(1), 13);
+        String otherAppsRecordIdBeforeHistoricLimit =
+                insertStepsRecordViaTestApp(daysBeforeNow(50), daysBeforeNow(40), 14);
+
+        List<String> stepsRecordsIdsReadByFilters =
+                getRecordIds(
+                        TestUtils.readRecords(
+                                new ReadRecordsRequestUsingFilters.Builder<>(StepsRecord.class)
+                                        .build()));
+
+        assertThat(stepsRecordsIdsReadByFilters)
+                .containsExactly(
+                        ownRecordId1,
+                        ownRecordId2,
+                        ownRecordId3,
+                        otherAppsRecordIdAfterHistoricLimit);
+        assertThat(stepsRecordsIdsReadByFilters)
+                .doesNotContain(otherAppsRecordIdBeforeHistoricLimit);
+    }
+
+    @Test
+    public void testReadInstantRecordsByFilters_expectCorrectResponse()
+            throws InterruptedException {
+        String ownRecordId1 = insertWeightRecord(daysBeforeNow(10), 10);
+        String ownRecordId2 = insertWeightRecord(daysBeforeNow(11), 11);
+        String ownRecordId3 = insertWeightRecord(daysBeforeNow(50), 12);
+        String otherAppsRecordIdAfterHistoricLimit =
+                insertWeightRecordViaTestApp(daysBeforeNow(2), 13);
+        String otherAppsRecordIdBeforeHistoricLimit =
+                insertWeightRecordViaTestApp(daysBeforeNow(50), 14);
+
+        List<String> weightRecordsIdsReadByFilters =
+                getRecordIds(
+                        TestUtils.readRecords(
+                                new ReadRecordsRequestUsingFilters.Builder<>(WeightRecord.class)
+                                        .build()));
+
+        assertThat(weightRecordsIdsReadByFilters)
+                .containsExactly(
+                        ownRecordId1,
+                        ownRecordId2,
+                        ownRecordId3,
+                        otherAppsRecordIdAfterHistoricLimit);
+        assertThat(weightRecordsIdsReadByFilters)
+                .doesNotContain(otherAppsRecordIdBeforeHistoricLimit);
+    }
+
+    @Test
+    public void testReadIntervalRecordsByIds_expectCorrectResponse() throws InterruptedException {
+        String otherAppsRecordIdBeforeHistoricLimit =
+                insertStepsRecordViaTestApp(daysBeforeNow(50), daysBeforeNow(40), 14);
+        List<String> insertedRecordIds =
+                List.of(
+                        insertStepsRecord(daysBeforeNow(10), daysBeforeNow(9), 10),
+                        insertStepsRecord(daysBeforeNow(11), daysBeforeNow(10), 11),
+                        insertStepsRecord(daysBeforeNow(50), daysBeforeNow(40), 12),
+                        insertStepsRecordViaTestApp(daysBeforeNow(2), daysBeforeNow(1), 13),
+                        otherAppsRecordIdBeforeHistoricLimit);
+
+        ReadRecordsRequestUsingIds.Builder<StepsRecord> readUsingIdsRequest =
+                new ReadRecordsRequestUsingIds.Builder<>(StepsRecord.class);
+        insertedRecordIds.stream().forEach(readUsingIdsRequest::addId);
+        List<String> recordIdsReadByIds =
+                getRecordIds(TestUtils.readRecords(readUsingIdsRequest.build()));
+
+        List<String> insertedRecordIdsWithoutOtherAppsRecordBeforeHistoricLimit =
+                new ArrayList<>(insertedRecordIds);
+        insertedRecordIdsWithoutOtherAppsRecordBeforeHistoricLimit.remove(
+                otherAppsRecordIdBeforeHistoricLimit);
+        assertThat(recordIdsReadByIds)
+                .containsExactlyElementsIn(
+                        insertedRecordIdsWithoutOtherAppsRecordBeforeHistoricLimit);
+    }
+
+    @Test
+    public void testReadInstantRecordsByIds_expectCorrectResponse() throws InterruptedException {
+        String otherAppsRecordIdBeforeHistoricLimit =
+                insertWeightRecordViaTestApp(daysBeforeNow(50), 14);
+        List<String> insertedRecordIds =
+                List.of(
+                        insertWeightRecord(daysBeforeNow(10), 10),
+                        insertWeightRecord(daysBeforeNow(11), 11),
+                        insertWeightRecord(daysBeforeNow(50), 12),
+                        insertWeightRecordViaTestApp(daysBeforeNow(2), 13),
+                        otherAppsRecordIdBeforeHistoricLimit);
+
+        ReadRecordsRequestUsingIds.Builder<WeightRecord> readUsingIdsRequest =
+                new ReadRecordsRequestUsingIds.Builder<>(WeightRecord.class);
+        insertedRecordIds.stream().forEach(readUsingIdsRequest::addId);
+        List<String> recordIdsReadByIds =
+                getRecordIds(TestUtils.readRecords(readUsingIdsRequest.build()));
+
+        List<String> insertedRecordIdsWithoutOtherAppsRecordBeforeHistoricLimit =
+                new ArrayList<>(insertedRecordIds);
+        insertedRecordIdsWithoutOtherAppsRecordBeforeHistoricLimit.remove(
+                otherAppsRecordIdBeforeHistoricLimit);
+        assertThat(recordIdsReadByIds)
+                .containsExactlyElementsIn(
+                        insertedRecordIdsWithoutOtherAppsRecordBeforeHistoricLimit);
+    }
+
+    @Test
+    public void testAggregateIntervalRecords_expectCorrectResponse() throws InterruptedException {
+        long ownRecordValueAfterHistoricLimit = 20;
+        long ownRecordValueBeforeHistoricLimit = 300;
+        long otherAppsRecordValueAfterHistoricLimit = 4_000;
+        long otherAppsRecordValueBeforeHistoricLimit = 50_000;
+        insertStepsRecord(daysBeforeNow(10), daysBeforeNow(9), ownRecordValueAfterHistoricLimit);
+        insertStepsRecord(daysBeforeNow(50), daysBeforeNow(40), ownRecordValueBeforeHistoricLimit);
+        insertStepsRecordViaTestApp(
+                daysBeforeNow(2), daysBeforeNow(1), otherAppsRecordValueAfterHistoricLimit);
+        insertStepsRecordViaTestApp(
+                daysBeforeNow(50), daysBeforeNow(40), otherAppsRecordValueBeforeHistoricLimit);
+        TimeInstantRangeFilter timeFilter =
+                new TimeInstantRangeFilter.Builder()
+                        .setStartTime(daysBeforeNow(1000))
+                        .setEndTime(mNow.plus(1000, DAYS))
+                        .build();
+
+        AggregateRecordsResponse<Long> totalStepsCountAggregation =
+                TestUtils.getAggregateResponse(
+                        new AggregateRecordsRequest.Builder<Long>(timeFilter)
+                                .addAggregationType(STEPS_COUNT_TOTAL)
+                                .build());
+
+        assertThat(totalStepsCountAggregation.get(STEPS_COUNT_TOTAL))
+                .isEqualTo(
+                        ownRecordValueAfterHistoricLimit
+                                + ownRecordValueBeforeHistoricLimit
+                                + otherAppsRecordValueAfterHistoricLimit);
+    }
+
+    @Test
+    public void testAggregateInstantRecords_expectCorrectResponse() throws InterruptedException {
+        long ownRecordValueAfterHistoricLimit = 20;
+        long ownRecordValueBeforeHistoricLimit = 300;
+        long otherAppsRecordValueAfterHistoricLimit = 4_000;
+        long otherAppsRecordValueBeforeHistoricLimit = 50_000;
+        insertWeightRecord(daysBeforeNow(10), ownRecordValueAfterHistoricLimit);
+        insertWeightRecord(daysBeforeNow(50), ownRecordValueBeforeHistoricLimit);
+        insertWeightRecordViaTestApp(daysBeforeNow(2), otherAppsRecordValueAfterHistoricLimit);
+        insertWeightRecordViaTestApp(daysBeforeNow(50), otherAppsRecordValueBeforeHistoricLimit);
+        TimeInstantRangeFilter timeFilter =
+                new TimeInstantRangeFilter.Builder()
+                        .setStartTime(daysBeforeNow(1000))
+                        .setEndTime(mNow.plus(1000, DAYS))
+                        .build();
+
+        AggregateRecordsResponse<Mass> averageWeightAggregation =
+                TestUtils.getAggregateResponse(
+                        new AggregateRecordsRequest.Builder<Mass>(timeFilter)
+                                .addAggregationType(WEIGHT_AVG)
+                                .build());
+
+        assertThat(averageWeightAggregation.get(WEIGHT_AVG).getInGrams())
+                .isEqualTo(
+                        (ownRecordValueAfterHistoricLimit
+                                        + ownRecordValueBeforeHistoricLimit
+                                        + otherAppsRecordValueAfterHistoricLimit)
+                                / 3d);
+    }
+
+    private String insertStepsRecord(Instant startTime, Instant endTime, long value)
+            throws InterruptedException {
+        return TestUtils.insertRecordAndGetId(
+                new StepsRecord.Builder(new Metadata.Builder().build(), startTime, endTime, value)
+                        .build());
+    }
+
+    private String insertWeightRecord(Instant time, long value) throws InterruptedException {
+        return TestUtils.insertRecordAndGetId(
+                new WeightRecord.Builder(
+                                new Metadata.Builder().build(),
+                                time,
+                                Mass.fromGrams((double) value))
+                        .build());
+    }
+
+    private String insertStepsRecordViaTestApp(Instant startTime, Instant endTime, long value) {
+        Bundle bundle = new Bundle();
+        bundle.putLongArray(EXTRA_TIMES, new long[] {startTime.toEpochMilli()});
+        bundle.putLongArray(EXTRA_END_TIMES, new long[] {endTime.toEpochMilli()});
+        bundle.putLongArray(EXTRA_RECORD_VALUES, new long[] {value});
+        TestReceiver.reset();
+        sendCommandToTestAppReceiver(mContext, ACTION_INSERT_STEPS_RECORDS, bundle);
+        return TestReceiver.getResult().getStringArrayList(EXTRA_RECORD_IDS).get(0);
+    }
+
+    private String insertWeightRecordViaTestApp(Instant startTime, long value) {
+        Bundle bundle = new Bundle();
+        bundle.putLongArray(EXTRA_TIMES, new long[] {startTime.toEpochMilli()});
+        bundle.putLongArray(EXTRA_RECORD_VALUES, new long[] {value});
+        TestReceiver.reset();
+        sendCommandToTestAppReceiver(mContext, ACTION_INSERT_WEIGHT_RECORDS, bundle);
+        return TestReceiver.getResult().getStringArrayList(EXTRA_RECORD_IDS).get(0);
+    }
+
+    private Instant daysBeforeNow(int days) {
+        return mNow.minus(days, DAYS);
+    }
+
+    private static List<String> getRecordIds(List<? extends Record> records) {
+        return records.stream().map(Record::getMetadata).map(Metadata::getId).toList();
+    }
+}
diff --git a/tests/cts/src/android/healthconnect/cts/HydrationRecordTest.java b/tests/cts/src/android/healthconnect/cts/HydrationRecordTest.java
index 83e6a9e..cbd95b8 100644
--- a/tests/cts/src/android/healthconnect/cts/HydrationRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/HydrationRecordTest.java
@@ -39,6 +39,7 @@
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.units.Volume;
+import android.healthconnect.cts.utils.TestUtils;
 
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.runner.AndroidJUnit4;
diff --git a/tests/cts/src/android/healthconnect/cts/IntermenstrualBleedingRecordTest.java b/tests/cts/src/android/healthconnect/cts/IntermenstrualBleedingRecordTest.java
index 241177f..f706f88 100644
--- a/tests/cts/src/android/healthconnect/cts/IntermenstrualBleedingRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/IntermenstrualBleedingRecordTest.java
@@ -34,6 +34,7 @@
 import android.health.connect.datatypes.IntermenstrualBleedingRecord;
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
diff --git a/tests/cts/src/android/healthconnect/cts/LeanBodyMassRecordTest.java b/tests/cts/src/android/healthconnect/cts/LeanBodyMassRecordTest.java
index 8fad9fb..cde7e32 100644
--- a/tests/cts/src/android/healthconnect/cts/LeanBodyMassRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/LeanBodyMassRecordTest.java
@@ -35,6 +35,7 @@
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.units.Mass;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
diff --git a/tests/cts/src/android/healthconnect/cts/MenstruationFlowRecordTest.java b/tests/cts/src/android/healthconnect/cts/MenstruationFlowRecordTest.java
index af48e57..f48a814 100644
--- a/tests/cts/src/android/healthconnect/cts/MenstruationFlowRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/MenstruationFlowRecordTest.java
@@ -34,6 +34,7 @@
 import android.health.connect.datatypes.MenstruationFlowRecord;
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
diff --git a/tests/cts/src/android/healthconnect/cts/MenstruationPeriodRecordTest.java b/tests/cts/src/android/healthconnect/cts/MenstruationPeriodRecordTest.java
index a0aa236..f87fd26 100644
--- a/tests/cts/src/android/healthconnect/cts/MenstruationPeriodRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/MenstruationPeriodRecordTest.java
@@ -34,6 +34,7 @@
 import android.health.connect.datatypes.MenstruationPeriodRecord;
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
diff --git a/tests/cts/src/android/healthconnect/cts/NutritionRecordTest.java b/tests/cts/src/android/healthconnect/cts/NutritionRecordTest.java
index aca523e..7449cfe 100644
--- a/tests/cts/src/android/healthconnect/cts/NutritionRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/NutritionRecordTest.java
@@ -82,6 +82,7 @@
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.units.Energy;
 import android.health.connect.datatypes.units.Mass;
+import android.healthconnect.cts.utils.TestUtils;
 
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.runner.AndroidJUnit4;
diff --git a/tests/cts/src/android/healthconnect/cts/OvulationTestRecordTest.java b/tests/cts/src/android/healthconnect/cts/OvulationTestRecordTest.java
index c897b19..4d08043 100644
--- a/tests/cts/src/android/healthconnect/cts/OvulationTestRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/OvulationTestRecordTest.java
@@ -34,6 +34,7 @@
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.OvulationTestRecord;
 import android.health.connect.datatypes.Record;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
diff --git a/tests/cts/src/android/healthconnect/cts/OxygenSaturationRecordTest.java b/tests/cts/src/android/healthconnect/cts/OxygenSaturationRecordTest.java
index f978789..559e518 100644
--- a/tests/cts/src/android/healthconnect/cts/OxygenSaturationRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/OxygenSaturationRecordTest.java
@@ -35,6 +35,7 @@
 import android.health.connect.datatypes.OxygenSaturationRecord;
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.units.Percentage;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
diff --git a/tests/cts/src/android/healthconnect/cts/PowerRecordTest.java b/tests/cts/src/android/healthconnect/cts/PowerRecordTest.java
index 0797735..e92221f 100644
--- a/tests/cts/src/android/healthconnect/cts/PowerRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/PowerRecordTest.java
@@ -41,6 +41,7 @@
 import android.health.connect.datatypes.PowerRecord;
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.units.Power;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
diff --git a/tests/cts/src/android/healthconnect/cts/RespiratoryRateRecordTest.java b/tests/cts/src/android/healthconnect/cts/RespiratoryRateRecordTest.java
index d86c828..e816d51 100644
--- a/tests/cts/src/android/healthconnect/cts/RespiratoryRateRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/RespiratoryRateRecordTest.java
@@ -34,6 +34,7 @@
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.RespiratoryRateRecord;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
diff --git a/tests/cts/src/android/healthconnect/cts/RestingHeartRateRecordTest.java b/tests/cts/src/android/healthconnect/cts/RestingHeartRateRecordTest.java
index b537b52..2115378 100644
--- a/tests/cts/src/android/healthconnect/cts/RestingHeartRateRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/RestingHeartRateRecordTest.java
@@ -36,6 +36,7 @@
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.RestingHeartRateRecord;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
diff --git a/tests/cts/src/android/healthconnect/cts/SessionDatatypeDisabledFeatureTest.java b/tests/cts/src/android/healthconnect/cts/SessionDatatypeDisabledFeatureTest.java
index 44c69f3..cfe1f15 100644
--- a/tests/cts/src/android/healthconnect/cts/SessionDatatypeDisabledFeatureTest.java
+++ b/tests/cts/src/android/healthconnect/cts/SessionDatatypeDisabledFeatureTest.java
@@ -25,6 +25,7 @@
 import android.health.connect.datatypes.ExerciseSessionRecord;
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.SleepSessionRecord;
+import android.healthconnect.cts.utils.TestUtils;
 import android.provider.DeviceConfig;
 
 import androidx.test.platform.app.InstrumentationRegistry;
diff --git a/tests/cts/src/android/healthconnect/cts/SexualActivityRecordTest.java b/tests/cts/src/android/healthconnect/cts/SexualActivityRecordTest.java
index d63965e..50cc1c6 100644
--- a/tests/cts/src/android/healthconnect/cts/SexualActivityRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/SexualActivityRecordTest.java
@@ -34,6 +34,7 @@
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.SexualActivityRecord;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
diff --git a/tests/cts/src/android/healthconnect/cts/SleepDurationAggregationTest.java b/tests/cts/src/android/healthconnect/cts/SleepDurationAggregationTest.java
index 645ef91..144f2de 100644
--- a/tests/cts/src/android/healthconnect/cts/SleepDurationAggregationTest.java
+++ b/tests/cts/src/android/healthconnect/cts/SleepDurationAggregationTest.java
@@ -17,8 +17,8 @@
 package android.healthconnect.cts;
 
 import static android.health.connect.datatypes.SleepSessionRecord.SLEEP_DURATION_TOTAL;
-import static android.healthconnect.cts.TestUtils.SESSION_END_TIME;
-import static android.healthconnect.cts.TestUtils.SESSION_START_TIME;
+import static android.healthconnect.cts.utils.TestUtils.SESSION_END_TIME;
+import static android.healthconnect.cts.utils.TestUtils.SESSION_START_TIME;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -27,6 +27,7 @@
 import android.health.connect.AggregateRecordsResponse;
 import android.health.connect.TimeInstantRangeFilter;
 import android.health.connect.datatypes.SleepSessionRecord;
+import android.healthconnect.cts.utils.TestUtils;
 
 import org.junit.After;
 import org.junit.Test;
diff --git a/tests/cts/src/android/healthconnect/cts/SleepSessionRecordTest.java b/tests/cts/src/android/healthconnect/cts/SleepSessionRecordTest.java
index d781438..3bd0a15 100644
--- a/tests/cts/src/android/healthconnect/cts/SleepSessionRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/SleepSessionRecordTest.java
@@ -16,9 +16,9 @@
 
 package android.healthconnect.cts;
 
-import static android.healthconnect.cts.TestUtils.SESSION_END_TIME;
-import static android.healthconnect.cts.TestUtils.SESSION_START_TIME;
-import static android.healthconnect.cts.TestUtils.buildSleepSession;
+import static android.healthconnect.cts.utils.TestUtils.SESSION_END_TIME;
+import static android.healthconnect.cts.utils.TestUtils.SESSION_START_TIME;
+import static android.healthconnect.cts.utils.TestUtils.buildSleepSession;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -38,6 +38,7 @@
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.SleepSessionRecord;
+import android.healthconnect.cts.utils.TestUtils;
 
 import androidx.test.core.app.ApplicationProvider;
 
diff --git a/tests/cts/src/android/healthconnect/cts/SpeedRecordTest.java b/tests/cts/src/android/healthconnect/cts/SpeedRecordTest.java
index c797c4f..4056e23 100644
--- a/tests/cts/src/android/healthconnect/cts/SpeedRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/SpeedRecordTest.java
@@ -42,6 +42,7 @@
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.SpeedRecord;
 import android.health.connect.datatypes.units.Velocity;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
diff --git a/tests/cts/src/android/healthconnect/cts/StepsCadenceRecordTest.java b/tests/cts/src/android/healthconnect/cts/StepsCadenceRecordTest.java
index efe59d0..36d0801 100644
--- a/tests/cts/src/android/healthconnect/cts/StepsCadenceRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/StepsCadenceRecordTest.java
@@ -41,6 +41,7 @@
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.StepsCadenceRecord;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
diff --git a/tests/cts/src/android/healthconnect/cts/StepsRecordTest.java b/tests/cts/src/android/healthconnect/cts/StepsRecordTest.java
index 7f40fc6..76893a6 100644
--- a/tests/cts/src/android/healthconnect/cts/StepsRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/StepsRecordTest.java
@@ -18,10 +18,14 @@
 
 import static android.health.connect.HealthConnectException.ERROR_INVALID_ARGUMENT;
 import static android.health.connect.datatypes.StepsRecord.STEPS_COUNT_TOTAL;
-import static android.healthconnect.cts.TestUtils.isHardwareAutomotive;
+import static android.healthconnect.cts.utils.TestUtils.readRecordsWithPagination;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static java.time.ZoneOffset.UTC;
+import static java.time.temporal.ChronoUnit.DAYS;
+import static java.time.temporal.ChronoUnit.HOURS;
+
 import android.content.Context;
 import android.health.connect.AggregateRecordsGroupedByDurationResponse;
 import android.health.connect.AggregateRecordsGroupedByPeriodResponse;
@@ -32,6 +36,7 @@
 import android.health.connect.LocalTimeRangeFilter;
 import android.health.connect.ReadRecordsRequestUsingFilters;
 import android.health.connect.ReadRecordsRequestUsingIds;
+import android.health.connect.ReadRecordsResponse;
 import android.health.connect.RecordIdFilter;
 import android.health.connect.TimeInstantRangeFilter;
 import android.health.connect.changelog.ChangeLogTokenRequest;
@@ -42,15 +47,14 @@
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.StepsRecord;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
-import android.util.Pair;
 
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.After;
 import org.junit.Assert;
-import org.junit.Assume;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -187,10 +191,10 @@
 
     static StepsRecord getBaseStepsRecord(Instant time, ZoneOffset zoneOffset, int value) {
         return new StepsRecord.Builder(
-                new Metadata.Builder().build(),
-                time,
-                time.plus(1, ChronoUnit.SECONDS),
-                value)
+                        new Metadata.Builder().build(),
+                        time,
+                        time.plus(1, ChronoUnit.SECONDS),
+                        value)
                 .setStartZoneOffset(zoneOffset)
                 .setEndZoneOffset(zoneOffset)
                 .build();
@@ -241,12 +245,12 @@
         List<Record> recordList =
                 Arrays.asList(getStepsRecord_minusDays(1), getStepsRecord_minusDays(2));
         TestUtils.insertRecords(recordList);
-        Pair<List<StepsRecord>, Long> newStepsRecords =
-                TestUtils.readRecordsWithPagination(
+        ReadRecordsResponse<StepsRecord> response =
+                readRecordsWithPagination(
                         new ReadRecordsRequestUsingFilters.Builder<>(StepsRecord.class)
                                 .setPageSize(1)
                                 .build());
-        assertThat(newStepsRecords.first.size()).isEqualTo(1);
+        assertThat(response.getRecords()).hasSize(1);
     }
 
     @Test
@@ -308,20 +312,21 @@
         assertThat(requestUsingFilters.isAscending()).isTrue();
         assertThat(requestUsingFilters.getPageSize()).isEqualTo(1);
         assertThat(requestUsingFilters.getTimeRangeFilter()).isNull();
-        Pair<List<StepsRecord>, Long> oldStepsRecord =
-                TestUtils.readRecordsWithPagination(requestUsingFilters);
-        assertThat(oldStepsRecord.first.size()).isEqualTo(1);
+        ReadRecordsResponse<StepsRecord> oldStepsRecord =
+                readRecordsWithPagination(requestUsingFilters);
+        assertThat(oldStepsRecord.getRecords()).hasSize(1);
         ReadRecordsRequestUsingFilters<StepsRecord> requestUsingFiltersNew =
                 new ReadRecordsRequestUsingFilters.Builder<>(StepsRecord.class)
                         .setPageSize(1)
-                        .setPageToken(oldStepsRecord.second)
+                        .setPageToken(oldStepsRecord.getNextPageToken())
                         .build();
         assertThat(requestUsingFiltersNew.getPageSize()).isEqualTo(1);
-        assertThat(requestUsingFiltersNew.getPageToken()).isEqualTo(oldStepsRecord.second);
+        assertThat(requestUsingFiltersNew.getPageToken())
+                .isEqualTo(oldStepsRecord.getNextPageToken());
         assertThat(requestUsingFiltersNew.getTimeRangeFilter()).isNull();
-        Pair<List<StepsRecord>, Long> newStepsRecords =
-                TestUtils.readRecordsWithPagination(requestUsingFiltersNew);
-        assertThat(newStepsRecords.first.size()).isEqualTo(1);
+        ReadRecordsResponse<StepsRecord> newStepsRecords =
+                readRecordsWithPagination(requestUsingFiltersNew);
+        assertThat(newStepsRecords.getRecords()).hasSize(1);
     }
 
     @Test
@@ -333,22 +338,21 @@
                         getStepsRecord_minusDays(3),
                         getStepsRecord_minusDays(4));
         TestUtils.insertRecords(recordList);
-        Pair<List<StepsRecord>, Long> oldStepsRecord =
-                TestUtils.readRecordsWithPagination(
+        ReadRecordsResponse<StepsRecord> page1 =
+                readRecordsWithPagination(
                         new ReadRecordsRequestUsingFilters.Builder<>(StepsRecord.class)
                                 .setPageSize(1)
                                 .setAscending(false)
                                 .build());
-        assertThat(oldStepsRecord.first.size()).isEqualTo(1);
-        Pair<List<StepsRecord>, Long> newStepsRecords =
-                TestUtils.readRecordsWithPagination(
+        assertThat(page1.getRecords()).hasSize(1);
+        ReadRecordsResponse<StepsRecord> page2 =
+                readRecordsWithPagination(
                         new ReadRecordsRequestUsingFilters.Builder<>(StepsRecord.class)
                                 .setPageSize(1)
-                                .setPageToken(oldStepsRecord.second)
+                                .setPageToken(page1.getNextPageToken())
                                 .build());
-        assertThat(newStepsRecords.first.size()).isEqualTo(1);
-        assertThat(newStepsRecords.second).isNotEqualTo(oldStepsRecord.second);
-        assertThat(newStepsRecords.second).isLessThan(oldStepsRecord.second);
+        assertThat(page2.getRecords()).hasSize(1);
+        assertThat(page2.getNextPageToken()).isNotEqualTo(page1.getNextPageToken());
     }
 
     @Test
@@ -356,22 +360,19 @@
         List<Record> recordList =
                 Arrays.asList(TestUtils.getStepsRecord(), TestUtils.getStepsRecord());
         TestUtils.insertRecords(recordList);
-        Pair<List<StepsRecord>, Long> oldStepsRecord =
-                TestUtils.readRecordsWithPagination(
+        ReadRecordsResponse<StepsRecord> prevPage =
+                readRecordsWithPagination(
                         new ReadRecordsRequestUsingFilters.Builder<>(StepsRecord.class).build());
-        Pair<List<StepsRecord>, Long> newStepsRecord;
-        while (oldStepsRecord.second != -1) {
-            newStepsRecord =
-                    TestUtils.readRecordsWithPagination(
+        ReadRecordsResponse<StepsRecord> nextPage;
+        while (prevPage.getNextPageToken() != -1) {
+            nextPage =
+                    readRecordsWithPagination(
                             new ReadRecordsRequestUsingFilters.Builder<>(StepsRecord.class)
-                                    .setPageToken(oldStepsRecord.second)
+                                    .setPageToken(prevPage.getNextPageToken())
                                     .build());
-            if (newStepsRecord.second != -1) {
-                assertThat(newStepsRecord.second).isGreaterThan(oldStepsRecord.second);
-            }
-            oldStepsRecord = newStepsRecord;
+            prevPage = nextPage;
         }
-        assertThat(oldStepsRecord.second).isEqualTo(-1);
+        assertThat(prevPage.getNextPageToken()).isEqualTo(-1);
     }
 
     @Test
@@ -384,21 +385,21 @@
                         getStepsRecord_minusDays(3),
                         getStepsRecord_minusDays(4));
         TestUtils.insertRecords(recordList);
-        Pair<List<StepsRecord>, Long> oldStepsRecord =
-                TestUtils.readRecordsWithPagination(
+        ReadRecordsResponse<StepsRecord> oldStepsRecord =
+                readRecordsWithPagination(
                         new ReadRecordsRequestUsingFilters.Builder<>(StepsRecord.class)
                                 .setPageSize(1)
                                 .setAscending(false)
                                 .build());
-        assertThat(oldStepsRecord.first.size()).isEqualTo(1);
+        assertThat(oldStepsRecord.getRecords()).hasSize(1);
         try {
             ReadRecordsRequestUsingFilters<StepsRecord> requestUsingFilters =
                     new ReadRecordsRequestUsingFilters.Builder<>(StepsRecord.class)
                             .setPageSize(1)
-                            .setPageToken(oldStepsRecord.second)
+                            .setPageToken(oldStepsRecord.getNextPageToken())
                             .setAscending(true)
                             .build();
-            TestUtils.readRecordsWithPagination(requestUsingFilters);
+            readRecordsWithPagination(requestUsingFilters);
             Assert.fail(
                     "IllegalStateException  expected when both page token and page order is set");
         } catch (Exception exception) {
@@ -416,21 +417,21 @@
                         getStepsRecord_minusDays(3),
                         getStepsRecord_minusDays(4));
         TestUtils.insertRecords(recordList);
-        Pair<List<StepsRecord>, Long> oldStepsRecord =
-                TestUtils.readRecordsWithPagination(
+        ReadRecordsResponse<StepsRecord> oldStepsRecord =
+                readRecordsWithPagination(
                         new ReadRecordsRequestUsingFilters.Builder<>(StepsRecord.class)
                                 .setPageSize(1)
                                 .setAscending(false)
                                 .build());
-        assertThat(oldStepsRecord.first.size()).isEqualTo(1);
+        assertThat(oldStepsRecord.getRecords()).hasSize(1);
         try {
             ReadRecordsRequestUsingFilters<StepsRecord> requestUsingFilters =
                     new ReadRecordsRequestUsingFilters.Builder<>(StepsRecord.class)
                             .setPageSize(1)
-                            .setPageToken(oldStepsRecord.second)
+                            .setPageToken(oldStepsRecord.getNextPageToken())
                             .setAscending(false)
                             .build();
-            TestUtils.readRecordsWithPagination(requestUsingFilters);
+            readRecordsWithPagination(requestUsingFilters);
             Assert.fail(
                     "IllegalStateException  expected when both page token and page order is set");
         } catch (Exception exception) {
@@ -439,47 +440,6 @@
     }
 
     @Test
-    public void testReadStepsRecord_beforePermissionGrant() throws InterruptedException {
-        Assume.assumeFalse(isHardwareAutomotive());
-        List<Record> recordList =
-                Arrays.asList(
-                        getStepsRecord_minusDays(45),
-                        getStepsRecord_minusDays(20),
-                        getStepsRecord(10));
-        TestUtils.insertRecords(recordList);
-        List<StepsRecord> newStepsRecords =
-                TestUtils.readRecords(
-                        new ReadRecordsRequestUsingFilters.Builder<>(StepsRecord.class).build());
-        assertThat(newStepsRecords.size()).isEqualTo(2);
-    }
-
-    @Test
-    public void testAggregate_someRecordsAreBeforeStartDateAccess_expectTheyAreNotIncluded()
-            throws InterruptedException {
-        Assume.assumeFalse(isHardwareAutomotive());
-        List<Record> recordList =
-                Arrays.asList(
-                        getStepsRecord_minusDays(45),
-                        getStepsRecord_minusDays(20),
-                        getStepsRecord(10));
-        AggregateRecordsRequest<Long> aggregateRecordsRequest =
-                new AggregateRecordsRequest.Builder<Long>(
-                        new TimeInstantRangeFilter.Builder()
-                                .setStartTime(Instant.ofEpochMilli(0))
-                                .setEndTime(Instant.now().plus(1, ChronoUnit.DAYS))
-                                .build())
-                        .addAggregationType(STEPS_COUNT_TOTAL)
-                        .build();
-
-        AggregateRecordsResponse<Long> response =
-                TestUtils.getAggregateResponse(aggregateRecordsRequest, recordList);
-
-        // 20 (= 10 + 10) because the first record created with getStepsRecord_minusDays(45) is out
-        // of 30 days window prior to the first grant time.
-        assertThat(response.get(STEPS_COUNT_TOTAL)).isEqualTo(20);
-    }
-
-    @Test
     public void testDeleteStepsRecord_no_filters() throws InterruptedException {
         String id = TestUtils.insertRecordAndGetId(TestUtils.getCompleteStepsRecord());
         TestUtils.verifyDeleteRecords(new DeleteUsingFiltersRequest.Builder().build());
@@ -523,33 +483,33 @@
                                 ZoneOffset.MIN,
                                 70));
         TestUtils.insertRecords(testRecord);
-        Pair<List<StepsRecord>, Long> newStepsRecords =
-                TestUtils.readRecordsWithPagination(
+        ReadRecordsResponse<StepsRecord> newStepsRecords =
+                readRecordsWithPagination(
                         new ReadRecordsRequestUsingFilters.Builder<>(StepsRecord.class)
                                 .setTimeRangeFilter(filter)
                                 .setPageSize(1)
                                 .build());
-        assertThat(newStepsRecords.first.size()).isEqualTo(1);
-        assertThat(newStepsRecords.first.get(0).getCount()).isEqualTo(70);
+        assertThat(newStepsRecords.getRecords()).hasSize(1);
+        assertThat(newStepsRecords.getRecords().get(0).getCount()).isEqualTo(70);
         newStepsRecords =
-                TestUtils.readRecordsWithPagination(
+                readRecordsWithPagination(
                         new ReadRecordsRequestUsingFilters.Builder<>(StepsRecord.class)
                                 .setTimeRangeFilter(filter)
                                 .setPageSize(1)
-                                .setPageToken(newStepsRecords.second)
+                                .setPageToken(newStepsRecords.getNextPageToken())
                                 .build());
-        assertThat(newStepsRecords.first.size()).isEqualTo(1);
-        assertThat(newStepsRecords.first.get(0).getCount()).isEqualTo(50);
+        assertThat(newStepsRecords.getRecords()).hasSize(1);
+        assertThat(newStepsRecords.getRecords().get(0).getCount()).isEqualTo(50);
         newStepsRecords =
-                TestUtils.readRecordsWithPagination(
+                readRecordsWithPagination(
                         new ReadRecordsRequestUsingFilters.Builder<>(StepsRecord.class)
                                 .setTimeRangeFilter(filter)
                                 .setPageSize(1)
-                                .setPageToken(newStepsRecords.second)
+                                .setPageToken(newStepsRecords.getNextPageToken())
                                 .build());
-        assertThat(newStepsRecords.first.size()).isEqualTo(1);
-        assertThat(newStepsRecords.first.get(0).getCount()).isEqualTo(20);
-        assertThat(newStepsRecords.second).isEqualTo(-1);
+        assertThat(newStepsRecords.getRecords()).hasSize(1);
+        assertThat(newStepsRecords.getRecords().get(0).getCount()).isEqualTo(20);
+        assertThat(newStepsRecords.getNextPageToken()).isEqualTo(-1);
     }
 
     @Test
@@ -715,10 +675,10 @@
                 Arrays.asList(getStepsRecord(1000, 1, 1), getStepsRecord(1000, 2, 1));
         AggregateRecordsRequest<Long> aggregateRecordsRequest =
                 new AggregateRecordsRequest.Builder<Long>(
-                        new TimeInstantRangeFilter.Builder()
-                                .setStartTime(Instant.ofEpochMilli(0))
-                                .setEndTime(Instant.now().plus(1, ChronoUnit.DAYS))
-                                .build())
+                                new TimeInstantRangeFilter.Builder()
+                                        .setStartTime(Instant.ofEpochMilli(0))
+                                        .setEndTime(Instant.now().plus(1, ChronoUnit.DAYS))
+                                        .build())
                         .addAggregationType(STEPS_COUNT_TOTAL)
                         .build();
         assertThat(aggregateRecordsRequest.getAggregationTypes()).isNotNull();
@@ -731,10 +691,10 @@
         AggregateRecordsResponse<Long> newResponse =
                 TestUtils.getAggregateResponse(
                         new AggregateRecordsRequest.Builder<Long>(
-                                new TimeInstantRangeFilter.Builder()
-                                        .setStartTime(Instant.ofEpochMilli(0))
-                                        .setEndTime(Instant.now().plus(1, ChronoUnit.DAYS))
-                                        .build())
+                                        new TimeInstantRangeFilter.Builder()
+                                                .setStartTime(Instant.ofEpochMilli(0))
+                                                .setEndTime(Instant.now().plus(1, ChronoUnit.DAYS))
+                                                .build())
                                 .addAggregationType(STEPS_COUNT_TOTAL)
                                 .build(),
                         recordNew);
@@ -754,10 +714,10 @@
         AggregateRecordsResponse<Long> newResponse2 =
                 TestUtils.getAggregateResponse(
                         new AggregateRecordsRequest.Builder<Long>(
-                                new TimeInstantRangeFilter.Builder()
-                                        .setStartTime(Instant.ofEpochMilli(0))
-                                        .setEndTime(Instant.now().plus(1, ChronoUnit.DAYS))
-                                        .build())
+                                        new TimeInstantRangeFilter.Builder()
+                                                .setStartTime(Instant.ofEpochMilli(0))
+                                                .setEndTime(Instant.now().plus(1, ChronoUnit.DAYS))
+                                                .build())
                                 .addAggregationType(STEPS_COUNT_TOTAL)
                                 .build(),
                         recordNew2);
@@ -796,17 +756,17 @@
         List<Record> record =
                 Arrays.asList(
                         new StepsRecord.Builder(
-                                new Metadata.Builder().build(),
-                                start,
-                                start.plus(1, ChronoUnit.HOURS),
-                                600)
+                                        new Metadata.Builder().build(),
+                                        start,
+                                        start.plus(1, HOURS),
+                                        600)
                                 .build());
         AggregateRecordsRequest<Long> request =
                 new AggregateRecordsRequest.Builder<Long>(
-                        new TimeInstantRangeFilter.Builder()
-                                .setStartTime(start.plus(10, ChronoUnit.MINUTES))
-                                .setEndTime(start.plus(1, ChronoUnit.DAYS))
-                                .build())
+                                new TimeInstantRangeFilter.Builder()
+                                        .setStartTime(start.plus(10, ChronoUnit.MINUTES))
+                                        .setEndTime(start.plus(1, ChronoUnit.DAYS))
+                                        .build())
                         .addAggregationType(STEPS_COUNT_TOTAL)
                         .build();
 
@@ -831,10 +791,10 @@
         List<AggregateRecordsGroupedByDurationResponse<Long>> responses =
                 TestUtils.getAggregateResponseGroupByDuration(
                         new AggregateRecordsRequest.Builder<Long>(
-                                new TimeInstantRangeFilter.Builder()
-                                        .setStartTime(start)
-                                        .setEndTime(end)
-                                        .build())
+                                        new TimeInstantRangeFilter.Builder()
+                                                .setStartTime(start)
+                                                .setEndTime(end)
+                                                .build())
                                 .addAggregationType(STEPS_COUNT_TOTAL)
                                 .build(),
                         Duration.ofDays(1));
@@ -850,10 +810,10 @@
         List<AggregateRecordsGroupedByDurationResponse<Long>> responses =
                 TestUtils.getAggregateResponseGroupByDuration(
                         new AggregateRecordsRequest.Builder<Long>(
-                                new TimeInstantRangeFilter.Builder()
-                                        .setStartTime(start)
-                                        .setEndTime(end)
-                                        .build())
+                                        new TimeInstantRangeFilter.Builder()
+                                                .setStartTime(start)
+                                                .setEndTime(end)
+                                                .build())
                                 .addAggregationType(STEPS_COUNT_TOTAL)
                                 .build(),
                         Duration.ofDays(1));
@@ -872,14 +832,14 @@
         Instant start = Instant.now().minus(1, ChronoUnit.DAYS);
         Instant end = Instant.now();
         for (int i = 0; i < 10; i++) {
-            Instant st = start.plus(i, ChronoUnit.HOURS);
+            Instant st = start.plus(i, HOURS);
             List<Record> records =
                     Arrays.asList(
                             new StepsRecord.Builder(
-                                    new Metadata.Builder().build(),
-                                    st,
-                                    st.plus(1, ChronoUnit.HOURS),
-                                    1000)
+                                            new Metadata.Builder().build(),
+                                            st,
+                                            st.plus(1, HOURS),
+                                            1000)
                                     .build());
             TestUtils.insertRecords(records);
             Thread.sleep(100);
@@ -889,10 +849,10 @@
         List<AggregateRecordsGroupedByDurationResponse<Long>> responses =
                 TestUtils.getAggregateResponseGroupByDuration(
                         new AggregateRecordsRequest.Builder<Long>(
-                                new TimeInstantRangeFilter.Builder()
-                                        .setStartTime(start)
-                                        .setEndTime(end)
-                                        .build())
+                                        new TimeInstantRangeFilter.Builder()
+                                                .setStartTime(start)
+                                                .setEndTime(end)
+                                                .build())
                                 .addAggregationType(STEPS_COUNT_TOTAL)
                                 .build(),
                         Duration.ofHours(1));
@@ -918,12 +878,12 @@
         List<AggregateRecordsGroupedByDurationResponse<Long>> responses =
                 TestUtils.getAggregateResponseGroupByDuration(
                         new AggregateRecordsRequest.Builder<Long>(
-                                new TimeInstantRangeFilter.Builder()
-                                        .setStartTime(end.minus(24, ChronoUnit.HOURS))
-                                        .setEndTime(
-                                                end.minus(22, ChronoUnit.HOURS)
-                                                        .minus(30, ChronoUnit.MINUTES))
-                                        .build())
+                                        new TimeInstantRangeFilter.Builder()
+                                                .setStartTime(end.minus(24, HOURS))
+                                                .setEndTime(
+                                                        end.minus(22, HOURS)
+                                                                .minus(30, ChronoUnit.MINUTES))
+                                                .build())
                                 .addAggregationType(STEPS_COUNT_TOTAL)
                                 .build(),
                         Duration.ofHours(1));
@@ -939,10 +899,10 @@
         AggregateRecordsResponse<Long> oldResponse =
                 TestUtils.getAggregateResponse(
                         new AggregateRecordsRequest.Builder<Long>(
-                                new TimeInstantRangeFilter.Builder()
-                                        .setStartTime(Instant.ofEpochMilli(0))
-                                        .setEndTime(Instant.now().plus(1, ChronoUnit.DAYS))
-                                        .build())
+                                        new TimeInstantRangeFilter.Builder()
+                                                .setStartTime(Instant.ofEpochMilli(0))
+                                                .setEndTime(Instant.now().plus(1, ChronoUnit.DAYS))
+                                                .build())
                                 .addAggregationType(STEPS_COUNT_TOTAL)
                                 .build(),
                         records);
@@ -951,10 +911,10 @@
         AggregateRecordsResponse<Long> newResponse =
                 TestUtils.getAggregateResponse(
                         new AggregateRecordsRequest.Builder<Long>(
-                                new TimeInstantRangeFilter.Builder()
-                                        .setStartTime(Instant.ofEpochMilli(0))
-                                        .setEndTime(Instant.now().plus(1, ChronoUnit.DAYS))
-                                        .build())
+                                        new TimeInstantRangeFilter.Builder()
+                                                .setStartTime(Instant.ofEpochMilli(0))
+                                                .setEndTime(Instant.now().plus(1, ChronoUnit.DAYS))
+                                                .build())
                                 .addAggregationType(STEPS_COUNT_TOTAL)
                                 .build(),
                         recordNew);
@@ -1113,10 +1073,10 @@
         response = TestUtils.getChangeLogs(changeLogsRequest);
         assertThat(response.getUpsertedRecords().size()).isEqualTo(1);
         assertThat(
-                response.getUpsertedRecords().stream()
-                        .map(Record::getMetadata)
-                        .map(Metadata::getId)
-                        .toList())
+                        response.getUpsertedRecords().stream()
+                                .map(Record::getMetadata)
+                                .map(Metadata::getId)
+                                .toList())
                 .containsExactlyElementsIn(
                         testRecord.stream().map(Record::getMetadata).map(Metadata::getId).toList());
         assertThat(response.getDeletedLogs().size()).isEqualTo(0);
@@ -1143,10 +1103,10 @@
     @Test(expected = IllegalArgumentException.class)
     public void testCreateStepsRecord_invalidValue() {
         new StepsRecord.Builder(
-                new Metadata.Builder().build(),
-                Instant.now(),
-                Instant.now().plusMillis(1000),
-                1000001)
+                        new Metadata.Builder().build(),
+                        Instant.now(),
+                        Instant.now().plusMillis(1000),
+                        1000001)
                 .build();
     }
 
@@ -1159,6 +1119,75 @@
         testAggregationLocalTimeOffset(ZoneOffset.ofHours(4));
     }
 
+    @Test
+    public void testAggregateGroupByMonthPeriod_slicedCorrectly() throws Exception {
+        Instant startTime = Instant.now().minus(40, DAYS);
+        LocalDateTime startLocalTime =
+                LocalDateTime.ofInstant(Instant.ofEpochMilli(startTime.toEpochMilli()), UTC);
+        Instant endTime = startTime.plus(35, DAYS);
+        LocalDateTime endLocalTime =
+                LocalDateTime.ofInstant(Instant.ofEpochMilli(endTime.toEpochMilli()), UTC);
+        Instant bucketBoundary = startLocalTime.plusMonths(1).toInstant(UTC);
+        int stepsCount1 = 123;
+        int stepsCount2 = 456;
+        int stepsCount3 = 789;
+        int stepsCount4 = 951;
+
+        // CTS tests only have permission to read data from past 30 days
+        StepsRecord month1Steps1 =
+                getStepsRecord(
+                        Instant.now(),
+                        stepsCount1,
+                        /* daysPast= */ 30,
+                        /* durationHours= */ 1,
+                        UTC);
+        StepsRecord month1Steps2 =
+                getStepsRecord(
+                        bucketBoundary.minus(1, HOURS),
+                        stepsCount2,
+                        /* daysPast= */ 0,
+                        /* durationHours= */ 1,
+                        UTC);
+        StepsRecord month2Steps1 =
+                getStepsRecord(
+                        bucketBoundary,
+                        stepsCount3,
+                        /* daysPast= */ 0,
+                        /* durationHours= */ 1,
+                        UTC);
+        StepsRecord month2Steps2 =
+                getStepsRecord(
+                        endTime.minus(1, HOURS),
+                        stepsCount4,
+                        /* daysPast= */ 0,
+                        /* durationHours= */ 1,
+                        UTC);
+        TestUtils.insertRecords(
+                Arrays.asList(month1Steps1, month1Steps2, month2Steps1, month2Steps2));
+
+        // Due to the Parcel implementation, we have to set local time at UTC zone
+        AggregateRecordsRequest<Long> request =
+                new AggregateRecordsRequest.Builder<Long>(
+                                new LocalTimeRangeFilter.Builder()
+                                        .setStartTime(startLocalTime)
+                                        .setEndTime(endLocalTime)
+                                        .build())
+                        .addAggregationType(STEPS_COUNT_TOTAL)
+                        .build();
+        List<AggregateRecordsGroupedByPeriodResponse<Long>> aggregateResponse =
+                TestUtils.getAggregateResponseGroupByPeriod(request, Period.ofMonths(1));
+
+        assertThat(aggregateResponse.size()).isEqualTo(2);
+        assertThat(aggregateResponse.get(0).getStartTime()).isEqualTo(startLocalTime);
+        assertThat(aggregateResponse.get(0).getEndTime()).isEqualTo(startLocalTime.plusMonths(1));
+        assertThat(aggregateResponse.get(0).get(STEPS_COUNT_TOTAL))
+                .isEqualTo(stepsCount1 + stepsCount2);
+        assertThat(aggregateResponse.get(1).getStartTime()).isEqualTo(startLocalTime.plusMonths(1));
+        assertThat(aggregateResponse.get(1).getEndTime()).isEqualTo(endLocalTime);
+        assertThat(aggregateResponse.get(1).get(STEPS_COUNT_TOTAL))
+                .isEqualTo(stepsCount3 + stepsCount4);
+    }
+
     private void testAggregationLocalTimeOffset(ZoneOffset offset) throws InterruptedException {
         LocalDateTime endTimeLocal = LocalDateTime.now(offset);
         LocalDateTime startTimeLocal = endTimeLocal.minusDays(4);
@@ -1168,10 +1197,10 @@
         List<AggregateRecordsGroupedByPeriodResponse<Long>> responses =
                 TestUtils.getAggregateResponseGroupByPeriod(
                         new AggregateRecordsRequest.Builder<Long>(
-                                new LocalTimeRangeFilter.Builder()
-                                        .setStartTime(startTimeLocal)
-                                        .setEndTime(endTimeLocal)
-                                        .build())
+                                        new LocalTimeRangeFilter.Builder()
+                                                .setStartTime(startTimeLocal)
+                                                .setEndTime(endTimeLocal)
+                                                .build())
                                 .addAggregationType(STEPS_COUNT_TOTAL)
                                 .build(),
                         Period.ofDays(1));
@@ -1188,12 +1217,12 @@
                     .isEqualTo(groupBoundary.getDayOfYear());
             assertThat(responses.get(i).getDataOrigins(STEPS_COUNT_TOTAL)).hasSize(1);
             assertThat(
-                    responses
-                            .get(i)
-                            .getDataOrigins(STEPS_COUNT_TOTAL)
-                            .iterator()
-                            .next()
-                            .getPackageName())
+                            responses
+                                    .get(i)
+                                    .getDataOrigins(STEPS_COUNT_TOTAL)
+                                    .iterator()
+                                    .next()
+                                    .getPackageName())
                     .isEqualTo(ApplicationProvider.getApplicationContext().getPackageName());
         }
 
@@ -1211,10 +1240,10 @@
         List<AggregateRecordsGroupedByPeriodResponse<Long>> responses =
                 TestUtils.getAggregateResponseGroupByPeriod(
                         new AggregateRecordsRequest.Builder<Long>(
-                                new LocalTimeRangeFilter.Builder()
-                                        .setStartTime(endTimeLocal.minusHours(60))
-                                        .setEndTime(endTimeLocal.minusHours(24))
-                                        .build())
+                                        new LocalTimeRangeFilter.Builder()
+                                                .setStartTime(endTimeLocal.minusHours(60))
+                                                .setEndTime(endTimeLocal.minusHours(24))
+                                                .build())
                                 .addAggregationType(STEPS_COUNT_TOTAL)
                                 .build(),
                         Period.ofDays(1));
@@ -1234,26 +1263,26 @@
         AggregateRecordsResponse<Long> response =
                 TestUtils.getAggregateResponse(
                         new AggregateRecordsRequest.Builder<Long>(
-                                new LocalTimeRangeFilter.Builder()
-                                        .setStartTime(endTimeLocal.minusHours(25))
-                                        .setEndTime(endTimeLocal.minusHours(15))
-                                        .build())
+                                        new LocalTimeRangeFilter.Builder()
+                                                .setStartTime(endTimeLocal.minusHours(25))
+                                                .setEndTime(endTimeLocal.minusHours(15))
+                                                .build())
                                 .addAggregationType(STEPS_COUNT_TOTAL)
                                 .build(),
                         List.of(
                                 new StepsRecord.Builder(
-                                        TestUtils.generateMetadata(),
-                                        endTimeInstant.minusSeconds(500),
-                                        endTimeInstant.minusSeconds(100),
-                                        100)
+                                                TestUtils.generateMetadata(),
+                                                endTimeInstant.minusSeconds(500),
+                                                endTimeInstant.minusSeconds(100),
+                                                100)
                                         .setStartZoneOffset(ZoneOffset.MIN)
                                         .setEndZoneOffset(ZoneOffset.MIN)
                                         .build(),
                                 new StepsRecord.Builder(
-                                        TestUtils.generateMetadata(),
-                                        endTimeInstant.minusSeconds(1000),
-                                        endTimeInstant.minusSeconds(800),
-                                        100)
+                                                TestUtils.generateMetadata(),
+                                                endTimeInstant.minusSeconds(1000),
+                                                endTimeInstant.minusSeconds(800),
+                                                100)
                                         .setStartZoneOffset(ZoneOffset.MIN)
                                         .setEndZoneOffset(ZoneOffset.MIN)
                                         .build()));
@@ -1272,10 +1301,10 @@
                         getStepsRecord(instant, 40, 1, 1, ZoneOffset.ofHours(4)));
         AggregateRecordsRequest<Long> aggregateRecordsRequest =
                 new AggregateRecordsRequest.Builder<Long>(
-                        new TimeInstantRangeFilter.Builder()
-                                .setStartTime(Instant.ofEpochMilli(0))
-                                .setEndTime(Instant.now().plus(1, ChronoUnit.DAYS))
-                                .build())
+                                new TimeInstantRangeFilter.Builder()
+                                        .setStartTime(Instant.ofEpochMilli(0))
+                                        .setEndTime(Instant.now().plus(1, ChronoUnit.DAYS))
+                                        .build())
                         .addAggregationType(STEPS_COUNT_TOTAL)
                         .build();
         AggregateRecordsResponse<Long> oldResponse =
@@ -1288,10 +1317,10 @@
         AggregateRecordsResponse<Long> newResponse =
                 TestUtils.getAggregateResponse(
                         new AggregateRecordsRequest.Builder<Long>(
-                                new TimeInstantRangeFilter.Builder()
-                                        .setStartTime(Instant.ofEpochMilli(0))
-                                        .setEndTime(Instant.now().plus(1, ChronoUnit.DAYS))
-                                        .build())
+                                        new TimeInstantRangeFilter.Builder()
+                                                .setStartTime(Instant.ofEpochMilli(0))
+                                                .setEndTime(Instant.now().plus(1, ChronoUnit.DAYS))
+                                                .build())
                                 .addAggregationType(STEPS_COUNT_TOTAL)
                                 .build(),
                         recordNew);
@@ -1308,21 +1337,20 @@
         TestUtils.insertRecords(
                 List.of(
                         getStepsRecord(instant, 10, 5, 1, ZoneOffset.ofHours(2)),
-                        getStepsRecord(
-                                instant.plus(3, ChronoUnit.HOURS), 10, 5, 13, ZoneOffset.UTC),
+                        getStepsRecord(instant.plus(3, HOURS), 10, 5, 13, ZoneOffset.UTC),
                         getStepsRecord(instant, 20, 4, 1, ZoneOffset.ofHours(3)),
-                        getStepsRecord(instant.plus(4, ChronoUnit.HOURS), 10, 4, 3, ZoneOffset.UTC),
+                        getStepsRecord(instant.plus(4, HOURS), 10, 4, 3, ZoneOffset.UTC),
                         getStepsRecord(instant, 30, 3, 1, ZoneOffset.ofHours(5)),
-                        getStepsRecord(instant.plus(5, ChronoUnit.HOURS), 10, 3, 3, ZoneOffset.UTC),
+                        getStepsRecord(instant.plus(5, HOURS), 10, 3, 3, ZoneOffset.UTC),
                         getStepsRecord(instant, 10, 2, 1, ZoneOffset.ofHours(2)),
                         getStepsRecord(instant, 40, 1, 1, ZoneOffset.UTC)));
         List<AggregateRecordsGroupedByDurationResponse<Long>> responses =
                 TestUtils.getAggregateResponseGroupByDuration(
                         new AggregateRecordsRequest.Builder<Long>(
-                                new LocalTimeRangeFilter.Builder()
-                                        .setStartTime(startTimeLocal)
-                                        .setEndTime(endTimeLocal)
-                                        .build())
+                                        new LocalTimeRangeFilter.Builder()
+                                                .setStartTime(startTimeLocal)
+                                                .setEndTime(endTimeLocal)
+                                                .build())
                                 .addAggregationType(STEPS_COUNT_TOTAL)
                                 .build(),
                         Duration.ofDays(1));
@@ -1356,10 +1384,10 @@
         List<AggregateRecordsGroupedByDurationResponse<Long>> responses =
                 TestUtils.getAggregateResponseGroupByDuration(
                         new AggregateRecordsRequest.Builder<Long>(
-                                new LocalTimeRangeFilter.Builder()
-                                        .setStartTime(startTimeLocal)
-                                        .setEndTime(endTimeLocal)
-                                        .build())
+                                        new LocalTimeRangeFilter.Builder()
+                                                .setStartTime(startTimeLocal)
+                                                .setEndTime(endTimeLocal)
+                                                .build())
                                 .addAggregationType(STEPS_COUNT_TOTAL)
                                 .build(),
                         Duration.ofDays(1));
@@ -1376,12 +1404,12 @@
                     .isEqualTo(groupBoundary.getEpochSecond());
             assertThat(responses.get(i).getDataOrigins(STEPS_COUNT_TOTAL)).hasSize(1);
             assertThat(
-                    responses
-                            .get(i)
-                            .getDataOrigins(STEPS_COUNT_TOTAL)
-                            .iterator()
-                            .next()
-                            .getPackageName())
+                            responses
+                                    .get(i)
+                                    .getDataOrigins(STEPS_COUNT_TOTAL)
+                                    .iterator()
+                                    .next()
+                                    .getPackageName())
                     .isEqualTo(ApplicationProvider.getApplicationContext().getPackageName());
         }
 
@@ -1410,10 +1438,10 @@
                         .setLastModifiedTime(metadata.getLastModifiedTime())
                         .build();
         return new StepsRecord.Builder(
-                metadataWithId,
-                duplicateRecord.getStartTime(),
-                duplicateRecord.getEndTime(),
-                20)
+                        metadataWithId,
+                        duplicateRecord.getStartTime(),
+                        duplicateRecord.getEndTime(),
+                        20)
                 .setStartZoneOffset(ZoneOffset.systemDefault().getRules().getOffset(Instant.now()))
                 .setEndZoneOffset(ZoneOffset.systemDefault().getRules().getOffset(Instant.now()))
                 .build();
@@ -1435,19 +1463,19 @@
 
     static StepsRecord getBaseStepsRecord() {
         return new StepsRecord.Builder(
-                new Metadata.Builder().build(),
-                Instant.now(),
-                Instant.now().plusMillis(1000),
-                10)
+                        new Metadata.Builder().build(),
+                        Instant.now(),
+                        Instant.now().plusMillis(1000),
+                        10)
                 .build();
     }
 
     static StepsRecord getStepsRecord(int count) {
         return new StepsRecord.Builder(
-                new Metadata.Builder().build(),
-                Instant.now(),
-                Instant.now().plusMillis(1000),
-                count)
+                        new Metadata.Builder().build(),
+                        Instant.now(),
+                        Instant.now().plusMillis(1000),
+                        count)
                 .build();
     }
 
@@ -1465,8 +1493,7 @@
                 new StepsRecord.Builder(
                         new Metadata.Builder().build(),
                         time.minus(daysPast, ChronoUnit.DAYS),
-                        time.minus(daysPast, ChronoUnit.DAYS)
-                                .plus(durationInHours, ChronoUnit.HOURS),
+                        time.minus(daysPast, ChronoUnit.DAYS).plus(durationInHours, HOURS),
                         count);
         if (offset != null) {
             builder.setStartZoneOffset(offset).setEndZoneOffset(offset);
@@ -1481,16 +1508,16 @@
         testMetadataBuilder.setClientRecordVersion(version);
         Metadata testMetaData = testMetadataBuilder.build();
         return new StepsRecord.Builder(
-                testMetaData, Instant.now(), Instant.now().plusMillis(1000), steps)
+                        testMetaData, Instant.now(), Instant.now().plusMillis(1000), steps)
                 .build();
     }
 
     static StepsRecord getStepsRecord_minusDays(int days) {
         return new StepsRecord.Builder(
-                new Metadata.Builder().build(),
-                Instant.now().minus(days, ChronoUnit.DAYS),
-                Instant.now().minus(days, ChronoUnit.DAYS).plusMillis(1000),
-                10)
+                        new Metadata.Builder().build(),
+                        Instant.now().minus(days, ChronoUnit.DAYS),
+                        Instant.now().minus(days, ChronoUnit.DAYS).plusMillis(1000),
+                        10)
                 .build();
     }
 }
diff --git a/tests/cts/src/android/healthconnect/cts/TestUtils.java b/tests/cts/src/android/healthconnect/cts/TestUtils.java
deleted file mode 100644
index 41d40a2..0000000
--- a/tests/cts/src/android/healthconnect/cts/TestUtils.java
+++ /dev/null
@@ -1,1453 +0,0 @@
-/*
- * Copyright (C) 2022 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.health.connect.HealthDataCategory.ACTIVITY;
-import static android.health.connect.HealthDataCategory.BODY_MEASUREMENTS;
-import static android.health.connect.HealthDataCategory.CYCLE_TRACKING;
-import static android.health.connect.HealthDataCategory.NUTRITION;
-import static android.health.connect.HealthDataCategory.SLEEP;
-import static android.health.connect.HealthDataCategory.VITALS;
-import static android.health.connect.HealthPermissionCategory.BASAL_METABOLIC_RATE;
-import static android.health.connect.HealthPermissionCategory.EXERCISE;
-import static android.health.connect.HealthPermissionCategory.HEART_RATE;
-import static android.health.connect.HealthPermissionCategory.STEPS;
-import static android.health.connect.datatypes.Metadata.RECORDING_METHOD_ACTIVELY_RECORDED;
-import static android.health.connect.datatypes.RecordTypeIdentifier.RECORD_TYPE_BASAL_METABOLIC_RATE;
-import static android.health.connect.datatypes.RecordTypeIdentifier.RECORD_TYPE_HEART_RATE;
-import static android.health.connect.datatypes.RecordTypeIdentifier.RECORD_TYPE_STEPS;
-
-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.runWithShellPermissionIdentity;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.app.UiAutomation;
-import android.content.Context;
-import android.health.connect.AggregateRecordsGroupedByDurationResponse;
-import android.health.connect.AggregateRecordsGroupedByPeriodResponse;
-import android.health.connect.AggregateRecordsRequest;
-import android.health.connect.AggregateRecordsResponse;
-import android.health.connect.ApplicationInfoResponse;
-import android.health.connect.DeleteUsingFiltersRequest;
-import android.health.connect.HealthConnectDataState;
-import android.health.connect.HealthConnectException;
-import android.health.connect.HealthConnectManager;
-import android.health.connect.HealthPermissionCategory;
-import android.health.connect.HealthPermissions;
-import android.health.connect.InsertRecordsResponse;
-import android.health.connect.ReadRecordsRequest;
-import android.health.connect.ReadRecordsRequestUsingIds;
-import android.health.connect.ReadRecordsResponse;
-import android.health.connect.RecordIdFilter;
-import android.health.connect.RecordTypeInfoResponse;
-import android.health.connect.TimeInstantRangeFilter;
-import android.health.connect.accesslog.AccessLog;
-import android.health.connect.changelog.ChangeLogTokenRequest;
-import android.health.connect.changelog.ChangeLogTokenResponse;
-import android.health.connect.changelog.ChangeLogsRequest;
-import android.health.connect.changelog.ChangeLogsResponse;
-import android.health.connect.datatypes.ActiveCaloriesBurnedRecord;
-import android.health.connect.datatypes.AppInfo;
-import android.health.connect.datatypes.BasalBodyTemperatureRecord;
-import android.health.connect.datatypes.BasalMetabolicRateRecord;
-import android.health.connect.datatypes.BloodGlucoseRecord;
-import android.health.connect.datatypes.BloodPressureRecord;
-import android.health.connect.datatypes.BodyFatRecord;
-import android.health.connect.datatypes.BodyTemperatureRecord;
-import android.health.connect.datatypes.BodyWaterMassRecord;
-import android.health.connect.datatypes.BoneMassRecord;
-import android.health.connect.datatypes.CervicalMucusRecord;
-import android.health.connect.datatypes.CyclingPedalingCadenceRecord;
-import android.health.connect.datatypes.DataOrigin;
-import android.health.connect.datatypes.Device;
-import android.health.connect.datatypes.DistanceRecord;
-import android.health.connect.datatypes.ElevationGainedRecord;
-import android.health.connect.datatypes.ExerciseLap;
-import android.health.connect.datatypes.ExerciseRoute;
-import android.health.connect.datatypes.ExerciseSegment;
-import android.health.connect.datatypes.ExerciseSegmentType;
-import android.health.connect.datatypes.ExerciseSessionRecord;
-import android.health.connect.datatypes.ExerciseSessionType;
-import android.health.connect.datatypes.FloorsClimbedRecord;
-import android.health.connect.datatypes.HeartRateRecord;
-import android.health.connect.datatypes.HeartRateVariabilityRmssdRecord;
-import android.health.connect.datatypes.HeightRecord;
-import android.health.connect.datatypes.HydrationRecord;
-import android.health.connect.datatypes.IntermenstrualBleedingRecord;
-import android.health.connect.datatypes.LeanBodyMassRecord;
-import android.health.connect.datatypes.MenstruationFlowRecord;
-import android.health.connect.datatypes.MenstruationPeriodRecord;
-import android.health.connect.datatypes.Metadata;
-import android.health.connect.datatypes.NutritionRecord;
-import android.health.connect.datatypes.OvulationTestRecord;
-import android.health.connect.datatypes.OxygenSaturationRecord;
-import android.health.connect.datatypes.PowerRecord;
-import android.health.connect.datatypes.Record;
-import android.health.connect.datatypes.RespiratoryRateRecord;
-import android.health.connect.datatypes.RestingHeartRateRecord;
-import android.health.connect.datatypes.SexualActivityRecord;
-import android.health.connect.datatypes.SleepSessionRecord;
-import android.health.connect.datatypes.SpeedRecord;
-import android.health.connect.datatypes.StepsCadenceRecord;
-import android.health.connect.datatypes.StepsRecord;
-import android.health.connect.datatypes.TotalCaloriesBurnedRecord;
-import android.health.connect.datatypes.Vo2MaxRecord;
-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.MigrationException;
-import android.os.OutcomeReceiver;
-import android.os.ParcelFileDescriptor;
-import android.util.Log;
-import android.util.Pair;
-
-import androidx.annotation.NonNull;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import java.io.BufferedReader;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.time.Duration;
-import java.time.Instant;
-import java.time.LocalDate;
-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;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.stream.Collectors;
-
-public class TestUtils {
-    public static final String MANAGE_HEALTH_DATA = HealthPermissions.MANAGE_HEALTH_DATA_PERMISSION;
-    public static final Instant SESSION_START_TIME = Instant.now().minus(10, ChronoUnit.DAYS);
-    public static final Instant SESSION_END_TIME =
-            Instant.now().minus(10, ChronoUnit.DAYS).plus(1, ChronoUnit.HOURS);
-    private static final String TAG = "HCTestUtils";
-    private static final int TIMEOUT_SECONDS = 5;
-
-    public static boolean isHardwareAutomotive() {
-        return hasSystemFeature(AUTOMOTIVE_FEATURE);
-    }
-
-    public static ChangeLogTokenResponse getChangeLogToken(ChangeLogTokenRequest request)
-            throws InterruptedException {
-        Context context = ApplicationProvider.getApplicationContext();
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        assertThat(service).isNotNull();
-        CountDownLatch latch = new CountDownLatch(1);
-        AtomicReference<ChangeLogTokenResponse> response = new AtomicReference<>();
-        AtomicReference<HealthConnectException> exceptionAtomicReference = new AtomicReference<>();
-        service.getChangeLogToken(
-                request,
-                Executors.newSingleThreadExecutor(),
-                new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(ChangeLogTokenResponse result) {
-                        response.set(result);
-                        latch.countDown();
-                    }
-
-                    @Override
-                    public void onError(HealthConnectException exception) {
-                        Log.e(TAG, exception.getMessage());
-                        exceptionAtomicReference.set(exception);
-                        latch.countDown();
-                    }
-                });
-        assertThat(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue();
-        if (exceptionAtomicReference.get() != null) {
-            throw exceptionAtomicReference.get();
-        }
-        return response.get();
-    }
-
-    public static String insertRecordAndGetId(Record record) throws InterruptedException {
-        return insertRecords(Collections.singletonList(record)).get(0).getMetadata().getId();
-    }
-
-    /**
-     * Inserts records to the database.
-     *
-     * @param records records to insert
-     * @return inserted records
-     */
-    public static List<Record> insertRecords(List<Record> records) throws InterruptedException {
-        Context context = ApplicationProvider.getApplicationContext();
-        CountDownLatch latch = new CountDownLatch(1);
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        assertThat(service).isNotNull();
-        AtomicReference<List<Record>> response = new AtomicReference<>();
-        AtomicReference<HealthConnectException> exceptionAtomicReference = new AtomicReference<>();
-        service.insertRecords(
-                records,
-                Executors.newSingleThreadExecutor(),
-                new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(InsertRecordsResponse result) {
-                        response.set(result.getRecords());
-                        latch.countDown();
-                    }
-
-                    @Override
-                    public void onError(HealthConnectException exception) {
-                        exceptionAtomicReference.set(exception);
-                        latch.countDown();
-                    }
-                });
-        assertThat(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue();
-        if (exceptionAtomicReference.get() != null) {
-            throw exceptionAtomicReference.get();
-        }
-        assertThat(response.get()).hasSize(records.size());
-
-        return response.get();
-    }
-
-    public static void updateRecords(List<Record> records) throws InterruptedException {
-        Context context = ApplicationProvider.getApplicationContext();
-        CountDownLatch latch = new CountDownLatch(1);
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        assertThat(service).isNotNull();
-        AtomicReference<HealthConnectException> exceptionAtomicReference = new AtomicReference<>();
-        service.updateRecords(
-                records,
-                Executors.newSingleThreadExecutor(),
-                new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(Void result) {
-                        latch.countDown();
-                    }
-
-                    @Override
-                    public void onError(HealthConnectException exception) {
-                        exceptionAtomicReference.set(exception);
-                        latch.countDown();
-                    }
-                });
-        assertThat(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue();
-        if (exceptionAtomicReference.get() != null) {
-            throw exceptionAtomicReference.get();
-        }
-    }
-
-    public static ChangeLogsResponse getChangeLogs(ChangeLogsRequest changeLogsRequest)
-            throws InterruptedException {
-        Context context = ApplicationProvider.getApplicationContext();
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        assertThat(service).isNotNull();
-
-        CountDownLatch latch = new CountDownLatch(1);
-        AtomicReference<ChangeLogsResponse> response = new AtomicReference<>();
-        AtomicReference<HealthConnectException> healthConnectExceptionAtomicReference =
-                new AtomicReference<>();
-        service.getChangeLogs(
-                changeLogsRequest,
-                Executors.newSingleThreadExecutor(),
-                new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(ChangeLogsResponse result) {
-                        response.set(result);
-                        latch.countDown();
-                    }
-
-                    @Override
-                    public void onError(HealthConnectException exception) {
-                        healthConnectExceptionAtomicReference.set(exception);
-                        latch.countDown();
-                    }
-                });
-        assertThat(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isEqualTo(true);
-        if (healthConnectExceptionAtomicReference.get() != null) {
-            throw healthConnectExceptionAtomicReference.get();
-        }
-
-        return response.get();
-    }
-
-    public static Device buildDevice() {
-        return new Device.Builder()
-                .setManufacturer("google")
-                .setModel("Pixel4a")
-                .setType(2)
-                .build();
-    }
-
-    public static List<Record> getTestRecords() {
-        return Arrays.asList(
-                getStepsRecord(),
-                getHeartRateRecord(),
-                getBasalMetabolicRateRecord(),
-                buildExerciseSession());
-    }
-
-    public static List<RecordAndIdentifier> getRecordsAndIdentifiers() {
-        return Arrays.asList(
-                new RecordAndIdentifier(RECORD_TYPE_STEPS, getStepsRecord()),
-                new RecordAndIdentifier(RECORD_TYPE_HEART_RATE, getHeartRateRecord()),
-                new RecordAndIdentifier(
-                        RECORD_TYPE_BASAL_METABOLIC_RATE, getBasalMetabolicRateRecord()));
-    }
-
-    public static ExerciseRoute.Location buildLocationTimePoint(Instant startTime) {
-        return new ExerciseRoute.Location.Builder(
-                        Instant.ofEpochMilli(
-                                (long) (startTime.toEpochMilli() + 10 + Math.random() * 50)),
-                        Math.random() * 50,
-                        Math.random() * 50)
-                .build();
-    }
-
-    public static ExerciseRoute buildExerciseRoute() {
-        return new ExerciseRoute(
-                List.of(
-                        buildLocationTimePoint(SESSION_START_TIME),
-                        buildLocationTimePoint(SESSION_START_TIME),
-                        buildLocationTimePoint(SESSION_START_TIME)));
-    }
-
-    public static StepsRecord getStepsRecord() {
-        Context context = ApplicationProvider.getApplicationContext();
-        Device device =
-                new Device.Builder().setManufacturer("google").setModel("Pixel").setType(1).build();
-        DataOrigin dataOrigin =
-                new DataOrigin.Builder().setPackageName(context.getPackageName()).build();
-        return new StepsRecord.Builder(
-                        new Metadata.Builder()
-                                .setDevice(device)
-                                .setDataOrigin(dataOrigin)
-                                .setClientRecordId("SR" + Math.random())
-                                .build(),
-                        Instant.now(),
-                        Instant.now().plusMillis(1000),
-                        10)
-                .build();
-    }
-
-    public static StepsRecord getStepsRecord(String id) {
-        Context context = ApplicationProvider.getApplicationContext();
-        Device device =
-                new Device.Builder().setManufacturer("google").setModel("Pixel").setType(1).build();
-        DataOrigin dataOrigin =
-                new DataOrigin.Builder().setPackageName(context.getPackageName()).build();
-        return new StepsRecord.Builder(
-                        new Metadata.Builder()
-                                .setDevice(device)
-                                .setId(id)
-                                .setDataOrigin(dataOrigin)
-                                .build(),
-                        Instant.now(),
-                        Instant.now().plusMillis(1000),
-                        10)
-                .build();
-    }
-
-    public static HeartRateRecord getHeartRateRecord() {
-        Context context = ApplicationProvider.getApplicationContext();
-
-        HeartRateRecord.HeartRateSample heartRateSample =
-                new HeartRateRecord.HeartRateSample(72, Instant.now().plusMillis(100));
-        ArrayList<HeartRateRecord.HeartRateSample> heartRateSamples = new ArrayList<>();
-        heartRateSamples.add(heartRateSample);
-        heartRateSamples.add(heartRateSample);
-        Device device =
-                new Device.Builder().setManufacturer("google").setModel("Pixel").setType(1).build();
-        DataOrigin dataOrigin =
-                new DataOrigin.Builder().setPackageName(context.getPackageName()).build();
-
-        return new HeartRateRecord.Builder(
-                        new Metadata.Builder()
-                                .setDevice(device)
-                                .setDataOrigin(dataOrigin)
-                                .setClientRecordId("HR" + Math.random())
-                                .build(),
-                        Instant.now(),
-                        Instant.now().plusMillis(500),
-                        heartRateSamples)
-                .build();
-    }
-
-    public static HeartRateRecord getHeartRateRecord(int heartRate) {
-        HeartRateRecord.HeartRateSample heartRateSample =
-                new HeartRateRecord.HeartRateSample(heartRate, Instant.now().plusMillis(100));
-        ArrayList<HeartRateRecord.HeartRateSample> heartRateSamples = new ArrayList<>();
-        heartRateSamples.add(heartRateSample);
-        heartRateSamples.add(heartRateSample);
-
-        return new HeartRateRecord.Builder(
-                        new Metadata.Builder().build(),
-                        Instant.now(),
-                        Instant.now().plusMillis(500),
-                        heartRateSamples)
-                .build();
-    }
-
-    public static HeartRateRecord getHeartRateRecord(int heartRate, Instant instant) {
-        HeartRateRecord.HeartRateSample heartRateSample =
-                new HeartRateRecord.HeartRateSample(heartRate, instant);
-        ArrayList<HeartRateRecord.HeartRateSample> heartRateSamples = new ArrayList<>();
-        heartRateSamples.add(heartRateSample);
-        heartRateSamples.add(heartRateSample);
-
-        return new HeartRateRecord.Builder(
-                        new Metadata.Builder().build(),
-                        instant,
-                        instant.plusMillis(1000),
-                        heartRateSamples)
-                .build();
-    }
-
-    public static BasalMetabolicRateRecord getBasalMetabolicRateRecord() {
-        Context context = ApplicationProvider.getApplicationContext();
-        Device device =
-                new Device.Builder()
-                        .setManufacturer("google")
-                        .setModel("Pixel4a")
-                        .setType(2)
-                        .build();
-        DataOrigin dataOrigin =
-                new DataOrigin.Builder().setPackageName(context.getPackageName()).build();
-        return new BasalMetabolicRateRecord.Builder(
-                        new Metadata.Builder()
-                                .setDevice(device)
-                                .setDataOrigin(dataOrigin)
-                                .setClientRecordId("BMR" + Math.random())
-                                .build(),
-                        Instant.now(),
-                        Power.fromWatts(100.0))
-                .build();
-    }
-
-    public static <T> AggregateRecordsResponse<T> getAggregateResponse(
-            AggregateRecordsRequest<T> request, List<Record> recordsToInsert)
-            throws InterruptedException {
-        Context context = ApplicationProvider.getApplicationContext();
-        if (recordsToInsert != null) {
-            insertRecords(recordsToInsert);
-        }
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        assertThat(service).isNotNull();
-        CountDownLatch latch = new CountDownLatch(1);
-        AtomicReference<AggregateRecordsResponse<T>> response = new AtomicReference<>();
-        AtomicReference<HealthConnectException> healthConnectExceptionAtomicReference =
-                new AtomicReference<>();
-        service.aggregate(
-                request,
-                Executors.newSingleThreadExecutor(),
-                new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(AggregateRecordsResponse<T> result) {
-                        response.set(result);
-                        latch.countDown();
-                    }
-
-                    @Override
-                    public void onError(HealthConnectException healthConnectException) {
-                        healthConnectExceptionAtomicReference.set(healthConnectException);
-                        latch.countDown();
-                    }
-                });
-        assertThat(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue();
-        if (healthConnectExceptionAtomicReference.get() != null) {
-            throw healthConnectExceptionAtomicReference.get();
-        }
-
-        return response.get();
-    }
-
-    public static <T>
-            List<AggregateRecordsGroupedByDurationResponse<T>> getAggregateResponseGroupByDuration(
-                    AggregateRecordsRequest<T> request, Duration duration)
-                    throws InterruptedException {
-        Context context = ApplicationProvider.getApplicationContext();
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        assertThat(service).isNotNull();
-        CountDownLatch latch = new CountDownLatch(1);
-        AtomicReference<List<AggregateRecordsGroupedByDurationResponse<T>>> response =
-                new AtomicReference<>();
-        AtomicReference<HealthConnectException> healthConnectExceptionAtomicReference =
-                new AtomicReference<>();
-        service.aggregateGroupByDuration(
-                request,
-                duration,
-                Executors.newSingleThreadExecutor(),
-                new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(
-                            List<AggregateRecordsGroupedByDurationResponse<T>> result) {
-                        response.set(result);
-                        latch.countDown();
-                    }
-
-                    @Override
-                    public void onError(HealthConnectException healthConnectException) {
-                        healthConnectExceptionAtomicReference.set(healthConnectException);
-                        latch.countDown();
-                    }
-                });
-        assertThat(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue();
-        if (healthConnectExceptionAtomicReference.get() != null) {
-            throw healthConnectExceptionAtomicReference.get();
-        }
-        return response.get();
-    }
-
-    public static <T>
-            List<AggregateRecordsGroupedByPeriodResponse<T>> getAggregateResponseGroupByPeriod(
-                    AggregateRecordsRequest<T> request, Period period) throws InterruptedException {
-        Context context = ApplicationProvider.getApplicationContext();
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        assertThat(service).isNotNull();
-        CountDownLatch latch = new CountDownLatch(1);
-        AtomicReference<List<AggregateRecordsGroupedByPeriodResponse<T>>> response =
-                new AtomicReference<>();
-        AtomicReference<HealthConnectException> healthConnectExceptionAtomicReference =
-                new AtomicReference<>();
-        service.aggregateGroupByPeriod(
-                request,
-                period,
-                Executors.newSingleThreadExecutor(),
-                new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(List<AggregateRecordsGroupedByPeriodResponse<T>> result) {
-                        response.set(result);
-                        latch.countDown();
-                    }
-
-                    @Override
-                    public void onError(HealthConnectException healthConnectException) {
-                        healthConnectExceptionAtomicReference.set(healthConnectException);
-                        latch.countDown();
-                    }
-                });
-        assertThat(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue();
-        if (healthConnectExceptionAtomicReference.get() != null) {
-            throw healthConnectExceptionAtomicReference.get();
-        }
-
-        return response.get();
-    }
-
-    public static <T extends Record> List<T> readRecords(ReadRecordsRequest<T> request)
-            throws InterruptedException {
-        Context context = ApplicationProvider.getApplicationContext();
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        CountDownLatch latch = new CountDownLatch(1);
-        assertThat(service).isNotNull();
-        assertThat(request.getRecordType()).isNotNull();
-        AtomicReference<List<T>> response = new AtomicReference<>();
-        AtomicReference<HealthConnectException> healthConnectExceptionAtomicReference =
-                new AtomicReference<>();
-        service.readRecords(
-                request,
-                Executors.newSingleThreadExecutor(),
-                new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(ReadRecordsResponse<T> result) {
-                        response.set(result.getRecords());
-                        latch.countDown();
-                    }
-
-                    @Override
-                    public void onError(HealthConnectException exception) {
-                        Log.e(TAG, exception.getMessage());
-                        healthConnectExceptionAtomicReference.set(exception);
-                        latch.countDown();
-                    }
-                });
-        assertThat(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isEqualTo(true);
-        if (healthConnectExceptionAtomicReference.get() != null) {
-            throw healthConnectExceptionAtomicReference.get();
-        }
-        return response.get();
-    }
-
-    public static <T extends Record> void assertRecordNotFound(String uuid, Class<T> recordType)
-            throws InterruptedException {
-        assertThat(
-                        readRecords(
-                                new ReadRecordsRequestUsingIds.Builder<>(recordType)
-                                        .addId(uuid)
-                                        .build()))
-                .isEmpty();
-    }
-
-    public static <T extends Record> void assertRecordFound(String uuid, Class<T> recordType)
-            throws InterruptedException {
-        assertThat(
-                        readRecords(
-                                new ReadRecordsRequestUsingIds.Builder<>(recordType)
-                                        .addId(uuid)
-                                        .build()))
-                .isNotEmpty();
-    }
-
-    public static <T extends Record> Pair<List<T>, Long> readRecordsWithPagination(
-            ReadRecordsRequest<T> request) throws InterruptedException {
-        Context context = ApplicationProvider.getApplicationContext();
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        CountDownLatch latch = new CountDownLatch(1);
-        assertThat(service).isNotNull();
-        AtomicReference<List<T>> response = new AtomicReference<>();
-        AtomicReference<HealthConnectException> healthConnectExceptionAtomicReference =
-                new AtomicReference<>();
-        AtomicReference<Long> pageToken = new AtomicReference<>();
-        service.readRecords(
-                request,
-                Executors.newSingleThreadExecutor(),
-                new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(ReadRecordsResponse<T> result) {
-                        response.set(result.getRecords());
-                        pageToken.set(result.getNextPageToken());
-                        latch.countDown();
-                    }
-
-                    @Override
-                    public void onError(HealthConnectException exception) {
-                        healthConnectExceptionAtomicReference.set(exception);
-                        latch.countDown();
-                    }
-                });
-        assertThat(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isEqualTo(true);
-        if (healthConnectExceptionAtomicReference.get() != null) {
-            throw healthConnectExceptionAtomicReference.get();
-        }
-        return Pair.create(response.get(), pageToken.get());
-    }
-
-    public static void setAutoDeletePeriod(int period) throws InterruptedException {
-        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
-        uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA);
-        try {
-            Context context = ApplicationProvider.getApplicationContext();
-            HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-            CountDownLatch latch = new CountDownLatch(1);
-            assertThat(service).isNotNull();
-            AtomicReference<HealthConnectException> exceptionAtomicReference =
-                    new AtomicReference<>();
-            service.setRecordRetentionPeriodInDays(
-                    period,
-                    Executors.newSingleThreadExecutor(),
-                    new OutcomeReceiver<>() {
-                        @Override
-                        public void onResult(Void result) {
-                            latch.countDown();
-                        }
-
-                        @Override
-                        public void onError(HealthConnectException healthConnectException) {
-                            exceptionAtomicReference.set(healthConnectException);
-                            latch.countDown();
-                        }
-                    });
-            assertThat(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue();
-            if (exceptionAtomicReference.get() != null) {
-                throw exceptionAtomicReference.get();
-            }
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    public static void verifyDeleteRecords(DeleteUsingFiltersRequest request)
-            throws InterruptedException {
-        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
-        uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA);
-        try {
-            Context context = ApplicationProvider.getApplicationContext();
-            HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-            CountDownLatch latch = new CountDownLatch(1);
-            AtomicReference<HealthConnectException> exceptionAtomicReference =
-                    new AtomicReference<>();
-            assertThat(service).isNotNull();
-            service.deleteRecords(
-                    request,
-                    Executors.newSingleThreadExecutor(),
-                    new OutcomeReceiver<>() {
-                        @Override
-                        public void onResult(Void result) {
-                            latch.countDown();
-                        }
-
-                        @Override
-                        public void onError(HealthConnectException healthConnectException) {
-                            exceptionAtomicReference.set(healthConnectException);
-                            latch.countDown();
-                        }
-                    });
-            assertThat(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isEqualTo(true);
-            if (exceptionAtomicReference.get() != null) {
-                throw exceptionAtomicReference.get();
-            }
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    public static void verifyDeleteRecords(List<RecordIdFilter> request)
-            throws InterruptedException {
-
-        Context context = ApplicationProvider.getApplicationContext();
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        CountDownLatch latch = new CountDownLatch(1);
-        AtomicReference<HealthConnectException> exceptionAtomicReference = new AtomicReference<>();
-        assertThat(service).isNotNull();
-        service.deleteRecords(
-                request,
-                Executors.newSingleThreadExecutor(),
-                new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(Void result) {
-                        latch.countDown();
-                    }
-
-                    @Override
-                    public void onError(HealthConnectException healthConnectException) {
-                        exceptionAtomicReference.set(healthConnectException);
-                        latch.countDown();
-                    }
-                });
-        assertThat(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isEqualTo(true);
-        if (exceptionAtomicReference.get() != null) {
-            throw exceptionAtomicReference.get();
-        }
-    }
-
-    public static void verifyDeleteRecords(
-            Class<? extends Record> recordType, TimeInstantRangeFilter timeRangeFilter)
-            throws InterruptedException {
-        Context context = ApplicationProvider.getApplicationContext();
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        CountDownLatch latch = new CountDownLatch(1);
-        assertThat(service).isNotNull();
-        AtomicReference<HealthConnectException> exceptionAtomicReference = new AtomicReference<>();
-
-        service.deleteRecords(
-                recordType,
-                timeRangeFilter,
-                Executors.newSingleThreadExecutor(),
-                new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(Void result) {
-                        latch.countDown();
-                    }
-
-                    @Override
-                    public void onError(HealthConnectException healthConnectException) {
-                        exceptionAtomicReference.set(healthConnectException);
-                        latch.countDown();
-                    }
-                });
-        assertThat(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isEqualTo(true);
-        if (exceptionAtomicReference.get() != null) {
-            throw exceptionAtomicReference.get();
-        }
-    }
-
-    public static void deleteRecords(List<Record> records) throws InterruptedException {
-        List<RecordIdFilter> recordIdFilters =
-                records.stream()
-                        .map(
-                                (record ->
-                                        RecordIdFilter.fromId(
-                                                record.getClass(), record.getMetadata().getId())))
-                        .collect(Collectors.toList());
-        verifyDeleteRecords(recordIdFilters);
-    }
-
-    public static List<AccessLog> queryAccessLogs() throws InterruptedException {
-        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
-        uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA);
-        try {
-            Context context = ApplicationProvider.getApplicationContext();
-            HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-            assertThat(service).isNotNull();
-
-            CountDownLatch latch = new CountDownLatch(1);
-            AtomicReference<List<AccessLog>> response = new AtomicReference<>();
-            AtomicReference<HealthConnectException> exceptionAtomicReference =
-                    new AtomicReference<>();
-            service.queryAccessLogs(
-                    Executors.newSingleThreadExecutor(),
-                    new OutcomeReceiver<List<AccessLog>, HealthConnectException>() {
-
-                        @Override
-                        public void onResult(List<AccessLog> result) {
-                            response.set(result);
-                            latch.countDown();
-                        }
-
-                        @Override
-                        public void onError(@NonNull HealthConnectException exception) {
-                            exceptionAtomicReference.set(exception);
-                            latch.countDown();
-                        }
-                    });
-            assertThat(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isEqualTo(true);
-            if (exceptionAtomicReference.get() != null) {
-                throw exceptionAtomicReference.get();
-            }
-            return response.get();
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    public static Map<Class<? extends Record>, RecordTypeInfoResponse> queryAllRecordTypesInfo()
-            throws InterruptedException, NullPointerException {
-        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
-        uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA);
-        AtomicReference<Map<Class<? extends Record>, RecordTypeInfoResponse>> response =
-                new AtomicReference<>();
-        AtomicReference<HealthConnectException> responseException = new AtomicReference<>();
-        try {
-            Context context = ApplicationProvider.getApplicationContext();
-            CountDownLatch latch = new CountDownLatch(1);
-            HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-            assertThat(service).isNotNull();
-            service.queryAllRecordTypesInfo(
-                    Executors.newSingleThreadExecutor(),
-                    new OutcomeReceiver<
-                            Map<Class<? extends Record>, RecordTypeInfoResponse>,
-                            HealthConnectException>() {
-                        @Override
-                        public void onResult(
-                                Map<Class<? extends Record>, RecordTypeInfoResponse> result) {
-                            response.set(result);
-                            latch.countDown();
-                        }
-
-                        @Override
-                        public void onError(HealthConnectException exception) {
-                            responseException.set(exception);
-                            latch.countDown();
-                        }
-                    });
-            assertThat(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isEqualTo(true);
-            assertThat(responseException.get()).isNull();
-            assertThat(response).isNotNull();
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-        return response.get();
-    }
-
-    public static List<LocalDate> getActivityDates(List<Class<? extends Record>> recordTypes)
-            throws InterruptedException {
-        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
-        uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA);
-        try {
-            Context context = ApplicationProvider.getApplicationContext();
-            HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-            CountDownLatch latch = new CountDownLatch(1);
-            assertThat(service).isNotNull();
-            AtomicReference<List<LocalDate>> response = new AtomicReference<>();
-            AtomicReference<HealthConnectException> exceptionAtomicReference =
-                    new AtomicReference<>();
-            service.queryActivityDates(
-                    recordTypes,
-                    Executors.newSingleThreadExecutor(),
-                    new OutcomeReceiver<>() {
-                        @Override
-                        public void onResult(List<LocalDate> result) {
-                            response.set(result);
-                            latch.countDown();
-                        }
-
-                        @Override
-                        public void onError(HealthConnectException exception) {
-                            exceptionAtomicReference.set(exception);
-                            latch.countDown();
-                        }
-                    });
-
-            assertThat(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isEqualTo(true);
-            if (exceptionAtomicReference.get() != null) {
-                throw exceptionAtomicReference.get();
-            }
-
-            return response.get();
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    public static ExerciseSessionRecord buildExerciseSession() {
-        return buildExerciseSession(buildExerciseRoute(), "Morning training", "rain");
-    }
-
-    public static SleepSessionRecord buildSleepSession() {
-        return new SleepSessionRecord.Builder(
-                        generateMetadata(), SESSION_START_TIME, SESSION_END_TIME)
-                .setNotes("warm")
-                .setTitle("Afternoon nap")
-                .setStages(
-                        List.of(
-                                new SleepSessionRecord.Stage(
-                                        SESSION_START_TIME,
-                                        SESSION_START_TIME.plusSeconds(300),
-                                        SleepSessionRecord.StageType.STAGE_TYPE_SLEEPING_LIGHT),
-                                new SleepSessionRecord.Stage(
-                                        SESSION_START_TIME.plusSeconds(300),
-                                        SESSION_START_TIME.plusSeconds(600),
-                                        SleepSessionRecord.StageType.STAGE_TYPE_SLEEPING_REM),
-                                new SleepSessionRecord.Stage(
-                                        SESSION_START_TIME.plusSeconds(900),
-                                        SESSION_START_TIME.plusSeconds(1200),
-                                        SleepSessionRecord.StageType.STAGE_TYPE_SLEEPING_DEEP)))
-                .build();
-    }
-
-    public static void startMigration() throws InterruptedException {
-        Context context = ApplicationProvider.getApplicationContext();
-        CountDownLatch latch = new CountDownLatch(1);
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        assertThat(service).isNotNull();
-        AtomicReference<MigrationException> migrationExceptionAtomicReference =
-                new AtomicReference<>();
-        service.startMigration(
-                Executors.newSingleThreadExecutor(),
-                new OutcomeReceiver<Void, MigrationException>() {
-                    @Override
-                    public void onResult(Void result) {
-                        latch.countDown();
-                    }
-
-                    @Override
-                    public void onError(MigrationException exception) {
-                        migrationExceptionAtomicReference.set(exception);
-                        Log.e(TAG, exception.getMessage());
-                        latch.countDown();
-                    }
-                });
-        assertThat(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue();
-        if (migrationExceptionAtomicReference.get() != null) {
-            throw migrationExceptionAtomicReference.get();
-        }
-    }
-
-    public static void finishMigration() throws InterruptedException {
-        Context context = ApplicationProvider.getApplicationContext();
-        CountDownLatch latch = new CountDownLatch(1);
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        assertThat(service).isNotNull();
-        AtomicReference<MigrationException> migrationExceptionAtomicReference =
-                new AtomicReference<>();
-        service.finishMigration(
-                Executors.newSingleThreadExecutor(),
-                new OutcomeReceiver<Void, MigrationException>() {
-
-                    @Override
-                    public void onResult(Void result) {
-                        latch.countDown();
-                    }
-
-                    @Override
-                    public void onError(MigrationException exception) {
-                        migrationExceptionAtomicReference.set(exception);
-                        Log.e(TAG, exception.getMessage());
-                        latch.countDown();
-                    }
-                });
-        assertThat(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue();
-        if (migrationExceptionAtomicReference.get() != null) {
-            throw migrationExceptionAtomicReference.get();
-        }
-    }
-
-    public static void insertMinDataMigrationSdkExtensionVersion(int version)
-            throws InterruptedException {
-        Context context = ApplicationProvider.getApplicationContext();
-        CountDownLatch latch = new CountDownLatch(1);
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        assertThat(service).isNotNull();
-        AtomicReference<MigrationException> migrationExceptionAtomicReference =
-                new AtomicReference<>();
-        service.insertMinDataMigrationSdkExtensionVersion(
-                version,
-                Executors.newSingleThreadExecutor(),
-                new OutcomeReceiver<>() {
-
-                    @Override
-                    public void onResult(Void result) {
-                        latch.countDown();
-                    }
-
-                    @Override
-                    public void onError(MigrationException exception) {
-                        migrationExceptionAtomicReference.set(exception);
-                        Log.e(TAG, exception.getMessage());
-                        latch.countDown();
-                    }
-                });
-        assertThat(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue();
-        if (migrationExceptionAtomicReference.get() != null) {
-            throw migrationExceptionAtomicReference.get();
-        }
-    }
-
-    public static void deleteAllStagedRemoteData() {
-        Context context = ApplicationProvider.getApplicationContext();
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        assertThat(service).isNotNull();
-        runWithShellPermissionIdentity(
-                () ->
-                        // TODO(b/241542162): Avoid reflection once TestApi can be called from CTS
-                        service.getClass().getMethod("deleteAllStagedRemoteData").invoke(service),
-                "android.permission.DELETE_STAGED_HEALTH_CONNECT_REMOTE_DATA");
-    }
-
-    public static int getHealthConnectDataMigrationState() throws InterruptedException {
-        CountDownLatch latch = new CountDownLatch(1);
-        Context context = ApplicationProvider.getApplicationContext();
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        assertThat(service).isNotNull();
-        AtomicReference<HealthConnectDataState> returnedHealthConnectDataState =
-                new AtomicReference<>();
-        AtomicReference<HealthConnectException> responseException = new AtomicReference<>();
-        service.getHealthConnectDataState(
-                Executors.newSingleThreadExecutor(),
-                new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(HealthConnectDataState healthConnectDataState) {
-                        returnedHealthConnectDataState.set(healthConnectDataState);
-                        latch.countDown();
-                    }
-
-                    @Override
-                    public void onError(@NonNull HealthConnectException exception) {
-                        responseException.set(exception);
-                        latch.countDown();
-                    }
-                });
-        assertThat(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isEqualTo(true);
-        if (responseException.get() != null) {
-            throw responseException.get();
-        }
-        return returnedHealthConnectDataState.get().getDataMigrationState();
-    }
-
-    public static List<AppInfo> getApplicationInfo() throws InterruptedException {
-        Context context = ApplicationProvider.getApplicationContext();
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        CountDownLatch latch = new CountDownLatch(1);
-        assertThat(service).isNotNull();
-        AtomicReference<List<AppInfo>> response = new AtomicReference<>();
-        AtomicReference<HealthConnectException> exceptionAtomicReference = new AtomicReference<>();
-        service.getContributorApplicationsInfo(
-                Executors.newSingleThreadExecutor(),
-                new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(ApplicationInfoResponse result) {
-                        response.set(result.getApplicationInfoList());
-                        latch.countDown();
-                    }
-
-                    @Override
-                    public void onError(HealthConnectException exception) {
-                        exceptionAtomicReference.set(exception);
-                        Log.e(TAG, exception.getMessage());
-                        latch.countDown();
-                    }
-                });
-        assertThat(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isEqualTo(true);
-        if (exceptionAtomicReference.get() != null) {
-            throw exceptionAtomicReference.get();
-        }
-        return response.get();
-    }
-
-    public static <T extends Record> T getRecordById(List<T> list, String id) {
-        for (T record : list) {
-            if (record.getMetadata().getId().equals(id)) {
-                return record;
-            }
-        }
-
-        throw new AssertionError("Record not found with id: " + id);
-    }
-
-    static Metadata generateMetadata() {
-        Context context = ApplicationProvider.getApplicationContext();
-        return new Metadata.Builder()
-                .setDevice(buildDevice())
-                .setId(UUID.randomUUID().toString())
-                .setClientRecordId("clientRecordId" + Math.random())
-                .setDataOrigin(
-                        new DataOrigin.Builder().setPackageName(context.getPackageName()).build())
-                .setDevice(buildDevice())
-                .setRecordingMethod(Metadata.RECORDING_METHOD_UNKNOWN)
-                .build();
-    }
-
-    public static HeartRateRecord getHugeHeartRateRecord() {
-        Device device =
-                new Device.Builder()
-                        .setManufacturer("google")
-                        .setModel("Pixel4a")
-                        .setType(2)
-                        .build();
-        DataOrigin dataOrigin =
-                new DataOrigin.Builder().setPackageName("android.healthconnect.cts").build();
-        Metadata.Builder testMetadataBuilder = new Metadata.Builder();
-        testMetadataBuilder.setDevice(device).setDataOrigin(dataOrigin);
-        testMetadataBuilder.setClientRecordId("HRR" + Math.random());
-        testMetadataBuilder.setRecordingMethod(Metadata.RECORDING_METHOD_ACTIVELY_RECORDED);
-
-        HeartRateRecord.HeartRateSample heartRateRecord =
-                new HeartRateRecord.HeartRateSample(10, Instant.now().plusMillis(100));
-        ArrayList<HeartRateRecord.HeartRateSample> heartRateRecords =
-                new ArrayList<>(Collections.nCopies(85000, heartRateRecord));
-
-        return new HeartRateRecord.Builder(
-                        testMetadataBuilder.build(),
-                        Instant.now(),
-                        Instant.now().plusMillis(500),
-                        heartRateRecords)
-                .build();
-    }
-
-    public static StepsRecord getCompleteStepsRecord() {
-        Device device =
-                new Device.Builder().setManufacturer("google").setModel("Pixel").setType(1).build();
-        DataOrigin dataOrigin =
-                new DataOrigin.Builder().setPackageName("android.healthconnect.cts").build();
-
-        Metadata.Builder testMetadataBuilder = new Metadata.Builder();
-        testMetadataBuilder.setDevice(device).setDataOrigin(dataOrigin);
-        testMetadataBuilder.setClientRecordId("SR" + Math.random());
-        testMetadataBuilder.setRecordingMethod(RECORDING_METHOD_ACTIVELY_RECORDED);
-        Metadata testMetaData = testMetadataBuilder.build();
-        assertThat(testMetaData.getRecordingMethod()).isEqualTo(RECORDING_METHOD_ACTIVELY_RECORDED);
-        return new StepsRecord.Builder(
-                        testMetaData, Instant.now(), Instant.now().plusMillis(1000), 10)
-                .build();
-    }
-
-    public static StepsRecord getStepsRecord_update(
-            Record record, String id, String clientRecordId) {
-        Metadata metadata = record.getMetadata();
-        Metadata metadataWithId =
-                new Metadata.Builder()
-                        .setId(id)
-                        .setClientRecordId(clientRecordId)
-                        .setClientRecordVersion(metadata.getClientRecordVersion())
-                        .setDataOrigin(metadata.getDataOrigin())
-                        .setDevice(metadata.getDevice())
-                        .setLastModifiedTime(metadata.getLastModifiedTime())
-                        .build();
-        return new StepsRecord.Builder(
-                        metadataWithId, Instant.now(), Instant.now().plusMillis(2000), 20)
-                .setStartZoneOffset(ZoneOffset.systemDefault().getRules().getOffset(Instant.now()))
-                .setEndZoneOffset(ZoneOffset.systemDefault().getRules().getOffset(Instant.now()))
-                .build();
-    }
-
-    private static ExerciseSessionRecord buildExerciseSession(
-            ExerciseRoute route, String title, String notes) {
-        return new ExerciseSessionRecord.Builder(
-                        generateMetadata(),
-                        SESSION_START_TIME,
-                        SESSION_END_TIME,
-                        ExerciseSessionType.EXERCISE_SESSION_TYPE_OTHER_WORKOUT)
-                .setRoute(route)
-                .setLaps(
-                        List.of(
-                                new ExerciseLap.Builder(
-                                                SESSION_START_TIME,
-                                                SESSION_START_TIME.plusSeconds(20))
-                                        .setLength(Length.fromMeters(10))
-                                        .build(),
-                                new ExerciseLap.Builder(
-                                                SESSION_END_TIME.minusSeconds(20), SESSION_END_TIME)
-                                        .build()))
-                .setSegments(
-                        List.of(
-                                new ExerciseSegment.Builder(
-                                                SESSION_START_TIME.plusSeconds(1),
-                                                SESSION_START_TIME.plusSeconds(10),
-                                                ExerciseSegmentType
-                                                        .EXERCISE_SEGMENT_TYPE_BENCH_PRESS)
-                                        .build(),
-                                new ExerciseSegment.Builder(
-                                                SESSION_START_TIME.plusSeconds(21),
-                                                SESSION_START_TIME.plusSeconds(124),
-                                                ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_BURPEE)
-                                        .setRepetitionsCount(15)
-                                        .build()))
-                .setEndZoneOffset(ZoneOffset.MAX)
-                .setStartZoneOffset(ZoneOffset.MIN)
-                .setNotes(notes)
-                .setTitle(title)
-                .build();
-    }
-
-    static void populateAndResetExpectedResponseMap(
-            HashMap<Class<? extends Record>, RecordTypeInfoTestResponse> expectedResponseMap) {
-        expectedResponseMap.put(
-                ElevationGainedRecord.class,
-                new RecordTypeInfoTestResponse(
-                        ACTIVITY, HealthPermissionCategory.ELEVATION_GAINED, new ArrayList<>()));
-        expectedResponseMap.put(
-                OvulationTestRecord.class,
-                new RecordTypeInfoTestResponse(
-                        CYCLE_TRACKING,
-                        HealthPermissionCategory.OVULATION_TEST,
-                        new ArrayList<>()));
-        expectedResponseMap.put(
-                DistanceRecord.class,
-                new RecordTypeInfoTestResponse(
-                        ACTIVITY, HealthPermissionCategory.DISTANCE, new ArrayList<>()));
-        expectedResponseMap.put(
-                SpeedRecord.class,
-                new RecordTypeInfoTestResponse(
-                        ACTIVITY, HealthPermissionCategory.SPEED, new ArrayList<>()));
-
-        expectedResponseMap.put(
-                Vo2MaxRecord.class,
-                new RecordTypeInfoTestResponse(
-                        ACTIVITY, HealthPermissionCategory.VO2_MAX, new ArrayList<>()));
-        expectedResponseMap.put(
-                OxygenSaturationRecord.class,
-                new RecordTypeInfoTestResponse(
-                        VITALS, HealthPermissionCategory.OXYGEN_SATURATION, new ArrayList<>()));
-        expectedResponseMap.put(
-                TotalCaloriesBurnedRecord.class,
-                new RecordTypeInfoTestResponse(
-                        ACTIVITY,
-                        HealthPermissionCategory.TOTAL_CALORIES_BURNED,
-                        new ArrayList<>()));
-        expectedResponseMap.put(
-                HydrationRecord.class,
-                new RecordTypeInfoTestResponse(
-                        NUTRITION, HealthPermissionCategory.HYDRATION, new ArrayList<>()));
-        expectedResponseMap.put(
-                StepsRecord.class,
-                new RecordTypeInfoTestResponse(ACTIVITY, STEPS, new ArrayList<>()));
-        expectedResponseMap.put(
-                CervicalMucusRecord.class,
-                new RecordTypeInfoTestResponse(
-                        CYCLE_TRACKING,
-                        HealthPermissionCategory.CERVICAL_MUCUS,
-                        new ArrayList<>()));
-        expectedResponseMap.put(
-                ExerciseSessionRecord.class,
-                new RecordTypeInfoTestResponse(ACTIVITY, EXERCISE, new ArrayList<>()));
-        expectedResponseMap.put(
-                HeartRateRecord.class,
-                new RecordTypeInfoTestResponse(VITALS, HEART_RATE, new ArrayList<>()));
-        expectedResponseMap.put(
-                RespiratoryRateRecord.class,
-                new RecordTypeInfoTestResponse(
-                        VITALS, HealthPermissionCategory.RESPIRATORY_RATE, new ArrayList<>()));
-        expectedResponseMap.put(
-                BasalBodyTemperatureRecord.class,
-                new RecordTypeInfoTestResponse(
-                        VITALS,
-                        HealthPermissionCategory.BASAL_BODY_TEMPERATURE,
-                        new ArrayList<>()));
-        expectedResponseMap.put(
-                WheelchairPushesRecord.class,
-                new RecordTypeInfoTestResponse(
-                        ACTIVITY, HealthPermissionCategory.WHEELCHAIR_PUSHES, new ArrayList<>()));
-        expectedResponseMap.put(
-                PowerRecord.class,
-                new RecordTypeInfoTestResponse(
-                        ACTIVITY, HealthPermissionCategory.POWER, new ArrayList<>()));
-        expectedResponseMap.put(
-                BodyWaterMassRecord.class,
-                new RecordTypeInfoTestResponse(
-                        BODY_MEASUREMENTS,
-                        HealthPermissionCategory.BODY_WATER_MASS,
-                        new ArrayList<>()));
-        expectedResponseMap.put(
-                WeightRecord.class,
-                new RecordTypeInfoTestResponse(
-                        BODY_MEASUREMENTS, HealthPermissionCategory.WEIGHT, new ArrayList<>()));
-        expectedResponseMap.put(
-                BoneMassRecord.class,
-                new RecordTypeInfoTestResponse(
-                        BODY_MEASUREMENTS, HealthPermissionCategory.BONE_MASS, new ArrayList<>()));
-        expectedResponseMap.put(
-                RestingHeartRateRecord.class,
-                new RecordTypeInfoTestResponse(
-                        VITALS, HealthPermissionCategory.RESTING_HEART_RATE, new ArrayList<>()));
-        expectedResponseMap.put(
-                ActiveCaloriesBurnedRecord.class,
-                new RecordTypeInfoTestResponse(
-                        ACTIVITY,
-                        HealthPermissionCategory.ACTIVE_CALORIES_BURNED,
-                        new ArrayList<>()));
-        expectedResponseMap.put(
-                BodyFatRecord.class,
-                new RecordTypeInfoTestResponse(
-                        BODY_MEASUREMENTS, HealthPermissionCategory.BODY_FAT, new ArrayList<>()));
-        expectedResponseMap.put(
-                BodyTemperatureRecord.class,
-                new RecordTypeInfoTestResponse(
-                        VITALS, HealthPermissionCategory.BODY_TEMPERATURE, new ArrayList<>()));
-        expectedResponseMap.put(
-                NutritionRecord.class,
-                new RecordTypeInfoTestResponse(
-                        NUTRITION, HealthPermissionCategory.NUTRITION, new ArrayList<>()));
-        expectedResponseMap.put(
-                LeanBodyMassRecord.class,
-                new RecordTypeInfoTestResponse(
-                        BODY_MEASUREMENTS,
-                        HealthPermissionCategory.LEAN_BODY_MASS,
-                        new ArrayList<>()));
-        expectedResponseMap.put(
-                HeartRateVariabilityRmssdRecord.class,
-                new RecordTypeInfoTestResponse(
-                        VITALS,
-                        HealthPermissionCategory.HEART_RATE_VARIABILITY,
-                        new ArrayList<>()));
-        expectedResponseMap.put(
-                MenstruationFlowRecord.class,
-                new RecordTypeInfoTestResponse(
-                        CYCLE_TRACKING, HealthPermissionCategory.MENSTRUATION, new ArrayList<>()));
-        expectedResponseMap.put(
-                BloodGlucoseRecord.class,
-                new RecordTypeInfoTestResponse(
-                        VITALS, HealthPermissionCategory.BLOOD_GLUCOSE, new ArrayList<>()));
-        expectedResponseMap.put(
-                BloodPressureRecord.class,
-                new RecordTypeInfoTestResponse(
-                        VITALS, HealthPermissionCategory.BLOOD_PRESSURE, new ArrayList<>()));
-        expectedResponseMap.put(
-                CyclingPedalingCadenceRecord.class,
-                new RecordTypeInfoTestResponse(ACTIVITY, EXERCISE, new ArrayList<>()));
-        expectedResponseMap.put(
-                IntermenstrualBleedingRecord.class,
-                new RecordTypeInfoTestResponse(
-                        CYCLE_TRACKING,
-                        HealthPermissionCategory.INTERMENSTRUAL_BLEEDING,
-                        new ArrayList<>()));
-        expectedResponseMap.put(
-                FloorsClimbedRecord.class,
-                new RecordTypeInfoTestResponse(
-                        ACTIVITY, HealthPermissionCategory.FLOORS_CLIMBED, new ArrayList<>()));
-        expectedResponseMap.put(
-                StepsCadenceRecord.class,
-                new RecordTypeInfoTestResponse(ACTIVITY, STEPS, new ArrayList<>()));
-        expectedResponseMap.put(
-                HeightRecord.class,
-                new RecordTypeInfoTestResponse(
-                        BODY_MEASUREMENTS, HealthPermissionCategory.HEIGHT, new ArrayList<>()));
-        expectedResponseMap.put(
-                SexualActivityRecord.class,
-                new RecordTypeInfoTestResponse(
-                        CYCLE_TRACKING,
-                        HealthPermissionCategory.SEXUAL_ACTIVITY,
-                        new ArrayList<>()));
-        expectedResponseMap.put(
-                MenstruationPeriodRecord.class,
-                new RecordTypeInfoTestResponse(
-                        CYCLE_TRACKING, HealthPermissionCategory.MENSTRUATION, new ArrayList<>()));
-        expectedResponseMap.put(
-                SleepSessionRecord.class,
-                new RecordTypeInfoTestResponse(
-                        SLEEP, HealthPermissionCategory.SLEEP, new ArrayList<>()));
-        expectedResponseMap.put(
-                BasalMetabolicRateRecord.class,
-                new RecordTypeInfoTestResponse(
-                        BODY_MEASUREMENTS, BASAL_METABOLIC_RATE, new ArrayList<>()));
-    }
-
-    static String runShellCommand(String command) throws IOException {
-        UiAutomation uiAutomation =
-                androidx.test.platform.app.InstrumentationRegistry.getInstrumentation()
-                        .getUiAutomation();
-        uiAutomation.adoptShellPermissionIdentity();
-        final ParcelFileDescriptor stdout = uiAutomation.executeShellCommand(command);
-        StringBuilder output = new StringBuilder();
-
-        try (BufferedReader reader =
-                new BufferedReader(
-                        new InputStreamReader(new FileInputStream(stdout.getFileDescriptor())))) {
-            char[] buffer = new char[4096];
-            int bytesRead;
-            while ((bytesRead = reader.read(buffer)) != -1) {
-                output.append(buffer, 0, bytesRead);
-            }
-        } catch (FileNotFoundException e) {
-            Log.e(TAG, e.getMessage());
-        }
-
-        return output.toString();
-    }
-
-    static final class RecordAndIdentifier {
-        private final int id;
-        private final Record recordClass;
-
-        public RecordAndIdentifier(int id, Record recordClass) {
-            this.id = id;
-            this.recordClass = recordClass;
-        }
-
-        public int getId() {
-            return id;
-        }
-
-        public Record getRecordClass() {
-            return recordClass;
-        }
-    }
-
-    static class RecordTypeInfoTestResponse {
-        private final int mRecordTypePermission;
-        private final ArrayList<String> mContributingPackages;
-        private final int mRecordTypeCategory;
-
-        RecordTypeInfoTestResponse(
-                int recordTypeCategory,
-                int recordTypePermission,
-                ArrayList<String> contributingPackages) {
-            mRecordTypeCategory = recordTypeCategory;
-            mRecordTypePermission = recordTypePermission;
-            mContributingPackages = contributingPackages;
-        }
-
-        public int getRecordTypeCategory() {
-            return mRecordTypeCategory;
-        }
-
-        public int getRecordTypePermission() {
-            return mRecordTypePermission;
-        }
-
-        public ArrayList<String> getContributingPackages() {
-            return mContributingPackages;
-        }
-    }
-}
diff --git a/tests/cts/src/android/healthconnect/cts/TotalCaloriesBurnedRecordTest.java b/tests/cts/src/android/healthconnect/cts/TotalCaloriesBurnedRecordTest.java
index fa4c605..0cb85ef 100644
--- a/tests/cts/src/android/healthconnect/cts/TotalCaloriesBurnedRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/TotalCaloriesBurnedRecordTest.java
@@ -38,6 +38,7 @@
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.TotalCaloriesBurnedRecord;
 import android.health.connect.datatypes.units.Energy;
+import android.healthconnect.cts.utils.TestUtils;
 
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.runner.AndroidJUnit4;
@@ -513,8 +514,9 @@
         Energy totEnergyAfter = newResponse.get(TotalCaloriesBurnedRecord.ENERGY_TOTAL);
         assertThat(totEnergyBefore).isNotNull();
         assertThat(totEnergyAfter).isNotNull();
-        assertThat(totEnergyBefore.getInCalories()).isWithin(1).of(4693520);
-        assertThat(totEnergyAfter.getInCalories()).isWithin(1).of(1564540);
+        // The default total calories burned for one day is approx 1564.5 kCals
+        assertThat(totEnergyBefore.getInCalories()).isWithin(1).of(4_693_520);
+        assertThat(totEnergyAfter.getInCalories()).isWithin(1).of(1_564_540);
         Set<DataOrigin> newDataOrigin =
                 newResponse.getDataOrigins(TotalCaloriesBurnedRecord.ENERGY_TOTAL);
         for (DataOrigin itr : newDataOrigin) {
diff --git a/tests/cts/src/android/healthconnect/cts/Vo2MaxRecordTest.java b/tests/cts/src/android/healthconnect/cts/Vo2MaxRecordTest.java
index e83f2ea..52af376 100644
--- a/tests/cts/src/android/healthconnect/cts/Vo2MaxRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/Vo2MaxRecordTest.java
@@ -34,6 +34,7 @@
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.Vo2MaxRecord;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
diff --git a/tests/cts/src/android/healthconnect/cts/WeightRecordTest.java b/tests/cts/src/android/healthconnect/cts/WeightRecordTest.java
index 757fae9..93981aa 100644
--- a/tests/cts/src/android/healthconnect/cts/WeightRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/WeightRecordTest.java
@@ -46,6 +46,7 @@
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.WeightRecord;
 import android.health.connect.datatypes.units.Mass;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.core.app.ApplicationProvider;
@@ -62,7 +63,6 @@
 import java.time.Period;
 import java.time.ZoneOffset;
 import java.time.temporal.ChronoUnit;
-import java.time.temporal.TemporalUnit;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -212,10 +212,10 @@
         AggregateRecordsResponse<Mass> response =
                 TestUtils.getAggregateResponse(
                         new AggregateRecordsRequest.Builder<Mass>(
-                                new TimeInstantRangeFilter.Builder()
-                                        .setStartTime(Instant.ofEpochMilli(0))
-                                        .setEndTime(Instant.now().plus(1, DAYS))
-                                        .build())
+                                        new TimeInstantRangeFilter.Builder()
+                                                .setStartTime(Instant.ofEpochMilli(0))
+                                                .setEndTime(Instant.now().plus(1, DAYS))
+                                                .build())
                                 .addAggregationType(WEIGHT_AVG)
                                 .addAggregationType(WEIGHT_MAX)
                                 .addAggregationType(WEIGHT_MIN)
@@ -244,10 +244,10 @@
     public void testAggregation_zeroDuration_throwsException() throws Exception {
         TestUtils.getAggregateResponseGroupByDuration(
                 new AggregateRecordsRequest.Builder<Mass>(
-                        new TimeInstantRangeFilter.Builder()
-                                .setStartTime(Instant.ofEpochMilli(0))
-                                .setEndTime(Instant.now())
-                                .build())
+                                new TimeInstantRangeFilter.Builder()
+                                        .setStartTime(Instant.ofEpochMilli(0))
+                                        .setEndTime(Instant.now())
+                                        .build())
                         .addAggregationType(WEIGHT_AVG)
                         .build(),
                 Duration.ZERO);
@@ -257,11 +257,11 @@
     public void testAggregation_zeroPeriod_throwsException() throws Exception {
         TestUtils.getAggregateResponseGroupByPeriod(
                 new AggregateRecordsRequest.Builder<Mass>(
-                        new LocalTimeRangeFilter.Builder()
-                                .setStartTime(
-                                        LocalDateTime.now(ZoneOffset.UTC).minusDays(1))
-                                .setEndTime(LocalDateTime.now(ZoneOffset.UTC))
-                                .build())
+                                new LocalTimeRangeFilter.Builder()
+                                        .setStartTime(
+                                                LocalDateTime.now(ZoneOffset.UTC).minusDays(1))
+                                        .setEndTime(LocalDateTime.now(ZoneOffset.UTC))
+                                        .build())
                         .addAggregationType(WEIGHT_AVG)
                         .build(),
                 Period.ZERO);
@@ -271,11 +271,11 @@
     public void testAggregationPeriod_lotsOfGroups_throwsException() throws Exception {
         TestUtils.getAggregateResponseGroupByPeriod(
                 new AggregateRecordsRequest.Builder<Mass>(
-                        new LocalTimeRangeFilter.Builder()
-                                .setStartTime(
-                                        LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.UTC))
-                                .setEndTime(LocalDateTime.now(ZoneOffset.UTC))
-                                .build())
+                                new LocalTimeRangeFilter.Builder()
+                                        .setStartTime(
+                                                LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.UTC))
+                                        .setEndTime(LocalDateTime.now(ZoneOffset.UTC))
+                                        .build())
                         .addAggregationType(WEIGHT_AVG)
                         .build(),
                 Period.ofDays(1));
@@ -285,10 +285,10 @@
     public void testAggregation_hugeNumberOfGroups_throwsException() throws Exception {
         TestUtils.getAggregateResponseGroupByDuration(
                 new AggregateRecordsRequest.Builder<Mass>(
-                        new TimeInstantRangeFilter.Builder()
-                                .setStartTime(Instant.ofEpochMilli(0))
-                                .setEndTime(Instant.now())
-                                .build())
+                                new TimeInstantRangeFilter.Builder()
+                                        .setStartTime(Instant.ofEpochMilli(0))
+                                        .setEndTime(Instant.now())
+                                        .build())
                         .addAggregationType(WEIGHT_AVG)
                         .build(),
                 Duration.ofSeconds(1));
@@ -560,10 +560,10 @@
         response = TestUtils.getChangeLogs(changeLogsRequest);
         assertThat(response.getUpsertedRecords().size()).isEqualTo(1);
         assertThat(
-                response.getUpsertedRecords().stream()
-                        .map(Record::getMetadata)
-                        .map(Metadata::getId)
-                        .toList())
+                        response.getUpsertedRecords().stream()
+                                .map(Record::getMetadata)
+                                .map(Metadata::getId)
+                                .toList())
                 .containsExactlyElementsIn(
                         testRecord.stream().map(Record::getMetadata).map(Metadata::getId).toList());
         assertThat(response.getDeletedLogs().size()).isEqualTo(0);
@@ -614,12 +614,12 @@
                     .isEqualTo(groupBoundary.getDayOfYear());
             assertThat(responses.get(i).getDataOrigins(WEIGHT_MIN)).hasSize(1);
             assertThat(
-                    responses
-                            .get(i)
-                            .getDataOrigins(WEIGHT_MIN)
-                            .iterator()
-                            .next()
-                            .getPackageName())
+                            responses
+                                    .get(i)
+                                    .getDataOrigins(WEIGHT_MIN)
+                                    .iterator()
+                                    .next()
+                                    .getPackageName())
                     .isEqualTo(ApplicationProvider.getApplicationContext().getPackageName());
         }
 
@@ -645,10 +645,10 @@
         List<AggregateRecordsGroupedByDurationResponse<Mass>> responses =
                 TestUtils.getAggregateResponseGroupByDuration(
                         new AggregateRecordsRequest.Builder<Mass>(
-                                new LocalTimeRangeFilter.Builder()
-                                        .setStartTime(endTimeLocal.minusDays(3))
-                                        .setEndTime(endTimeLocal)
-                                        .build())
+                                        new LocalTimeRangeFilter.Builder()
+                                                .setStartTime(endTimeLocal.minusDays(3))
+                                                .setEndTime(endTimeLocal)
+                                                .build())
                                 .addAggregationType(WEIGHT_MAX)
                                 .build(),
                         Duration.ofDays(1));
@@ -665,12 +665,12 @@
                     .isEqualTo(groupBoundary.getEpochSecond());
             assertThat(responses.get(i).getDataOrigins(WEIGHT_MAX)).hasSize(1);
             assertThat(
-                    responses
-                            .get(i)
-                            .getDataOrigins(WEIGHT_MAX)
-                            .iterator()
-                            .next()
-                            .getPackageName())
+                            responses
+                                    .get(i)
+                                    .getDataOrigins(WEIGHT_MAX)
+                                    .iterator()
+                                    .next()
+                                    .getPackageName())
                     .isEqualTo(ApplicationProvider.getApplicationContext().getPackageName());
         }
 
@@ -694,25 +694,25 @@
         AggregateRecordsResponse<Mass> response =
                 TestUtils.getAggregateResponse(
                         new AggregateRecordsRequest.Builder<Mass>(
-                                new LocalTimeRangeFilter.Builder()
-                                        .setStartTime(endTimeLocal.minusHours(25))
-                                        .setEndTime(endTimeLocal.minusHours(15))
-                                        .build())
+                                        new LocalTimeRangeFilter.Builder()
+                                                .setStartTime(endTimeLocal.minusHours(25))
+                                                .setEndTime(endTimeLocal.minusHours(15))
+                                                .build())
                                 .addAggregationType(WEIGHT_MAX)
                                 .addAggregationType(WEIGHT_MIN)
                                 .addAggregationType(WEIGHT_AVG)
                                 .build(),
                         List.of(
                                 new WeightRecord.Builder(
-                                        TestUtils.generateMetadata(),
-                                        endTimeInstant,
-                                        Mass.fromGrams(10.0))
+                                                TestUtils.generateMetadata(),
+                                                endTimeInstant,
+                                                Mass.fromGrams(10.0))
                                         .setZoneOffset(ZoneOffset.MIN)
                                         .build(),
                                 new WeightRecord.Builder(
-                                        TestUtils.generateMetadata(),
-                                        endTimeInstant,
-                                        Mass.fromGrams(20.0))
+                                                TestUtils.generateMetadata(),
+                                                endTimeInstant,
+                                                Mass.fromGrams(20.0))
                                         .setZoneOffset(ZoneOffset.MIN)
                                         .build()));
 
@@ -740,25 +740,25 @@
         AggregateRecordsResponse<Mass> response =
                 TestUtils.getAggregateResponse(
                         new AggregateRecordsRequest.Builder<Mass>(
-                                new LocalTimeRangeFilter.Builder()
-                                        .setStartTime(endTimeLocal.minusMinutes(1))
-                                        .setEndTime(endTimeLocal.plusMinutes(1))
-                                        .build())
+                                        new LocalTimeRangeFilter.Builder()
+                                                .setStartTime(endTimeLocal.minusMinutes(1))
+                                                .setEndTime(endTimeLocal.plusMinutes(1))
+                                                .build())
                                 .addAggregationType(WEIGHT_MAX)
                                 .addAggregationType(WEIGHT_MIN)
                                 .addAggregationType(WEIGHT_AVG)
                                 .build(),
                         List.of(
                                 new WeightRecord.Builder(
-                                        TestUtils.generateMetadata(),
-                                        endTimeInstant,
-                                        Mass.fromGrams(10.0))
+                                                TestUtils.generateMetadata(),
+                                                endTimeInstant,
+                                                Mass.fromGrams(10.0))
                                         .setZoneOffset(offset)
                                         .build(),
                                 new WeightRecord.Builder(
-                                        TestUtils.generateMetadata(),
-                                        endTimeInstant,
-                                        Mass.fromGrams(20.0))
+                                                TestUtils.generateMetadata(),
+                                                endTimeInstant,
+                                                Mass.fromGrams(20.0))
                                         .setZoneOffset(offset)
                                         .build()));
 
@@ -779,19 +779,19 @@
         TestUtils.insertRecords(
                 List.of(
                         new WeightRecord.Builder(
-                                TestUtils.generateMetadata(),
-                                endTimeInstant.minus(20, DAYS),
-                                Mass.fromGrams(10.0))
+                                        TestUtils.generateMetadata(),
+                                        endTimeInstant.minus(20, DAYS),
+                                        Mass.fromGrams(10.0))
                                 .setZoneOffset(ZoneOffset.MIN)
                                 .build()));
 
         List<AggregateRecordsGroupedByPeriodResponse<Mass>> responses =
                 TestUtils.getAggregateResponseGroupByPeriod(
                         new AggregateRecordsRequest.Builder<Mass>(
-                                new LocalTimeRangeFilter.Builder()
-                                        .setStartTime(endTimeLocal.minusDays(30))
-                                        .setEndTime(endTimeLocal)
-                                        .build())
+                                        new LocalTimeRangeFilter.Builder()
+                                                .setStartTime(endTimeLocal.minusDays(30))
+                                                .setEndTime(endTimeLocal)
+                                                .build())
                                 .addAggregationType(WEIGHT_MAX)
                                 .build(),
                         Period.ofDays(15));
@@ -801,35 +801,6 @@
         assertThat(responses.get(1).get(WEIGHT_MAX)).isNull();
     }
 
-    @Test
-    public void testAggregate_someRecordsAreBeforeStartDateAccess_expectTheyAreNotIncluded()
-            throws InterruptedException {
-        Instant now = Instant.now();
-        double[] weights = {50_000, 51_000, 52_000};
-        List<Record> recordList =
-                Arrays.asList(
-                        getBaseWeightRecord(now, weights[0]),
-                        getBaseWeightRecord(now.minus(10, DAYS), weights[1]),
-                        getBaseWeightRecord(now.minus(40, DAYS), weights[2])
-                );
-        AggregateRecordsRequest<Mass> aggregateRecordsRequest =
-                new AggregateRecordsRequest.Builder<Mass>(
-                        new TimeInstantRangeFilter.Builder()
-                                .setStartTime(Instant.ofEpochMilli(0))
-                                .setEndTime(Instant.now().plus(1, DAYS))
-                                .build())
-                        .addAggregationType(WEIGHT_AVG)
-                        .build();
-
-        AggregateRecordsResponse<Mass> response =
-                TestUtils.getAggregateResponse(aggregateRecordsRequest, recordList);
-
-        // weights[1] is has time out of 30 days range from now, so it should be excluded from the
-        // avg
-        assertThat(response.get(WEIGHT_AVG)).isEqualTo(
-                Mass.fromGrams((weights[0] + weights[1]) / 2));
-    }
-
     private void readWeightRecordUsingClientId(List<Record> insertedRecord)
             throws InterruptedException {
         ReadRecordsRequestUsingIds.Builder<WeightRecord> request =
@@ -859,7 +830,7 @@
     @Test(expected = IllegalArgumentException.class)
     public void testCreateWeightRecord_invalidValue() {
         new WeightRecord.Builder(
-                new Metadata.Builder().build(), Instant.now(), Mass.fromGrams(1000000.1))
+                        new Metadata.Builder().build(), Instant.now(), Mass.fromGrams(1000000.1))
                 .build();
     }
 
@@ -881,21 +852,21 @@
 
     private static WeightRecord getBaseWeightRecord() {
         return new WeightRecord.Builder(
-                new Metadata.Builder().build(), Instant.now(), Mass.fromGrams(10.0))
+                        new Metadata.Builder().build(), Instant.now(), Mass.fromGrams(10.0))
                 .build();
     }
 
     static WeightRecord getBaseWeightRecord(Instant time, double weight) {
         return new WeightRecord.Builder(
-                new Metadata.Builder().build(), time, Mass.fromGrams(weight))
+                        new Metadata.Builder().build(), time, Mass.fromGrams(weight))
                 .build();
     }
 
     static WeightRecord getBaseWeightRecord(double weight) {
         return new WeightRecord.Builder(
-                new Metadata.Builder().setClientRecordId("WR" + Math.random()).build(),
-                Instant.now(),
-                Mass.fromGrams(weight))
+                        new Metadata.Builder().setClientRecordId("WR" + Math.random()).build(),
+                        Instant.now(),
+                        Mass.fromGrams(weight))
                 .build();
     }
 
diff --git a/tests/cts/src/android/healthconnect/cts/WheelchairPushesRecordTest.java b/tests/cts/src/android/healthconnect/cts/WheelchairPushesRecordTest.java
index ed49508..04dd5e4 100644
--- a/tests/cts/src/android/healthconnect/cts/WheelchairPushesRecordTest.java
+++ b/tests/cts/src/android/healthconnect/cts/WheelchairPushesRecordTest.java
@@ -37,6 +37,7 @@
 import android.health.connect.datatypes.Metadata;
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.WheelchairPushesRecord;
+import android.healthconnect.cts.utils.TestUtils;
 
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.runner.AndroidJUnit4;
diff --git a/tests/cts/src/android/healthconnect/cts/nopermission/HealthConnectManagerNoPermissionsGrantedTest.java b/tests/cts/src/android/healthconnect/cts/nopermission/HealthConnectManagerNoPermissionsGrantedTest.java
index 09a6ea3..4056741 100644
--- a/tests/cts/src/android/healthconnect/cts/nopermission/HealthConnectManagerNoPermissionsGrantedTest.java
+++ b/tests/cts/src/android/healthconnect/cts/nopermission/HealthConnectManagerNoPermissionsGrantedTest.java
@@ -29,7 +29,7 @@
 import android.health.connect.changelog.ChangeLogTokenRequest;
 import android.health.connect.datatypes.DataOrigin;
 import android.health.connect.datatypes.Record;
-import android.healthconnect.cts.TestUtils;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.runner.AndroidJUnit4;
diff --git a/tests/cts/src/android/healthconnect/cts/ratelimiter/RateLimiterTest.java b/tests/cts/src/android/healthconnect/cts/ratelimiter/RateLimiterTest.java
index 06e4d8b..0a2b298 100644
--- a/tests/cts/src/android/healthconnect/cts/ratelimiter/RateLimiterTest.java
+++ b/tests/cts/src/android/healthconnect/cts/ratelimiter/RateLimiterTest.java
@@ -34,7 +34,7 @@
 import android.health.connect.datatypes.DataOrigin;
 import android.health.connect.datatypes.Record;
 import android.health.connect.datatypes.StepsRecord;
-import android.healthconnect.cts.TestUtils;
+import android.healthconnect.cts.utils.TestUtils;
 import android.platform.test.annotations.AppModeFull;
 import android.provider.DeviceConfig;
 
@@ -275,8 +275,8 @@
     /**
      * This method tries to use the Maximum write quota possible. Distributes the load across
      * Insert, and Update APIs. Also, we provide dataManagement permission to
-     * TestUtils.verifyDeleteRecords. We test unmetered rate limting as well here. No write quota is
-     * used by TestUtils.verifyDeleteRecords.
+     * MultiAppTestUtils.verifyDeleteRecords. We test unmetered rate limting as well here. No write
+     * quota is used by MultiAppTestUtils.verifyDeleteRecords.
      */
     private void tryAcquireCallQuotaNTimesForWrite(int nTimes) throws InterruptedException {
         List<Record> testRecord = Arrays.asList(TestUtils.getCompleteStepsRecord());
diff --git a/tests/cts/src/android/healthconnect/cts/showmigrationinfointent/ShowMigrationInfoIntentAbsentTest.java b/tests/cts/src/android/healthconnect/cts/showmigrationinfointent/ShowMigrationInfoIntentAbsentTest.java
index 5b4b362..1f1ceaa 100644
--- a/tests/cts/src/android/healthconnect/cts/showmigrationinfointent/ShowMigrationInfoIntentAbsentTest.java
+++ b/tests/cts/src/android/healthconnect/cts/showmigrationinfointent/ShowMigrationInfoIntentAbsentTest.java
@@ -24,7 +24,7 @@
 import android.content.Context;
 import android.health.connect.HealthConnectManager;
 import android.health.connect.migration.MigrationException;
-import android.healthconnect.cts.TestUtils;
+import android.healthconnect.cts.utils.TestUtils;
 import android.os.Build;
 import android.os.OutcomeReceiver;
 import android.os.ext.SdkExtensions;
diff --git a/tests/cts/src/android/healthconnect/cts/ui/CategoriesFragmentTest.kt b/tests/cts/src/android/healthconnect/cts/ui/CategoriesFragmentTest.kt
index a653d71..2eff621 100644
--- a/tests/cts/src/android/healthconnect/cts/ui/CategoriesFragmentTest.kt
+++ b/tests/cts/src/android/healthconnect/cts/ui/CategoriesFragmentTest.kt
@@ -19,9 +19,9 @@
 import android.health.connect.datatypes.DistanceRecord
 import android.health.connect.datatypes.Record
 import android.health.connect.datatypes.StepsRecord
-import android.healthconnect.cts.TestUtils.insertRecords
-import android.healthconnect.cts.TestUtils.setAutoDeletePeriod
-import android.healthconnect.cts.TestUtils.verifyDeleteRecords
+import android.healthconnect.cts.utils.TestUtils.insertRecords
+import android.healthconnect.cts.utils.TestUtils.setAutoDeletePeriod
+import android.healthconnect.cts.utils.TestUtils.verifyDeleteRecords
 import android.healthconnect.cts.lib.ActivityLauncher.launchDataActivity
 import android.healthconnect.cts.lib.UiTestUtils.clickOnText
 import android.healthconnect.cts.lib.UiTestUtils.stepsRecordFromTestApp
diff --git a/tests/cts/src/android/healthconnect/cts/ui/DataAccessFragmentTest.kt b/tests/cts/src/android/healthconnect/cts/ui/DataAccessFragmentTest.kt
index 359ca50..dbe0665 100644
--- a/tests/cts/src/android/healthconnect/cts/ui/DataAccessFragmentTest.kt
+++ b/tests/cts/src/android/healthconnect/cts/ui/DataAccessFragmentTest.kt
@@ -17,8 +17,8 @@
 
 import android.health.connect.TimeInstantRangeFilter
 import android.health.connect.datatypes.StepsRecord
-import android.healthconnect.cts.TestUtils.insertRecords
-import android.healthconnect.cts.TestUtils.verifyDeleteRecords
+import android.healthconnect.cts.utils.TestUtils.insertRecords
+import android.healthconnect.cts.utils.TestUtils.verifyDeleteRecords
 import android.healthconnect.cts.lib.ActivityLauncher.launchDataActivity
 import android.healthconnect.cts.lib.UiTestUtils.clickOnText
 import android.healthconnect.cts.lib.UiTestUtils.stepsRecordFromTestApp
diff --git a/tests/cts/src/android/healthconnect/cts/ui/DataEntriesFragmentTest.kt b/tests/cts/src/android/healthconnect/cts/ui/DataEntriesFragmentTest.kt
index 32e1abc..783393f 100644
--- a/tests/cts/src/android/healthconnect/cts/ui/DataEntriesFragmentTest.kt
+++ b/tests/cts/src/android/healthconnect/cts/ui/DataEntriesFragmentTest.kt
@@ -18,8 +18,8 @@
 import android.health.connect.TimeInstantRangeFilter
 import android.health.connect.datatypes.DistanceRecord
 import android.health.connect.datatypes.StepsRecord
-import android.healthconnect.cts.TestUtils.insertRecords
-import android.healthconnect.cts.TestUtils.verifyDeleteRecords
+import android.healthconnect.cts.utils.TestUtils.insertRecords
+import android.healthconnect.cts.utils.TestUtils.verifyDeleteRecords
 import android.healthconnect.cts.lib.ActivityLauncher.launchDataActivity
 import android.healthconnect.cts.lib.UiTestUtils.clickOnText
 import android.healthconnect.cts.lib.UiTestUtils.distanceRecordFromTestApp
diff --git a/tests/cts/src/android/healthconnect/cts/ui/HomeFragmentTest.kt b/tests/cts/src/android/healthconnect/cts/ui/HomeFragmentTest.kt
index 5715538..6adcbd1 100644
--- a/tests/cts/src/android/healthconnect/cts/ui/HomeFragmentTest.kt
+++ b/tests/cts/src/android/healthconnect/cts/ui/HomeFragmentTest.kt
@@ -19,9 +19,9 @@
 import android.health.connect.datatypes.BasalMetabolicRateRecord
 import android.health.connect.datatypes.HeartRateRecord
 import android.health.connect.datatypes.StepsRecord
-import android.healthconnect.cts.TestUtils.verifyDeleteRecords
+import android.healthconnect.cts.utils.TestUtils.verifyDeleteRecords
 import android.healthconnect.cts.lib.ActivityLauncher.launchMainActivity
-import android.healthconnect.cts.lib.TestUtils.insertRecordAs
+import android.healthconnect.cts.lib.MultiAppTestUtils.insertRecordAs
 import android.healthconnect.cts.lib.UiTestUtils.clickOnText
 import android.healthconnect.cts.lib.UiTestUtils.waitDisplayed
 import androidx.test.uiautomator.By
diff --git a/tests/cts/src/android/healthconnect/cts/ui/PermissionTypesFragmentTest.kt b/tests/cts/src/android/healthconnect/cts/ui/PermissionTypesFragmentTest.kt
index d59a853..9ff9024 100644
--- a/tests/cts/src/android/healthconnect/cts/ui/PermissionTypesFragmentTest.kt
+++ b/tests/cts/src/android/healthconnect/cts/ui/PermissionTypesFragmentTest.kt
@@ -19,9 +19,9 @@
 import android.health.connect.datatypes.BasalMetabolicRateRecord
 import android.health.connect.datatypes.HeartRateRecord
 import android.health.connect.datatypes.StepsRecord
-import android.healthconnect.cts.TestUtils.verifyDeleteRecords
+import android.healthconnect.cts.utils.TestUtils.verifyDeleteRecords
 import android.healthconnect.cts.lib.ActivityLauncher.launchDataActivity
-import android.healthconnect.cts.lib.TestUtils.insertRecordAs
+import android.healthconnect.cts.lib.MultiAppTestUtils.insertRecordAs
 import android.healthconnect.cts.lib.UiTestUtils.clickOnText
 import android.healthconnect.cts.lib.UiTestUtils.waitDisplayed
 import androidx.test.uiautomator.By
diff --git a/tests/cts/utils/HealthConnectTestUtils/Android.bp b/tests/cts/utils/HealthConnectTestUtils/Android.bp
new file mode 100644
index 0000000..23a822c
--- /dev/null
+++ b/tests/cts/utils/HealthConnectTestUtils/Android.bp
@@ -0,0 +1,33 @@
+  // 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 {
+      default_applicable_licenses: ["Android-Apache-2.0"],
+  }
+
+  java_library {
+      name: "cts-healthconnect-utils",
+      srcs: [
+            "src/**/*.java",
+            "src/**/*.kt",
+            ":healthfitness-permissions-testapp-srcs",
+      ],
+      static_libs: [
+                   "androidx.appcompat_appcompat",
+                   "androidx.test.rules",
+                    "cts-install-lib",
+                    "platform-test-annotations",
+      ],
+      sdk_version: "test_current"
+  }
diff --git a/tests/cts/utils/HealthConnectTestUtils/src/android/healthconnect/cts/utils/TestReceiver.java b/tests/cts/utils/HealthConnectTestUtils/src/android/healthconnect/cts/utils/TestReceiver.java
new file mode 100644
index 0000000..2b77ab4
--- /dev/null
+++ b/tests/cts/utils/HealthConnectTestUtils/src/android/healthconnect/cts/utils/TestReceiver.java
@@ -0,0 +1,91 @@
+/*
+ * 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.utils;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Receives responses from test apps, e.g. from {@link
+ * android.healthconnect.test.app.TestAppReceiver}.
+ */
+public class TestReceiver extends BroadcastReceiver {
+
+    private static final String ACTION_SUCCESS = "action.SUCCESS";
+    private static final String ACTION_ERROR = "action.ERROR";
+    private static final String EXTRA_ERROR_CODE = "extra.ERROR_CODE";
+    private static final String EXTRA_ERROR_MESSAGE = "extra.ERROR_MESSAGE";
+
+    private static CountDownLatch sLatch = new CountDownLatch(1);
+    private static Bundle sResult = null;
+    private static Integer sErrorCode = null;
+    private static String sErrorMessage = null;
+
+    public static Bundle getResult() {
+        await();
+        return sResult;
+    }
+
+    public static Integer getErrorCode() {
+        await();
+        return sErrorCode;
+    }
+
+    public static String getErrorMessage() {
+        await();
+        return sErrorMessage;
+    }
+
+    private static void await() {
+        try {
+            if (!sLatch.await(10, TimeUnit.SECONDS)) {
+                throw new RuntimeException("Timeout waiting for response");
+            }
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static void reset() {
+        sResult = null;
+        sErrorCode = null;
+        sErrorMessage = null;
+        sLatch = new CountDownLatch(1);
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        switch (intent.getAction()) {
+            case ACTION_SUCCESS:
+                sResult = intent.getExtras();
+                sLatch.countDown();
+                break;
+            case ACTION_ERROR:
+                sErrorCode = intent.getIntExtra(EXTRA_ERROR_CODE, 0);
+                sErrorMessage = intent.getStringExtra(EXTRA_ERROR_MESSAGE);
+                sLatch.countDown();
+                break;
+            default:
+                throw new IllegalStateException("Unsupported action: " + intent.getAction());
+        }
+    }
+}
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
new file mode 100644
index 0000000..d3109af
--- /dev/null
+++ b/tests/cts/utils/HealthConnectTestUtils/src/android/healthconnect/cts/utils/TestUtils.java
@@ -0,0 +1,1473 @@
+/*
+ * 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.utils;
+
+import static android.content.pm.PackageManager.GET_PERMISSIONS;
+import static android.health.connect.HealthDataCategory.ACTIVITY;
+import static android.health.connect.HealthDataCategory.BODY_MEASUREMENTS;
+import static android.health.connect.HealthDataCategory.CYCLE_TRACKING;
+import static android.health.connect.HealthDataCategory.NUTRITION;
+import static android.health.connect.HealthDataCategory.SLEEP;
+import static android.health.connect.HealthDataCategory.VITALS;
+import static android.health.connect.HealthPermissionCategory.BASAL_METABOLIC_RATE;
+import static android.health.connect.HealthPermissionCategory.EXERCISE;
+import static android.health.connect.HealthPermissionCategory.HEART_RATE;
+import static android.health.connect.HealthPermissionCategory.STEPS;
+import static android.health.connect.datatypes.Metadata.RECORDING_METHOD_ACTIVELY_RECORDED;
+import static android.health.connect.datatypes.RecordTypeIdentifier.RECORD_TYPE_BASAL_METABOLIC_RATE;
+import static android.health.connect.datatypes.RecordTypeIdentifier.RECORD_TYPE_HEART_RATE;
+import static android.health.connect.datatypes.RecordTypeIdentifier.RECORD_TYPE_STEPS;
+import static android.healthconnect.test.app.TestAppReceiver.EXTRA_SENDER_PACKAGE_NAME;
+
+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.Objects.requireNonNull;
+
+import android.app.UiAutomation;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.health.connect.AggregateRecordsGroupedByDurationResponse;
+import android.health.connect.AggregateRecordsGroupedByPeriodResponse;
+import android.health.connect.AggregateRecordsRequest;
+import android.health.connect.AggregateRecordsResponse;
+import android.health.connect.ApplicationInfoResponse;
+import android.health.connect.DeleteUsingFiltersRequest;
+import android.health.connect.FetchDataOriginsPriorityOrderResponse;
+import android.health.connect.HealthConnectDataState;
+import android.health.connect.HealthConnectException;
+import android.health.connect.HealthConnectManager;
+import android.health.connect.HealthPermissionCategory;
+import android.health.connect.HealthPermissions;
+import android.health.connect.InsertRecordsResponse;
+import android.health.connect.ReadRecordsRequest;
+import android.health.connect.ReadRecordsRequestUsingIds;
+import android.health.connect.ReadRecordsResponse;
+import android.health.connect.RecordIdFilter;
+import android.health.connect.RecordTypeInfoResponse;
+import android.health.connect.TimeInstantRangeFilter;
+import android.health.connect.UpdateDataOriginPriorityOrderRequest;
+import android.health.connect.accesslog.AccessLog;
+import android.health.connect.changelog.ChangeLogTokenRequest;
+import android.health.connect.changelog.ChangeLogTokenResponse;
+import android.health.connect.changelog.ChangeLogsRequest;
+import android.health.connect.changelog.ChangeLogsResponse;
+import android.health.connect.datatypes.ActiveCaloriesBurnedRecord;
+import android.health.connect.datatypes.AppInfo;
+import android.health.connect.datatypes.BasalBodyTemperatureRecord;
+import android.health.connect.datatypes.BasalMetabolicRateRecord;
+import android.health.connect.datatypes.BloodGlucoseRecord;
+import android.health.connect.datatypes.BloodPressureRecord;
+import android.health.connect.datatypes.BodyFatRecord;
+import android.health.connect.datatypes.BodyTemperatureRecord;
+import android.health.connect.datatypes.BodyWaterMassRecord;
+import android.health.connect.datatypes.BoneMassRecord;
+import android.health.connect.datatypes.CervicalMucusRecord;
+import android.health.connect.datatypes.CyclingPedalingCadenceRecord;
+import android.health.connect.datatypes.DataOrigin;
+import android.health.connect.datatypes.Device;
+import android.health.connect.datatypes.DistanceRecord;
+import android.health.connect.datatypes.ElevationGainedRecord;
+import android.health.connect.datatypes.ExerciseLap;
+import android.health.connect.datatypes.ExerciseRoute;
+import android.health.connect.datatypes.ExerciseSegment;
+import android.health.connect.datatypes.ExerciseSegmentType;
+import android.health.connect.datatypes.ExerciseSessionRecord;
+import android.health.connect.datatypes.ExerciseSessionType;
+import android.health.connect.datatypes.FloorsClimbedRecord;
+import android.health.connect.datatypes.HeartRateRecord;
+import android.health.connect.datatypes.HeartRateVariabilityRmssdRecord;
+import android.health.connect.datatypes.HeightRecord;
+import android.health.connect.datatypes.HydrationRecord;
+import android.health.connect.datatypes.IntermenstrualBleedingRecord;
+import android.health.connect.datatypes.LeanBodyMassRecord;
+import android.health.connect.datatypes.MenstruationFlowRecord;
+import android.health.connect.datatypes.MenstruationPeriodRecord;
+import android.health.connect.datatypes.Metadata;
+import android.health.connect.datatypes.NutritionRecord;
+import android.health.connect.datatypes.OvulationTestRecord;
+import android.health.connect.datatypes.OxygenSaturationRecord;
+import android.health.connect.datatypes.PowerRecord;
+import android.health.connect.datatypes.Record;
+import android.health.connect.datatypes.RespiratoryRateRecord;
+import android.health.connect.datatypes.RestingHeartRateRecord;
+import android.health.connect.datatypes.SexualActivityRecord;
+import android.health.connect.datatypes.SleepSessionRecord;
+import android.health.connect.datatypes.SpeedRecord;
+import android.health.connect.datatypes.StepsCadenceRecord;
+import android.health.connect.datatypes.StepsRecord;
+import android.health.connect.datatypes.TotalCaloriesBurnedRecord;
+import android.health.connect.datatypes.Vo2MaxRecord;
+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.MigrationException;
+import android.healthconnect.test.app.TestAppReceiver;
+import android.os.Bundle;
+import android.os.OutcomeReceiver;
+import android.os.ParcelFileDescriptor;
+import android.os.UserHandle;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.SystemUtil;
+import com.android.cts.install.lib.TestApp;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Serializable;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.Period;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+
+public final class TestUtils {
+    public static final String MANAGE_HEALTH_PERMISSIONS =
+            HealthPermissions.MANAGE_HEALTH_PERMISSIONS;
+    public static final String READ_EXERCISE_ROUTE_PERMISSION =
+            "android.permission.health.READ_EXERCISE_ROUTE";
+    private static final String HEALTH_PERMISSION_PREFIX = "android.permission.health.";
+    public static final String MANAGE_HEALTH_DATA = HealthPermissions.MANAGE_HEALTH_DATA_PERMISSION;
+    public static final Instant SESSION_START_TIME = Instant.now().minus(10, ChronoUnit.DAYS);
+    public static final Instant SESSION_END_TIME =
+            Instant.now().minus(10, ChronoUnit.DAYS).plus(1, ChronoUnit.HOURS);
+    private static final String TAG = "HCTestUtils";
+    private static final int TIMEOUT_SECONDS = 5;
+
+    private static final String PKG_TEST_APP = "android.healthconnect.test.app";
+    private static final String TEST_APP_RECEIVER =
+            PKG_TEST_APP + "." + TestAppReceiver.class.getSimpleName();
+
+    public static boolean isHardwareAutomotive() {
+        return hasSystemFeature(AUTOMOTIVE_FEATURE);
+    }
+
+    public static ChangeLogTokenResponse getChangeLogToken(ChangeLogTokenRequest request)
+            throws InterruptedException {
+        return getChangeLogToken(request, ApplicationProvider.getApplicationContext());
+    }
+
+    public static ChangeLogTokenResponse getChangeLogToken(
+            ChangeLogTokenRequest request, Context context) throws InterruptedException {
+        HealthConnectReceiver<ChangeLogTokenResponse> receiver = new HealthConnectReceiver<>();
+        getHealthConnectManager(context)
+                .getChangeLogToken(request, Executors.newSingleThreadExecutor(), receiver);
+        return receiver.getResponse();
+    }
+
+    public static String insertRecordAndGetId(Record record) throws InterruptedException {
+        return insertRecords(Collections.singletonList(record)).get(0).getMetadata().getId();
+    }
+
+    public static String insertRecordAndGetId(Record record, Context context)
+            throws InterruptedException {
+        return insertRecords(Collections.singletonList(record), context)
+                .get(0)
+                .getMetadata()
+                .getId();
+    }
+
+    /**
+     * Inserts records to the database.
+     *
+     * @param records records to insert
+     * @return inserted records
+     */
+    public static List<Record> insertRecords(List<Record> records) throws InterruptedException {
+        return insertRecords(records, ApplicationProvider.getApplicationContext());
+    }
+
+    public static List<Record> insertRecords(List<Record> records, Context context)
+            throws InterruptedException {
+        HealthConnectReceiver<InsertRecordsResponse> receiver = new HealthConnectReceiver<>();
+        getHealthConnectManager(context)
+                .insertRecords(records, Executors.newSingleThreadExecutor(), receiver);
+        List<Record> returnedRecords = receiver.getResponse().getRecords();
+        assertThat(returnedRecords).hasSize(records.size());
+        return returnedRecords;
+    }
+
+    public static List<RecordTypeAndRecordIds> insertRecordsAndGetIds(
+            List<Record> records, Context context) throws InterruptedException {
+        List<Record> insertedRecords = insertRecords(records, context);
+
+        Map<String, List<String>> recordTypeToRecordIdsMap = new HashMap<>();
+        for (Record record : insertedRecords) {
+            recordTypeToRecordIdsMap.putIfAbsent(record.getClass().getName(), new ArrayList<>());
+            recordTypeToRecordIdsMap
+                    .get(record.getClass().getName())
+                    .add(record.getMetadata().getId());
+        }
+
+        List<RecordTypeAndRecordIds> recordTypeAndRecordIdsList = new ArrayList<>();
+        for (String recordType : recordTypeToRecordIdsMap.keySet()) {
+            recordTypeAndRecordIdsList.add(
+                    new RecordTypeAndRecordIds(
+                            recordType, recordTypeToRecordIdsMap.get(recordType)));
+        }
+
+        return recordTypeAndRecordIdsList;
+    }
+
+    public static void updateRecords(List<Record> records) throws InterruptedException {
+        updateRecords(records, ApplicationProvider.getApplicationContext());
+    }
+
+    public static void updateRecords(List<Record> records, Context context)
+            throws InterruptedException {
+        HealthConnectReceiver<Void> receiver = new HealthConnectReceiver<>();
+        getHealthConnectManager(context)
+                .updateRecords(records, Executors.newSingleThreadExecutor(), receiver);
+        receiver.verifyNoExceptionOrThrow();
+    }
+
+    public static ChangeLogsResponse getChangeLogs(ChangeLogsRequest changeLogsRequest)
+            throws InterruptedException {
+        return getChangeLogs(changeLogsRequest, ApplicationProvider.getApplicationContext());
+    }
+
+    public static ChangeLogsResponse getChangeLogs(
+            ChangeLogsRequest changeLogsRequest, Context context) throws InterruptedException {
+        HealthConnectReceiver<ChangeLogsResponse> receiver = new HealthConnectReceiver<>();
+        getHealthConnectManager(context)
+                .getChangeLogs(changeLogsRequest, Executors.newSingleThreadExecutor(), receiver);
+        return receiver.getResponse();
+    }
+
+    public static Device buildDevice() {
+        return new Device.Builder()
+                .setManufacturer("google")
+                .setModel("Pixel4a")
+                .setType(2)
+                .build();
+    }
+
+    private static Metadata buildSessionMetadata(String packageName, double clientId) {
+        Device device =
+                new Device.Builder().setManufacturer("google").setModel("Pixel").setType(1).build();
+        DataOrigin dataOrigin = new DataOrigin.Builder().setPackageName(packageName).build();
+        return new Metadata.Builder()
+                .setDevice(device)
+                .setDataOrigin(dataOrigin)
+                .setClientRecordId(String.valueOf(clientId))
+                .build();
+    }
+
+    public static List<Record> getTestRecords() {
+        return Arrays.asList(
+                getStepsRecord(),
+                getHeartRateRecord(),
+                getBasalMetabolicRateRecord(),
+                buildExerciseSession());
+    }
+
+    public static List<Record> getTestRecords(String packageName) {
+        double clientId = Math.random();
+        return getTestRecords(packageName, clientId);
+    }
+
+    public static List<Record> getTestRecords(String packageName, Double clientId) {
+        return Arrays.asList(
+                getExerciseSessionRecord(packageName, clientId, /* withRoute= */ true),
+                getStepsRecord(packageName, clientId),
+                getHeartRateRecord(packageName, clientId),
+                getBasalMetabolicRateRecord(packageName, clientId));
+    }
+
+    public static List<RecordAndIdentifier> getRecordsAndIdentifiers() {
+        return Arrays.asList(
+                new RecordAndIdentifier(RECORD_TYPE_STEPS, getStepsRecord()),
+                new RecordAndIdentifier(RECORD_TYPE_HEART_RATE, getHeartRateRecord()),
+                new RecordAndIdentifier(
+                        RECORD_TYPE_BASAL_METABOLIC_RATE, getBasalMetabolicRateRecord()));
+    }
+
+    public static ExerciseRoute.Location buildLocationTimePoint(Instant startTime) {
+        return new ExerciseRoute.Location.Builder(
+                        Instant.ofEpochMilli(
+                                (long) (startTime.toEpochMilli() + 10 + Math.random() * 50)),
+                        Math.random() * 50,
+                        Math.random() * 50)
+                .build();
+    }
+
+    public static ExerciseRoute buildExerciseRoute() {
+        return new ExerciseRoute(
+                List.of(
+                        buildLocationTimePoint(SESSION_START_TIME),
+                        buildLocationTimePoint(SESSION_START_TIME),
+                        buildLocationTimePoint(SESSION_START_TIME)));
+    }
+
+    public static StepsRecord getStepsRecord() {
+        double clientId = Math.random();
+        String packageName = ApplicationProvider.getApplicationContext().getPackageName();
+        return getStepsRecord(packageName, clientId);
+    }
+
+    public static StepsRecord getStepsRecord(String packageName, double clientId) {
+        Device device =
+                new Device.Builder().setManufacturer("google").setModel("Pixel").setType(1).build();
+        DataOrigin dataOrigin = new DataOrigin.Builder().setPackageName(packageName).build();
+        return new StepsRecord.Builder(
+                        new Metadata.Builder()
+                                .setDevice(device)
+                                .setDataOrigin(dataOrigin)
+                                .setClientRecordId("SR" + clientId)
+                                .build(),
+                        Instant.now(),
+                        Instant.now().plusMillis(1000),
+                        10)
+                .build();
+    }
+
+    public static StepsRecord getStepsRecord(String id) {
+        Context context = ApplicationProvider.getApplicationContext();
+        Device device =
+                new Device.Builder().setManufacturer("google").setModel("Pixel").setType(1).build();
+        DataOrigin dataOrigin =
+                new DataOrigin.Builder().setPackageName(context.getPackageName()).build();
+        return new StepsRecord.Builder(
+                        new Metadata.Builder()
+                                .setDevice(device)
+                                .setId(id)
+                                .setDataOrigin(dataOrigin)
+                                .build(),
+                        Instant.now(),
+                        Instant.now().plusMillis(1000),
+                        10)
+                .build();
+    }
+
+    public static HeartRateRecord getHeartRateRecord() {
+        String packageName = ApplicationProvider.getApplicationContext().getPackageName();
+        double clientId = Math.random();
+        return getHeartRateRecord(packageName, clientId);
+    }
+
+    public static HeartRateRecord getHeartRateRecord(String packageName, double clientId) {
+        HeartRateRecord.HeartRateSample heartRateSample =
+                new HeartRateRecord.HeartRateSample(72, Instant.now().plusMillis(100));
+        ArrayList<HeartRateRecord.HeartRateSample> heartRateSamples = new ArrayList<>();
+        heartRateSamples.add(heartRateSample);
+        heartRateSamples.add(heartRateSample);
+        Device device =
+                new Device.Builder().setManufacturer("google").setModel("Pixel").setType(1).build();
+        DataOrigin dataOrigin = new DataOrigin.Builder().setPackageName(packageName).build();
+
+        return new HeartRateRecord.Builder(
+                        new Metadata.Builder()
+                                .setDevice(device)
+                                .setDataOrigin(dataOrigin)
+                                .setClientRecordId("HR" + clientId)
+                                .build(),
+                        Instant.now(),
+                        Instant.now().plusMillis(500),
+                        heartRateSamples)
+                .build();
+    }
+
+    public static HeartRateRecord getHeartRateRecord(int heartRate) {
+        HeartRateRecord.HeartRateSample heartRateSample =
+                new HeartRateRecord.HeartRateSample(heartRate, Instant.now().plusMillis(100));
+        ArrayList<HeartRateRecord.HeartRateSample> heartRateSamples = new ArrayList<>();
+        heartRateSamples.add(heartRateSample);
+        heartRateSamples.add(heartRateSample);
+
+        return new HeartRateRecord.Builder(
+                        new Metadata.Builder().build(),
+                        Instant.now(),
+                        Instant.now().plusMillis(500),
+                        heartRateSamples)
+                .build();
+    }
+
+    public static HeartRateRecord getHeartRateRecord(int heartRate, Instant instant) {
+        HeartRateRecord.HeartRateSample heartRateSample =
+                new HeartRateRecord.HeartRateSample(heartRate, instant);
+        ArrayList<HeartRateRecord.HeartRateSample> heartRateSamples = new ArrayList<>();
+        heartRateSamples.add(heartRateSample);
+        heartRateSamples.add(heartRateSample);
+
+        return new HeartRateRecord.Builder(
+                        new Metadata.Builder().build(),
+                        instant,
+                        instant.plusMillis(1000),
+                        heartRateSamples)
+                .build();
+    }
+
+    public static BasalMetabolicRateRecord getBasalMetabolicRateRecord() {
+        String packageName = ApplicationProvider.getApplicationContext().getPackageName();
+        double clientId = Math.random();
+
+        return getBasalMetabolicRateRecord(packageName, clientId);
+    }
+
+    public static BasalMetabolicRateRecord getBasalMetabolicRateRecord(
+            String packageName, double clientId) {
+        Device device =
+                new Device.Builder()
+                        .setManufacturer("google")
+                        .setModel("Pixel4a")
+                        .setType(2)
+                        .build();
+        DataOrigin dataOrigin = new DataOrigin.Builder().setPackageName(packageName).build();
+        return new BasalMetabolicRateRecord.Builder(
+                        new Metadata.Builder()
+                                .setDevice(device)
+                                .setDataOrigin(dataOrigin)
+                                .setClientRecordId("BMR" + clientId)
+                                .build(),
+                        Instant.now(),
+                        Power.fromWatts(100.0))
+                .build();
+    }
+
+    public static ExerciseSessionRecord getExerciseSessionRecord(
+            String packageName, double clientId, boolean withRoute) {
+        Instant startTime = Instant.now().minusSeconds(3000).truncatedTo(ChronoUnit.MILLIS);
+        Instant endTime = Instant.now();
+        ExerciseSessionRecord.Builder builder =
+                new ExerciseSessionRecord.Builder(
+                                buildSessionMetadata(packageName, clientId),
+                                startTime,
+                                endTime,
+                                ExerciseSessionType.EXERCISE_SESSION_TYPE_OTHER_WORKOUT)
+                        .setEndZoneOffset(ZoneOffset.MAX)
+                        .setStartZoneOffset(ZoneOffset.MIN)
+                        .setNotes("notes")
+                        .setTitle("title");
+
+        if (withRoute) {
+            builder.setRoute(
+                    new ExerciseRoute(
+                            List.of(
+                                    new ExerciseRoute.Location.Builder(startTime, 50., 50.).build(),
+                                    new ExerciseRoute.Location.Builder(
+                                                    startTime.plusSeconds(2), 51., 51.)
+                                            .build())));
+        }
+        return builder.build();
+    }
+
+    public static StepsRecord buildStepsRecord(
+            String startTime, String endTime, int stepsCount, String packageName) {
+        Device device =
+                new Device.Builder().setManufacturer("google").setModel("Pixel").setType(1).build();
+        DataOrigin dataOrigin = new DataOrigin.Builder().setPackageName(packageName).build();
+        return new StepsRecord.Builder(
+                        new Metadata.Builder().setDevice(device).setDataOrigin(dataOrigin).build(),
+                        getInstantTime(startTime),
+                        getInstantTime(endTime),
+                        stepsCount)
+                .build();
+    }
+
+    public static ExerciseSessionRecord buildExerciseSession(
+            String sessionStartTime, String sessionEndTime, Context context) {
+        return new ExerciseSessionRecord.Builder(
+                        new Metadata.Builder()
+                                .setDataOrigin(
+                                        new DataOrigin.Builder()
+                                                .setPackageName(context.getPackageName())
+                                                .build())
+                                .setId("ExerciseSession" + Math.random())
+                                .setClientRecordId("ExerciseSessionClient" + Math.random())
+                                .build(),
+                        getInstantTime(sessionStartTime),
+                        getInstantTime(sessionEndTime),
+                        ExerciseSessionType.EXERCISE_SESSION_TYPE_FOOTBALL_AMERICAN)
+                .build();
+    }
+
+    public static ExerciseSessionRecord buildExerciseSession(
+            String sessionStartTime,
+            String sessionEndTime,
+            String pauseStart,
+            String pauseEnd,
+            Context context) {
+        List<ExerciseSegment> segmentList =
+                List.of(
+                        new ExerciseSegment.Builder(
+                                        getInstantTime(sessionStartTime),
+                                        getInstantTime(pauseStart),
+                                        ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_OTHER_WORKOUT)
+                                .setRepetitionsCount(10)
+                                .build(),
+                        new ExerciseSegment.Builder(
+                                        getInstantTime(pauseStart),
+                                        getInstantTime(pauseEnd),
+                                        ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_PAUSE)
+                                .build());
+
+        if (getInstantTime(sessionEndTime).compareTo(getInstantTime(pauseEnd)) > 0) {
+            segmentList.add(
+                    new ExerciseSegment.Builder(
+                                    getInstantTime(pauseEnd),
+                                    getInstantTime(sessionEndTime),
+                                    ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_OTHER_WORKOUT)
+                            .setRepetitionsCount(10)
+                            .build());
+        }
+
+        return new ExerciseSessionRecord.Builder(
+                        new Metadata.Builder()
+                                .setDataOrigin(
+                                        new DataOrigin.Builder()
+                                                .setPackageName(context.getPackageName())
+                                                .build())
+                                .setId("ExerciseSession" + Math.random())
+                                .setClientRecordId("ExerciseSessionClient" + Math.random())
+                                .build(),
+                        getInstantTime(sessionStartTime),
+                        getInstantTime(sessionEndTime),
+                        ExerciseSessionType.EXERCISE_SESSION_TYPE_FOOTBALL_AMERICAN)
+                .setSegments(segmentList)
+                .build();
+    }
+
+    public static Instant getInstantTime(String time) {
+        return LocalDateTime.parse(
+                        time + " Mon 5/15/2023",
+                        DateTimeFormatter.ofPattern("hh:mm a EEE M/d/uuuu", Locale.US))
+                .atZone(ZoneId.of("America/Toronto"))
+                .toInstant();
+    }
+
+    public static <T> AggregateRecordsResponse<T> getAggregateResponse(
+            AggregateRecordsRequest<T> request) throws InterruptedException {
+        HealthConnectReceiver<AggregateRecordsResponse<T>> receiver =
+                new HealthConnectReceiver<AggregateRecordsResponse<T>>();
+        getHealthConnectManager().aggregate(request, Executors.newSingleThreadExecutor(), receiver);
+        return receiver.getResponse();
+    }
+
+    public static <T> AggregateRecordsResponse<T> getAggregateResponse(
+            AggregateRecordsRequest<T> request, List<Record> recordsToInsert)
+            throws InterruptedException {
+        if (recordsToInsert != null) {
+            insertRecords(recordsToInsert);
+        }
+
+        HealthConnectReceiver<AggregateRecordsResponse<T>> receiver = new HealthConnectReceiver<>();
+        getHealthConnectManager().aggregate(request, Executors.newSingleThreadExecutor(), receiver);
+        return receiver.getResponse();
+    }
+
+    public static <T>
+            List<AggregateRecordsGroupedByDurationResponse<T>> getAggregateResponseGroupByDuration(
+                    AggregateRecordsRequest<T> request, Duration duration)
+                    throws InterruptedException {
+        HealthConnectReceiver<List<AggregateRecordsGroupedByDurationResponse<T>>> receiver =
+                new HealthConnectReceiver<>();
+        getHealthConnectManager()
+                .aggregateGroupByDuration(
+                        request, duration, Executors.newSingleThreadExecutor(), receiver);
+        return receiver.getResponse();
+    }
+
+    public static <T>
+            List<AggregateRecordsGroupedByPeriodResponse<T>> getAggregateResponseGroupByPeriod(
+                    AggregateRecordsRequest<T> request, Period period) throws InterruptedException {
+        HealthConnectReceiver<List<AggregateRecordsGroupedByPeriodResponse<T>>> receiver =
+                new HealthConnectReceiver<>();
+        getHealthConnectManager()
+                .aggregateGroupByPeriod(
+                        request, period, Executors.newSingleThreadExecutor(), receiver);
+        return receiver.getResponse();
+    }
+
+    public static <T extends Record> List<T> readRecords(ReadRecordsRequest<T> request)
+            throws InterruptedException {
+        return readRecords(request, ApplicationProvider.getApplicationContext());
+    }
+
+    public static <T extends Record> List<T> readRecords(
+            ReadRecordsRequest<T> request, Context context) throws InterruptedException {
+        assertThat(request.getRecordType()).isNotNull();
+        HealthConnectReceiver<ReadRecordsResponse<T>> receiver = new HealthConnectReceiver<>();
+        getHealthConnectManager(context)
+                .readRecords(request, Executors.newSingleThreadExecutor(), receiver);
+        return receiver.getResponse().getRecords();
+    }
+
+    public static <T extends Record> void assertRecordNotFound(String uuid, Class<T> recordType)
+            throws InterruptedException {
+        assertThat(
+                        readRecords(
+                                new ReadRecordsRequestUsingIds.Builder<>(recordType)
+                                        .addId(uuid)
+                                        .build()))
+                .isEmpty();
+    }
+
+    public static <T extends Record> void assertRecordFound(String uuid, Class<T> recordType)
+            throws InterruptedException {
+        assertThat(
+                        readRecords(
+                                new ReadRecordsRequestUsingIds.Builder<>(recordType)
+                                        .addId(uuid)
+                                        .build()))
+                .isNotEmpty();
+    }
+
+    public static <T extends Record> ReadRecordsResponse<T> readRecordsWithPagination(
+            ReadRecordsRequest<T> request) throws InterruptedException {
+        HealthConnectReceiver<ReadRecordsResponse<T>> receiver = new HealthConnectReceiver<>();
+        getHealthConnectManager()
+                .readRecords(request, Executors.newSingleThreadExecutor(), receiver);
+        return receiver.getResponse();
+    }
+
+    public static void setAutoDeletePeriod(int period) throws InterruptedException {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA);
+        try {
+            HealthConnectReceiver<Void> receiver = new HealthConnectReceiver<>();
+            getHealthConnectManager()
+                    .setRecordRetentionPeriodInDays(
+                            period, Executors.newSingleThreadExecutor(), receiver);
+            receiver.verifyNoExceptionOrThrow();
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    public static void verifyDeleteRecords(DeleteUsingFiltersRequest request)
+            throws InterruptedException {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA);
+        try {
+            HealthConnectReceiver<Void> receiver = new HealthConnectReceiver<>();
+            getHealthConnectManager()
+                    .deleteRecords(request, Executors.newSingleThreadExecutor(), receiver);
+            receiver.verifyNoExceptionOrThrow();
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    public static void verifyDeleteRecords(List<RecordIdFilter> request)
+            throws InterruptedException {
+        verifyDeleteRecords(request, ApplicationProvider.getApplicationContext());
+    }
+
+    public static void verifyDeleteRecords(List<RecordIdFilter> request, Context context)
+            throws InterruptedException {
+        HealthConnectReceiver<Void> receiver = new HealthConnectReceiver<>();
+        getHealthConnectManager(context)
+                .deleteRecords(request, Executors.newSingleThreadExecutor(), receiver);
+        receiver.verifyNoExceptionOrThrow();
+    }
+
+    public static void verifyDeleteRecords(
+            Class<? extends Record> recordType, TimeInstantRangeFilter timeRangeFilter)
+            throws InterruptedException {
+        HealthConnectReceiver<Void> receiver = new HealthConnectReceiver<>();
+        getHealthConnectManager()
+                .deleteRecords(
+                        recordType, timeRangeFilter, Executors.newSingleThreadExecutor(), receiver);
+        receiver.verifyNoExceptionOrThrow();
+    }
+
+    public static void deleteRecords(List<Record> records) throws InterruptedException {
+        List<RecordIdFilter> recordIdFilters =
+                records.stream()
+                        .map(
+                                (record ->
+                                        RecordIdFilter.fromId(
+                                                record.getClass(), record.getMetadata().getId())))
+                        .collect(Collectors.toList());
+        verifyDeleteRecords(recordIdFilters);
+    }
+
+    public static List<AccessLog> queryAccessLogs() throws InterruptedException {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA);
+        try {
+            HealthConnectReceiver<List<AccessLog>> receiver = new HealthConnectReceiver<>();
+            getHealthConnectManager()
+                    .queryAccessLogs(Executors.newSingleThreadExecutor(), receiver);
+            return receiver.getResponse();
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    public static Map<Class<? extends Record>, RecordTypeInfoResponse> queryAllRecordTypesInfo()
+            throws InterruptedException, NullPointerException {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA);
+        try {
+            HealthConnectReceiver<Map<Class<? extends Record>, RecordTypeInfoResponse>> receiver =
+                    new HealthConnectReceiver<>();
+            getHealthConnectManager()
+                    .queryAllRecordTypesInfo(Executors.newSingleThreadExecutor(), receiver);
+            return receiver.getResponse();
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    public static List<LocalDate> getActivityDates(List<Class<? extends Record>> recordTypes)
+            throws InterruptedException {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA);
+        try {
+            HealthConnectReceiver<List<LocalDate>> receiver = new HealthConnectReceiver<>();
+            getHealthConnectManager()
+                    .queryActivityDates(recordTypes, Executors.newSingleThreadExecutor(), receiver);
+            return receiver.getResponse();
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    public static ExerciseSessionRecord buildExerciseSession() {
+        return buildExerciseSession(buildExerciseRoute(), "Morning training", "rain");
+    }
+
+    public static SleepSessionRecord buildSleepSession() {
+        return new SleepSessionRecord.Builder(
+                        generateMetadata(), SESSION_START_TIME, SESSION_END_TIME)
+                .setNotes("warm")
+                .setTitle("Afternoon nap")
+                .setStages(
+                        List.of(
+                                new SleepSessionRecord.Stage(
+                                        SESSION_START_TIME,
+                                        SESSION_START_TIME.plusSeconds(300),
+                                        SleepSessionRecord.StageType.STAGE_TYPE_SLEEPING_LIGHT),
+                                new SleepSessionRecord.Stage(
+                                        SESSION_START_TIME.plusSeconds(300),
+                                        SESSION_START_TIME.plusSeconds(600),
+                                        SleepSessionRecord.StageType.STAGE_TYPE_SLEEPING_REM),
+                                new SleepSessionRecord.Stage(
+                                        SESSION_START_TIME.plusSeconds(900),
+                                        SESSION_START_TIME.plusSeconds(1200),
+                                        SleepSessionRecord.StageType.STAGE_TYPE_SLEEPING_DEEP)))
+                .build();
+    }
+
+    public static void startMigration() throws InterruptedException {
+        MigrationReceiver receiver = new MigrationReceiver();
+        getHealthConnectManager().startMigration(Executors.newSingleThreadExecutor(), receiver);
+        receiver.verifyNoExceptionOrThrow();
+    }
+
+    public static void finishMigration() throws InterruptedException {
+        MigrationReceiver receiver = new MigrationReceiver();
+        getHealthConnectManager().finishMigration(Executors.newSingleThreadExecutor(), receiver);
+        receiver.verifyNoExceptionOrThrow();
+    }
+
+    public static void insertMinDataMigrationSdkExtensionVersion(int version)
+            throws InterruptedException {
+        MigrationReceiver receiver = new MigrationReceiver();
+        getHealthConnectManager()
+                .insertMinDataMigrationSdkExtensionVersion(
+                        version, Executors.newSingleThreadExecutor(), receiver);
+        receiver.verifyNoExceptionOrThrow();
+    }
+
+    public static void deleteAllStagedRemoteData() {
+        HealthConnectManager service = getHealthConnectManager();
+        runWithShellPermissionIdentity(
+                () ->
+                        // TODO(b/241542162): Avoid reflection once TestApi can be called from CTS
+                        service.getClass().getMethod("deleteAllStagedRemoteData").invoke(service),
+                "android.permission.DELETE_STAGED_HEALTH_CONNECT_REMOTE_DATA");
+    }
+
+    public static int getHealthConnectDataMigrationState() throws InterruptedException {
+        HealthConnectReceiver<HealthConnectDataState> receiver = new HealthConnectReceiver<>();
+        getHealthConnectManager()
+                .getHealthConnectDataState(Executors.newSingleThreadExecutor(), receiver);
+        return receiver.getResponse().getDataMigrationState();
+    }
+
+    public static int getHealthConnectDataRestoreState() throws InterruptedException {
+        HealthConnectReceiver<HealthConnectDataState> receiver = new HealthConnectReceiver<>();
+        runWithShellPermissionIdentity(
+                () ->
+                        getHealthConnectManager()
+                                .getHealthConnectDataState(
+                                        Executors.newSingleThreadExecutor(), receiver),
+                MANAGE_HEALTH_DATA);
+        return receiver.getResponse().getDataRestoreState();
+    }
+
+    public static List<AppInfo> getApplicationInfo() throws InterruptedException {
+        HealthConnectReceiver<ApplicationInfoResponse> receiver = new HealthConnectReceiver<>();
+        getHealthConnectManager()
+                .getContributorApplicationsInfo(Executors.newSingleThreadExecutor(), receiver);
+        return receiver.getResponse().getApplicationInfoList();
+    }
+
+    public static <T extends Record> T getRecordById(List<T> list, String id) {
+        for (T record : list) {
+            if (record.getMetadata().getId().equals(id)) {
+                return record;
+            }
+        }
+
+        throw new AssertionError("Record not found with id: " + id);
+    }
+
+    public static Metadata generateMetadata() {
+        Context context = ApplicationProvider.getApplicationContext();
+        return new Metadata.Builder()
+                .setDevice(buildDevice())
+                .setId(UUID.randomUUID().toString())
+                .setClientRecordId("clientRecordId" + Math.random())
+                .setDataOrigin(
+                        new DataOrigin.Builder().setPackageName(context.getPackageName()).build())
+                .setDevice(buildDevice())
+                .setRecordingMethod(Metadata.RECORDING_METHOD_UNKNOWN)
+                .build();
+    }
+
+    public static HeartRateRecord getHugeHeartRateRecord() {
+        Device device =
+                new Device.Builder()
+                        .setManufacturer("google")
+                        .setModel("Pixel4a")
+                        .setType(2)
+                        .build();
+        DataOrigin dataOrigin =
+                new DataOrigin.Builder().setPackageName("android.healthconnect.cts").build();
+        Metadata.Builder testMetadataBuilder = new Metadata.Builder();
+        testMetadataBuilder.setDevice(device).setDataOrigin(dataOrigin);
+        testMetadataBuilder.setClientRecordId("HRR" + Math.random());
+        testMetadataBuilder.setRecordingMethod(Metadata.RECORDING_METHOD_ACTIVELY_RECORDED);
+
+        HeartRateRecord.HeartRateSample heartRateRecord =
+                new HeartRateRecord.HeartRateSample(10, Instant.now().plusMillis(100));
+        ArrayList<HeartRateRecord.HeartRateSample> heartRateRecords =
+                new ArrayList<>(Collections.nCopies(85000, heartRateRecord));
+
+        return new HeartRateRecord.Builder(
+                        testMetadataBuilder.build(),
+                        Instant.now(),
+                        Instant.now().plusMillis(500),
+                        heartRateRecords)
+                .build();
+    }
+
+    public static StepsRecord getCompleteStepsRecord() {
+        Device device =
+                new Device.Builder().setManufacturer("google").setModel("Pixel").setType(1).build();
+        DataOrigin dataOrigin =
+                new DataOrigin.Builder().setPackageName("android.healthconnect.cts").build();
+
+        Metadata.Builder testMetadataBuilder = new Metadata.Builder();
+        testMetadataBuilder.setDevice(device).setDataOrigin(dataOrigin);
+        testMetadataBuilder.setClientRecordId("SR" + Math.random());
+        testMetadataBuilder.setRecordingMethod(RECORDING_METHOD_ACTIVELY_RECORDED);
+        Metadata testMetaData = testMetadataBuilder.build();
+        assertThat(testMetaData.getRecordingMethod()).isEqualTo(RECORDING_METHOD_ACTIVELY_RECORDED);
+        return new StepsRecord.Builder(
+                        testMetaData, Instant.now(), Instant.now().plusMillis(1000), 10)
+                .build();
+    }
+
+    public static StepsRecord getStepsRecord_update(
+            Record record, String id, String clientRecordId) {
+        Metadata metadata = record.getMetadata();
+        Metadata metadataWithId =
+                new Metadata.Builder()
+                        .setId(id)
+                        .setClientRecordId(clientRecordId)
+                        .setClientRecordVersion(metadata.getClientRecordVersion())
+                        .setDataOrigin(metadata.getDataOrigin())
+                        .setDevice(metadata.getDevice())
+                        .setLastModifiedTime(metadata.getLastModifiedTime())
+                        .build();
+        return new StepsRecord.Builder(
+                        metadataWithId, Instant.now(), Instant.now().plusMillis(2000), 20)
+                .setStartZoneOffset(ZoneOffset.systemDefault().getRules().getOffset(Instant.now()))
+                .setEndZoneOffset(ZoneOffset.systemDefault().getRules().getOffset(Instant.now()))
+                .build();
+    }
+
+    private static ExerciseSessionRecord buildExerciseSession(
+            ExerciseRoute route, String title, String notes) {
+        return new ExerciseSessionRecord.Builder(
+                        generateMetadata(),
+                        SESSION_START_TIME,
+                        SESSION_END_TIME,
+                        ExerciseSessionType.EXERCISE_SESSION_TYPE_OTHER_WORKOUT)
+                .setRoute(route)
+                .setLaps(
+                        List.of(
+                                new ExerciseLap.Builder(
+                                                SESSION_START_TIME,
+                                                SESSION_START_TIME.plusSeconds(20))
+                                        .setLength(Length.fromMeters(10))
+                                        .build(),
+                                new ExerciseLap.Builder(
+                                                SESSION_END_TIME.minusSeconds(20), SESSION_END_TIME)
+                                        .build()))
+                .setSegments(
+                        List.of(
+                                new ExerciseSegment.Builder(
+                                                SESSION_START_TIME.plusSeconds(1),
+                                                SESSION_START_TIME.plusSeconds(10),
+                                                ExerciseSegmentType
+                                                        .EXERCISE_SEGMENT_TYPE_BENCH_PRESS)
+                                        .build(),
+                                new ExerciseSegment.Builder(
+                                                SESSION_START_TIME.plusSeconds(21),
+                                                SESSION_START_TIME.plusSeconds(124),
+                                                ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_BURPEE)
+                                        .setRepetitionsCount(15)
+                                        .build()))
+                .setEndZoneOffset(ZoneOffset.MAX)
+                .setStartZoneOffset(ZoneOffset.MIN)
+                .setNotes(notes)
+                .setTitle(title)
+                .build();
+    }
+
+    public static void populateAndResetExpectedResponseMap(
+            HashMap<Class<? extends Record>, RecordTypeInfoTestResponse> expectedResponseMap) {
+        expectedResponseMap.put(
+                ElevationGainedRecord.class,
+                new RecordTypeInfoTestResponse(
+                        ACTIVITY, HealthPermissionCategory.ELEVATION_GAINED, new ArrayList<>()));
+        expectedResponseMap.put(
+                OvulationTestRecord.class,
+                new RecordTypeInfoTestResponse(
+                        CYCLE_TRACKING,
+                        HealthPermissionCategory.OVULATION_TEST,
+                        new ArrayList<>()));
+        expectedResponseMap.put(
+                DistanceRecord.class,
+                new RecordTypeInfoTestResponse(
+                        ACTIVITY, HealthPermissionCategory.DISTANCE, new ArrayList<>()));
+        expectedResponseMap.put(
+                SpeedRecord.class,
+                new RecordTypeInfoTestResponse(
+                        ACTIVITY, HealthPermissionCategory.SPEED, new ArrayList<>()));
+
+        expectedResponseMap.put(
+                Vo2MaxRecord.class,
+                new RecordTypeInfoTestResponse(
+                        ACTIVITY, HealthPermissionCategory.VO2_MAX, new ArrayList<>()));
+        expectedResponseMap.put(
+                OxygenSaturationRecord.class,
+                new RecordTypeInfoTestResponse(
+                        VITALS, HealthPermissionCategory.OXYGEN_SATURATION, new ArrayList<>()));
+        expectedResponseMap.put(
+                TotalCaloriesBurnedRecord.class,
+                new RecordTypeInfoTestResponse(
+                        ACTIVITY,
+                        HealthPermissionCategory.TOTAL_CALORIES_BURNED,
+                        new ArrayList<>()));
+        expectedResponseMap.put(
+                HydrationRecord.class,
+                new RecordTypeInfoTestResponse(
+                        NUTRITION, HealthPermissionCategory.HYDRATION, new ArrayList<>()));
+        expectedResponseMap.put(
+                StepsRecord.class,
+                new RecordTypeInfoTestResponse(ACTIVITY, STEPS, new ArrayList<>()));
+        expectedResponseMap.put(
+                CervicalMucusRecord.class,
+                new RecordTypeInfoTestResponse(
+                        CYCLE_TRACKING,
+                        HealthPermissionCategory.CERVICAL_MUCUS,
+                        new ArrayList<>()));
+        expectedResponseMap.put(
+                ExerciseSessionRecord.class,
+                new RecordTypeInfoTestResponse(ACTIVITY, EXERCISE, new ArrayList<>()));
+        expectedResponseMap.put(
+                HeartRateRecord.class,
+                new RecordTypeInfoTestResponse(VITALS, HEART_RATE, new ArrayList<>()));
+        expectedResponseMap.put(
+                RespiratoryRateRecord.class,
+                new RecordTypeInfoTestResponse(
+                        VITALS, HealthPermissionCategory.RESPIRATORY_RATE, new ArrayList<>()));
+        expectedResponseMap.put(
+                BasalBodyTemperatureRecord.class,
+                new RecordTypeInfoTestResponse(
+                        VITALS,
+                        HealthPermissionCategory.BASAL_BODY_TEMPERATURE,
+                        new ArrayList<>()));
+        expectedResponseMap.put(
+                WheelchairPushesRecord.class,
+                new RecordTypeInfoTestResponse(
+                        ACTIVITY, HealthPermissionCategory.WHEELCHAIR_PUSHES, new ArrayList<>()));
+        expectedResponseMap.put(
+                PowerRecord.class,
+                new RecordTypeInfoTestResponse(
+                        ACTIVITY, HealthPermissionCategory.POWER, new ArrayList<>()));
+        expectedResponseMap.put(
+                BodyWaterMassRecord.class,
+                new RecordTypeInfoTestResponse(
+                        BODY_MEASUREMENTS,
+                        HealthPermissionCategory.BODY_WATER_MASS,
+                        new ArrayList<>()));
+        expectedResponseMap.put(
+                WeightRecord.class,
+                new RecordTypeInfoTestResponse(
+                        BODY_MEASUREMENTS, HealthPermissionCategory.WEIGHT, new ArrayList<>()));
+        expectedResponseMap.put(
+                BoneMassRecord.class,
+                new RecordTypeInfoTestResponse(
+                        BODY_MEASUREMENTS, HealthPermissionCategory.BONE_MASS, new ArrayList<>()));
+        expectedResponseMap.put(
+                RestingHeartRateRecord.class,
+                new RecordTypeInfoTestResponse(
+                        VITALS, HealthPermissionCategory.RESTING_HEART_RATE, new ArrayList<>()));
+        expectedResponseMap.put(
+                ActiveCaloriesBurnedRecord.class,
+                new RecordTypeInfoTestResponse(
+                        ACTIVITY,
+                        HealthPermissionCategory.ACTIVE_CALORIES_BURNED,
+                        new ArrayList<>()));
+        expectedResponseMap.put(
+                BodyFatRecord.class,
+                new RecordTypeInfoTestResponse(
+                        BODY_MEASUREMENTS, HealthPermissionCategory.BODY_FAT, new ArrayList<>()));
+        expectedResponseMap.put(
+                BodyTemperatureRecord.class,
+                new RecordTypeInfoTestResponse(
+                        VITALS, HealthPermissionCategory.BODY_TEMPERATURE, new ArrayList<>()));
+        expectedResponseMap.put(
+                NutritionRecord.class,
+                new RecordTypeInfoTestResponse(
+                        NUTRITION, HealthPermissionCategory.NUTRITION, new ArrayList<>()));
+        expectedResponseMap.put(
+                LeanBodyMassRecord.class,
+                new RecordTypeInfoTestResponse(
+                        BODY_MEASUREMENTS,
+                        HealthPermissionCategory.LEAN_BODY_MASS,
+                        new ArrayList<>()));
+        expectedResponseMap.put(
+                HeartRateVariabilityRmssdRecord.class,
+                new RecordTypeInfoTestResponse(
+                        VITALS,
+                        HealthPermissionCategory.HEART_RATE_VARIABILITY,
+                        new ArrayList<>()));
+        expectedResponseMap.put(
+                MenstruationFlowRecord.class,
+                new RecordTypeInfoTestResponse(
+                        CYCLE_TRACKING, HealthPermissionCategory.MENSTRUATION, new ArrayList<>()));
+        expectedResponseMap.put(
+                BloodGlucoseRecord.class,
+                new RecordTypeInfoTestResponse(
+                        VITALS, HealthPermissionCategory.BLOOD_GLUCOSE, new ArrayList<>()));
+        expectedResponseMap.put(
+                BloodPressureRecord.class,
+                new RecordTypeInfoTestResponse(
+                        VITALS, HealthPermissionCategory.BLOOD_PRESSURE, new ArrayList<>()));
+        expectedResponseMap.put(
+                CyclingPedalingCadenceRecord.class,
+                new RecordTypeInfoTestResponse(ACTIVITY, EXERCISE, new ArrayList<>()));
+        expectedResponseMap.put(
+                IntermenstrualBleedingRecord.class,
+                new RecordTypeInfoTestResponse(
+                        CYCLE_TRACKING,
+                        HealthPermissionCategory.INTERMENSTRUAL_BLEEDING,
+                        new ArrayList<>()));
+        expectedResponseMap.put(
+                FloorsClimbedRecord.class,
+                new RecordTypeInfoTestResponse(
+                        ACTIVITY, HealthPermissionCategory.FLOORS_CLIMBED, new ArrayList<>()));
+        expectedResponseMap.put(
+                StepsCadenceRecord.class,
+                new RecordTypeInfoTestResponse(ACTIVITY, STEPS, new ArrayList<>()));
+        expectedResponseMap.put(
+                HeightRecord.class,
+                new RecordTypeInfoTestResponse(
+                        BODY_MEASUREMENTS, HealthPermissionCategory.HEIGHT, new ArrayList<>()));
+        expectedResponseMap.put(
+                SexualActivityRecord.class,
+                new RecordTypeInfoTestResponse(
+                        CYCLE_TRACKING,
+                        HealthPermissionCategory.SEXUAL_ACTIVITY,
+                        new ArrayList<>()));
+        expectedResponseMap.put(
+                MenstruationPeriodRecord.class,
+                new RecordTypeInfoTestResponse(
+                        CYCLE_TRACKING, HealthPermissionCategory.MENSTRUATION, new ArrayList<>()));
+        expectedResponseMap.put(
+                SleepSessionRecord.class,
+                new RecordTypeInfoTestResponse(
+                        SLEEP, HealthPermissionCategory.SLEEP, new ArrayList<>()));
+        expectedResponseMap.put(
+                BasalMetabolicRateRecord.class,
+                new RecordTypeInfoTestResponse(
+                        BODY_MEASUREMENTS, BASAL_METABOLIC_RATE, new ArrayList<>()));
+    }
+
+    public static FetchDataOriginsPriorityOrderResponse fetchDataOriginsPriorityOrder(
+            int dataCategory) throws InterruptedException {
+        HealthConnectReceiver<FetchDataOriginsPriorityOrderResponse> receiver =
+                new HealthConnectReceiver<>();
+        getHealthConnectManager()
+                .fetchDataOriginsPriorityOrder(
+                        dataCategory, Executors.newSingleThreadExecutor(), receiver);
+        return receiver.getResponse();
+    }
+
+    public static void updateDataOriginPriorityOrder(UpdateDataOriginPriorityOrderRequest request)
+            throws InterruptedException {
+        HealthConnectReceiver<Void> receiver = new HealthConnectReceiver<>();
+        getHealthConnectManager()
+                .updateDataOriginPriorityOrder(
+                        request, Executors.newSingleThreadExecutor(), receiver);
+        receiver.verifyNoExceptionOrThrow();
+    }
+
+    public static void grantPermission(String pkgName, String permission) {
+        HealthConnectManager service = getHealthConnectManager();
+        runWithShellPermissionIdentity(
+                () ->
+                        service.getClass()
+                                .getMethod("grantHealthPermission", String.class, String.class)
+                                .invoke(service, pkgName, permission),
+                MANAGE_HEALTH_PERMISSIONS);
+    }
+
+    public static void revokePermission(String pkgName, String permission) {
+        HealthConnectManager service = getHealthConnectManager();
+        runWithShellPermissionIdentity(
+                () ->
+                        service.getClass()
+                                .getMethod(
+                                        "revokeHealthPermission",
+                                        String.class,
+                                        String.class,
+                                        String.class)
+                                .invoke(service, pkgName, permission, null),
+                MANAGE_HEALTH_PERMISSIONS);
+    }
+
+    /**
+     * Utility method to call {@link HealthConnectManager#revokeAllHealthPermissions(String,
+     * String)}.
+     */
+    public static void revokeAllPermissions(String packageName, @Nullable String reason) {
+        HealthConnectManager service = getHealthConnectManager();
+        runWithShellPermissionIdentity(
+                () ->
+                        service.getClass()
+                                .getMethod("revokeAllHealthPermissions", String.class, String.class)
+                                .invoke(service, packageName, reason),
+                MANAGE_HEALTH_PERMISSIONS);
+    }
+
+    /**
+     * Same as {@link #revokeAllPermissions(String, String)} but with a delay to wait for grant time
+     * to be updated.
+     */
+    public static void revokeAllPermissionsWithDelay(String packageName, @Nullable String reason)
+            throws InterruptedException {
+        revokeAllPermissions(packageName, reason);
+        Thread.sleep(500);
+    }
+
+    /**
+     * Utility method to call {@link
+     * HealthConnectManager#getHealthDataHistoricalAccessStartDate(String)}.
+     */
+    public static Instant getHealthDataHistoricalAccessStartDate(String packageName) {
+        HealthConnectManager service = getHealthConnectManager();
+        return (Instant)
+                runWithShellPermissionIdentity(
+                        () ->
+                                service.getClass()
+                                        .getMethod(
+                                                "getHealthDataHistoricalAccessStartDate",
+                                                String.class)
+                                        .invoke(service, packageName),
+                        MANAGE_HEALTH_PERMISSIONS);
+    }
+
+    public static void revokeHealthPermissions(String packageName) {
+        runWithShellPermissionIdentity(() -> revokeHealthPermissionsPrivileged(packageName));
+    }
+
+    private static void revokeHealthPermissionsPrivileged(String packageName)
+            throws PackageManager.NameNotFoundException {
+        final Context targetContext = androidx.test.InstrumentationRegistry.getTargetContext();
+        final PackageManager packageManager = targetContext.getPackageManager();
+        final UserHandle user = targetContext.getUser();
+
+        final PackageInfo packageInfo =
+                packageManager.getPackageInfo(
+                        packageName,
+                        PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS));
+
+        final String[] permissions = packageInfo.requestedPermissions;
+        if (permissions == null) {
+            return;
+        }
+
+        for (String permission : permissions) {
+            if (permission.startsWith(HEALTH_PERMISSION_PREFIX)) {
+                packageManager.revokeRuntimePermission(packageName, permission, user);
+            }
+        }
+    }
+
+    public static List<String> getGrantedHealthPermissions(String pkgName) {
+        final PackageInfo pi = getAppPackageInfo(pkgName);
+        final String[] requestedPermissions = pi.requestedPermissions;
+        final int[] requestedPermissionsFlags = pi.requestedPermissionsFlags;
+
+        if (requestedPermissions == null) {
+            return List.of();
+        }
+
+        final List<String> permissions = new ArrayList<>();
+
+        for (int i = 0; i < requestedPermissions.length; i++) {
+            if ((requestedPermissionsFlags[i] & PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0) {
+                if (requestedPermissions[i].startsWith(HEALTH_PERMISSION_PREFIX)) {
+                    permissions.add(requestedPermissions[i]);
+                }
+            }
+        }
+
+        return permissions;
+    }
+
+    private static PackageInfo getAppPackageInfo(String pkgName) {
+        final Context targetContext = androidx.test.InstrumentationRegistry.getTargetContext();
+        return runWithShellPermissionIdentity(
+                () ->
+                        targetContext
+                                .getPackageManager()
+                                .getPackageInfo(
+                                        pkgName,
+                                        PackageManager.PackageInfoFlags.of(GET_PERMISSIONS)));
+    }
+
+    public static void deleteTestData() throws InterruptedException {
+        verifyDeleteRecords(
+                new DeleteUsingFiltersRequest.Builder()
+                        .setTimeRangeFilter(
+                                new TimeInstantRangeFilter.Builder()
+                                        .setStartTime(Instant.EPOCH)
+                                        .setEndTime(Instant.now().plus(10, ChronoUnit.DAYS))
+                                        .build())
+                        .addRecordType(ExerciseSessionRecord.class)
+                        .addRecordType(StepsRecord.class)
+                        .addRecordType(HeartRateRecord.class)
+                        .addRecordType(BasalMetabolicRateRecord.class)
+                        .build());
+    }
+
+    public static void revokeAndThenGrantHealthPermissions(TestApp testApp) {
+        List<String> healthPerms = getGrantedHealthPermissions(testApp.getPackageName());
+
+        revokeHealthPermissions(testApp.getPackageName());
+
+        for (String perm : healthPerms) {
+            grantPermission(testApp.getPackageName(), perm);
+        }
+    }
+
+    public static String runShellCommand(String command) throws IOException {
+        UiAutomation uiAutomation =
+                androidx.test.platform.app.InstrumentationRegistry.getInstrumentation()
+                        .getUiAutomation();
+        uiAutomation.adoptShellPermissionIdentity();
+        final ParcelFileDescriptor stdout = uiAutomation.executeShellCommand(command);
+        StringBuilder output = new StringBuilder();
+
+        try (BufferedReader reader =
+                new BufferedReader(
+                        new InputStreamReader(new FileInputStream(stdout.getFileDescriptor())))) {
+            char[] buffer = new char[4096];
+            int bytesRead;
+            while ((bytesRead = reader.read(buffer)) != -1) {
+                output.append(buffer, 0, bytesRead);
+            }
+        } catch (FileNotFoundException e) {
+            Log.e(TAG, e.getMessage());
+        }
+
+        return output.toString();
+    }
+
+    @NonNull
+    private static HealthConnectManager getHealthConnectManager() {
+        return getHealthConnectManager(ApplicationProvider.getApplicationContext());
+    }
+
+    @NonNull
+    private static HealthConnectManager getHealthConnectManager(Context context) {
+        return requireNonNull(context.getSystemService(HealthConnectManager.class));
+    }
+
+    public static String getDeviceConfigValue(String key) {
+        return SystemUtil.runShellCommand("device_config get health_fitness " + key);
+    }
+
+    public static void setDeviceConfigValue(String key, String value) {
+        SystemUtil.runShellCommand("device_config put health_fitness " + key + " " + value);
+    }
+
+    public static void sendCommandToTestAppReceiver(Context context, String action) {
+        sendCommandToTestAppReceiver(context, action, /*extras=*/ null);
+    }
+
+    public static void sendCommandToTestAppReceiver(Context context, String action, Bundle extras) {
+        final Intent intent = new Intent(action).setClassName(PKG_TEST_APP, TEST_APP_RECEIVER);
+        intent.putExtra(EXTRA_SENDER_PACKAGE_NAME, context.getPackageName());
+        if (extras != null) {
+            intent.putExtras(extras);
+        }
+        context.sendBroadcast(intent);
+    }
+
+    public static final class RecordAndIdentifier {
+        private final int mId;
+        private final Record mRecordClass;
+
+        public RecordAndIdentifier(int id, Record recordClass) {
+            this.mId = id;
+            this.mRecordClass = recordClass;
+        }
+
+        public int getId() {
+            return mId;
+        }
+
+        public Record getRecordClass() {
+            return mRecordClass;
+        }
+    }
+
+    public static class RecordTypeInfoTestResponse {
+        private final int mRecordTypePermission;
+        private final ArrayList<String> mContributingPackages;
+        private final int mRecordTypeCategory;
+
+        RecordTypeInfoTestResponse(
+                int recordTypeCategory,
+                int recordTypePermission,
+                ArrayList<String> contributingPackages) {
+            mRecordTypeCategory = recordTypeCategory;
+            mRecordTypePermission = recordTypePermission;
+            mContributingPackages = contributingPackages;
+        }
+
+        public int getRecordTypeCategory() {
+            return mRecordTypeCategory;
+        }
+
+        public int getRecordTypePermission() {
+            return mRecordTypePermission;
+        }
+
+        public ArrayList<String> getContributingPackages() {
+            return mContributingPackages;
+        }
+    }
+
+    public static class RecordTypeAndRecordIds implements Serializable {
+        private String mRecordType;
+        private List<String> mRecordIds;
+
+        public RecordTypeAndRecordIds(String recordType, List<String> ids) {
+            mRecordType = recordType;
+            mRecordIds = ids;
+        }
+
+        public String getRecordType() {
+            return mRecordType;
+        }
+
+        public List<String> getRecordIds() {
+            return mRecordIds;
+        }
+    }
+
+    private static class TestReceiver<T, E extends RuntimeException>
+            implements OutcomeReceiver<T, E> {
+        private final CountDownLatch mLatch = new CountDownLatch(1);
+        private final AtomicReference<T> mResponse = new AtomicReference<>();
+        private final AtomicReference<E> mException = new AtomicReference<>();
+
+        public T getResponse() throws InterruptedException {
+            verifyNoExceptionOrThrow();
+            return mResponse.get();
+        }
+
+        public void verifyNoExceptionOrThrow() throws InterruptedException {
+            assertThat(mLatch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue();
+            if (mException.get() != null) {
+                throw mException.get();
+            }
+        }
+
+        @Override
+        public void onResult(T result) {
+            mResponse.set(result);
+            mLatch.countDown();
+        }
+
+        @Override
+        public void onError(@NonNull E error) {
+            mException.set(error);
+            Log.e(TAG, error.getMessage());
+            mLatch.countDown();
+        }
+    }
+
+    private static final class HealthConnectReceiver<T>
+            extends TestReceiver<T, HealthConnectException> {}
+
+    private static final class MigrationReceiver extends TestReceiver<Void, MigrationException> {}
+}
diff --git a/tests/integrationtests/Android.bp b/tests/integrationtests/Android.bp
index 979178e..808a7fa 100644
--- a/tests/integrationtests/Android.bp
+++ b/tests/integrationtests/Android.bp
@@ -17,6 +17,42 @@
 }
 
 android_test {
+    name: "HealthFitnessIntegrationBackupRestoreTests",
+    test_suites: [
+        "device-tests",
+        "general-tests",
+        "mts-healthfitness",
+    ],
+
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+        "framework-configinfrastructure",
+        "framework-sdkextensions",
+        "framework-healthfitness.impl"
+    ],
+
+    static_libs: [
+        "androidx.test.rules",
+        "compatibility-device-util-axt",
+        "androidx.test.ext.truth",
+        "cts-healthconnect-utils",
+    ],
+    srcs: [
+        "src/android/healthconnect/tests/backuprestore/*.java"
+    ],
+    min_sdk_version: "34",
+    sdk_version: "module_current",
+    test_config:"AndroidTestBackupRestore.xml",
+    manifest: "AndroidManifestBackupRestore.xml",
+    resource_dirs: ["res"],
+    data: [
+        ":HealthFitnessCtsTestApp",
+        ":HealthFitnessCtsTestApp2"
+    ]
+}
+
+android_test {
     name: "HealthFitnessIntegrationTests",
     test_suites: [
         "device-tests",
@@ -31,11 +67,15 @@
         "androidx.test.rules",
         "compatibility-device-util-axt",
         "androidx.test.ext.truth",
+        "cts-healthconnect-utils",
     ],
 
     srcs: [
         ":healthfitness-permissions-testapp-srcs",
-        "src/**/*.java",
+        "src/**/*.java"
+    ],
+    exclude_srcs: [
+        "src/android/healthconnect/tests/backuprestore/*.java"
     ],
     sdk_version: "module_current",
     data: [
diff --git a/tests/integrationtests/AndroidManifest.xml b/tests/integrationtests/AndroidManifest.xml
index b9c86ad..1e62fa7 100644
--- a/tests/integrationtests/AndroidManifest.xml
+++ b/tests/integrationtests/AndroidManifest.xml
@@ -34,6 +34,7 @@
 
     <uses-permission android:name="android.permission.health.READ_BODY_FAT"/>
     <uses-permission android:name="android.permission.health.READ_HEIGHT"/>
+    <uses-permission android:name="android.permission.health.WRITE_ACTIVE_CALORIES_BURNED"/>
 
     <application android:debuggable="true">
         <uses-library android:name="android.test.runner"/>
@@ -45,6 +46,8 @@
             </intent-filter>
         </activity>
 
+        <receiver android:name="android.healthconnect.cts.utils.TestReceiver"
+                  android:exported="true"/>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/tests/integrationtests/AndroidManifestBackupRestore.xml b/tests/integrationtests/AndroidManifestBackupRestore.xml
new file mode 100644
index 0000000..109cf04
--- /dev/null
+++ b/tests/integrationtests/AndroidManifestBackupRestore.xml
@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.healthconnect.integrationtests.backuprestore">
+
+    <application
+        android:debuggable="true"
+        android:largeHeap="true">
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name=".EmptyActivity"
+            android:label="EmptyActivity"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW_PERMISSION_USAGE"/>
+                <category android:name="android.intent.category.HEALTH_PERMISSIONS"/>
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.health.connect.action.SHOW_MIGRATION_INFO"/>
+            </intent-filter>
+        </activity>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.healthconnect.integrationtests.backuprestore"
+        android:label="Integration tests for HC Backup and Restore">
+    </instrumentation>
+
+    <!-- This is needed to turn enable D2D flag for B&R local transport -->
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
+    <!-- This is needed to query for HC BR APK package name -->
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+
+    <uses-permission android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED"/>
+    <uses-permission android:name="android.permission.health.READ_BASAL_BODY_TEMPERATURE"/>
+    <uses-permission android:name="android.permission.health.READ_BASAL_METABOLIC_RATE"/>
+    <uses-permission android:name="android.permission.health.READ_BLOOD_GLUCOSE"/>
+    <uses-permission android:name="android.permission.health.READ_BLOOD_PRESSURE"/>
+    <uses-permission android:name="android.permission.health.READ_BODY_FAT"/>
+    <uses-permission android:name="android.permission.health.READ_BODY_TEMPERATURE"/>
+    <uses-permission android:name="android.permission.health.READ_BODY_WATER_MASS"/>
+    <uses-permission android:name="android.permission.health.READ_BONE_MASS"/>
+    <uses-permission android:name="android.permission.health.READ_CERVICAL_MUCUS"/>
+    <uses-permission android:name="android.permission.health.READ_INTERMENSTRUAL_BLEEDING"/>
+    <uses-permission android:name="android.permission.health.READ_DISTANCE"/>
+    <uses-permission android:name="android.permission.health.READ_ELEVATION_GAINED"/>
+    <uses-permission android:name="android.permission.health.READ_EXERCISE"/>
+    <uses-permission android:name="android.permission.health.READ_FLOORS_CLIMBED"/>
+    <uses-permission android:name="android.permission.health.READ_HEART_RATE"/>
+    <uses-permission android:name="android.permission.health.READ_HEART_RATE_VARIABILITY"/>
+    <uses-permission android:name="android.permission.health.READ_HEIGHT"/>
+    <uses-permission android:name="android.permission.health.READ_HYDRATION"/>
+    <uses-permission android:name="android.permission.health.READ_LEAN_BODY_MASS"/>
+    <uses-permission android:name="android.permission.health.READ_MENSTRUATION"/>
+    <uses-permission android:name="android.permission.health.READ_NUTRITION"/>
+    <uses-permission android:name="android.permission.health.READ_OVULATION_TEST"/>
+    <uses-permission android:name="android.permission.health.READ_OXYGEN_SATURATION"/>
+    <uses-permission android:name="android.permission.health.READ_POWER"/>
+    <uses-permission android:name="android.permission.health.READ_RESPIRATORY_RATE"/>
+    <uses-permission android:name="android.permission.health.READ_RESTING_HEART_RATE"/>
+    <uses-permission android:name="android.permission.health.READ_SEXUAL_ACTIVITY"/>
+    <uses-permission android:name="android.permission.health.READ_SLEEP"/>
+    <uses-permission android:name="android.permission.health.READ_SPEED"/>
+    <uses-permission android:name="android.permission.health.READ_STEPS"/>
+    <uses-permission android:name="android.permission.health.READ_TOTAL_CALORIES_BURNED"/>
+    <uses-permission android:name="android.permission.health.READ_VO2_MAX"/>
+    <uses-permission android:name="android.permission.health.READ_WEIGHT"/>
+    <uses-permission android:name="android.permission.health.READ_WHEELCHAIR_PUSHES"/>
+    <uses-permission android:name="android.permission.health.WRITE_ACTIVE_CALORIES_BURNED"/>
+    <uses-permission android:name="android.permission.health.WRITE_BASAL_BODY_TEMPERATURE"/>
+    <uses-permission android:name="android.permission.health.WRITE_BASAL_METABOLIC_RATE"/>
+    <uses-permission android:name="android.permission.health.WRITE_BLOOD_GLUCOSE"/>
+    <uses-permission android:name="android.permission.health.WRITE_BLOOD_PRESSURE"/>
+    <uses-permission android:name="android.permission.health.WRITE_BODY_FAT"/>
+    <uses-permission android:name="android.permission.health.WRITE_BODY_TEMPERATURE"/>
+    <uses-permission android:name="android.permission.health.WRITE_BODY_WATER_MASS"/>
+    <uses-permission android:name="android.permission.health.WRITE_BONE_MASS"/>
+    <uses-permission android:name="android.permission.health.WRITE_CERVICAL_MUCUS"/>
+    <uses-permission android:name="android.permission.health.WRITE_INTERMENSTRUAL_BLEEDING"/>
+    <uses-permission android:name="android.permission.health.WRITE_DISTANCE"/>
+    <uses-permission android:name="android.permission.health.WRITE_ELEVATION_GAINED"/>
+    <uses-permission android:name="android.permission.health.WRITE_EXERCISE"/>
+    <uses-permission android:name="android.permission.health.WRITE_EXERCISE_ROUTE"/>
+    <uses-permission android:name="android.permission.health.WRITE_FLOORS_CLIMBED"/>
+    <uses-permission android:name="android.permission.health.WRITE_HEART_RATE"/>
+    <uses-permission android:name="android.permission.health.WRITE_HEART_RATE_VARIABILITY"/>
+    <uses-permission android:name="android.permission.health.WRITE_HEIGHT"/>
+    <uses-permission android:name="android.permission.health.WRITE_HYDRATION"/>
+    <uses-permission android:name="android.permission.health.WRITE_LEAN_BODY_MASS"/>
+    <uses-permission android:name="android.permission.health.WRITE_MENSTRUATION"/>
+    <uses-permission android:name="android.permission.health.WRITE_NUTRITION"/>
+    <uses-permission android:name="android.permission.health.WRITE_OVULATION_TEST"/>
+    <uses-permission android:name="android.permission.health.WRITE_OXYGEN_SATURATION"/>
+    <uses-permission android:name="android.permission.health.WRITE_POWER"/>
+    <uses-permission android:name="android.permission.health.WRITE_RESPIRATORY_RATE"/>
+    <uses-permission android:name="android.permission.health.WRITE_RESTING_HEART_RATE"/>
+    <uses-permission android:name="android.permission.health.WRITE_SEXUAL_ACTIVITY"/>
+    <uses-permission android:name="android.permission.health.WRITE_SLEEP"/>
+    <uses-permission android:name="android.permission.health.WRITE_SPEED"/>
+    <uses-permission android:name="android.permission.health.WRITE_STEPS"/>
+    <uses-permission android:name="android.permission.health.WRITE_TOTAL_CALORIES_BURNED"/>
+    <uses-permission android:name="android.permission.health.WRITE_VO2_MAX"/>
+    <uses-permission android:name="android.permission.health.WRITE_WEIGHT"/>
+    <uses-permission android:name="android.permission.health.WRITE_WHEELCHAIR_PUSHES"/>
+</manifest>
diff --git a/tests/integrationtests/AndroidTestBackupRestore.xml b/tests/integrationtests/AndroidTestBackupRestore.xml
new file mode 100644
index 0000000..e4a97ea
--- /dev/null
+++ b/tests/integrationtests/AndroidTestBackupRestore.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 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.
+  -->
+<configuration description="Config for Health Connect Backup Restore test cases">
+    <option name="test-suite-tag" value="apct"/>
+    <option name="config-descriptor:metadata" key="component" value="framework"/>
+    <option name="config-descriptor:metadata" key="parameter" value="not_instant_app"/>
+    <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi"/>
+    <!--  HealthFitnessManager APIs should work the same in any user. -->
+    <!--  While running with atest use 'user-type secondary_user'-->
+    <option name="config-descriptor:metadata" key="parameter" value="secondary_user"/>
+    <option name="config-descriptor:metadata" key="parameter" value="no_foldable_states"/>
+    <option name="not-shardable" value="true"/>
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true"/>
+        <option name="test-file-name" value="HealthFitnessIntegrationBackupRestoreTests.apk"/>
+        <option name="test-file-name" value="HealthFitnessCtsTestApp.apk"/>
+        <option name="test-file-name" value="HealthFitnessCtsTestApp2.apk"/>
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+        <option name="package" value="android.healthconnect.integrationtests.backuprestore"/>
+        <option name="hidden-api-checks" value="false"/>
+    </test>
+
+    <option
+        name="config-descriptor:metadata"
+        key="mainline-param"
+        value="com.google.android.healthfitness.apex"/>
+
+    <object type="module_controller"
+            class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="mainline-module-package-name" value="com.google.android.healthfitness"/>
+    </object>
+</configuration>
diff --git a/tests/integrationtests/TestApp/Android.bp b/tests/integrationtests/TestApp/Android.bp
index 70b4115..8dd3c4e 100644
--- a/tests/integrationtests/TestApp/Android.bp
+++ b/tests/integrationtests/TestApp/Android.bp
@@ -42,8 +42,8 @@
         "general-tests",
     ],
 
-    sdk_version: "system_current",
-
+    target_sdk_version: "34",
+    min_sdk_version: "34",
 }
 
 android_test_helper_app {
diff --git a/tests/integrationtests/TestApp/AndroidManifest.xml b/tests/integrationtests/TestApp/AndroidManifest.xml
index 7f3b313..647c9a9 100644
--- a/tests/integrationtests/TestApp/AndroidManifest.xml
+++ b/tests/integrationtests/TestApp/AndroidManifest.xml
@@ -20,6 +20,10 @@
     <!-- Health-related permissions. -->
     <uses-permission android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED"/>
     <uses-permission android:name="android.permission.health.WRITE_ACTIVE_CALORIES_BURNED"/>
+    <uses-permission android:name="android.permission.health.WRITE_STEPS"/>
+    <uses-permission android:name="android.permission.health.WRITE_WEIGHT"/>
+
+    <uses-permission android:name="android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND"/>
 
     <application>
         <uses-library android:name="android.test.runner"/>
@@ -39,5 +43,8 @@
                 <action android:name="any.action"/>
             </intent-filter>
         </activity>
+
+        <receiver android:name=".TestAppReceiver"
+                  android:exported="true"/>
     </application>
 </manifest>
\ No newline at end of file
diff --git a/tests/integrationtests/TestApp/src/android/healthconnect/test/app/BlockingOutcomeReceiver.java b/tests/integrationtests/TestApp/src/android/healthconnect/test/app/BlockingOutcomeReceiver.java
new file mode 100644
index 0000000..16a723f
--- /dev/null
+++ b/tests/integrationtests/TestApp/src/android/healthconnect/test/app/BlockingOutcomeReceiver.java
@@ -0,0 +1,63 @@
+/*
+ * 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.test.app;
+
+import android.health.connect.HealthConnectException;
+import android.os.OutcomeReceiver;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public final class BlockingOutcomeReceiver<T>
+        implements OutcomeReceiver<T, HealthConnectException> {
+
+    private final CountDownLatch mLatch = new CountDownLatch(1);
+    private T mResult;
+    private HealthConnectException mError;
+
+    @Override
+    public void onResult(T result) {
+        mResult = result;
+        mLatch.countDown();
+    }
+
+    @Override
+    public void onError(HealthConnectException error) {
+        mError = error;
+        mLatch.countDown();
+    }
+
+    public T getResult() throws HealthConnectException {
+        await();
+        return mResult;
+    }
+
+    public HealthConnectException getError() {
+        await();
+        return mError;
+    }
+
+    private void await() {
+        try {
+            if (!mLatch.await(10, TimeUnit.SECONDS)) {
+                throw new RuntimeException("Timeout waiting for outcome");
+            }
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/tests/integrationtests/TestApp/src/android/healthconnect/test/app/TestAppReceiver.java b/tests/integrationtests/TestApp/src/android/healthconnect/test/app/TestAppReceiver.java
new file mode 100644
index 0000000..1253b64
--- /dev/null
+++ b/tests/integrationtests/TestApp/src/android/healthconnect/test/app/TestAppReceiver.java
@@ -0,0 +1,315 @@
+/*
+ * 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.test.app;
+
+import static android.health.connect.datatypes.ActiveCaloriesBurnedRecord.ACTIVE_CALORIES_TOTAL;
+
+import static java.util.Objects.requireNonNull;
+import static java.util.concurrent.Executors.newSingleThreadExecutor;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.health.connect.AggregateRecordsRequest;
+import android.health.connect.AggregateRecordsResponse;
+import android.health.connect.HealthConnectException;
+import android.health.connect.HealthConnectManager;
+import android.health.connect.InsertRecordsResponse;
+import android.health.connect.ReadRecordsRequestUsingFilters;
+import android.health.connect.ReadRecordsResponse;
+import android.health.connect.TimeInstantRangeFilter;
+import android.health.connect.changelog.ChangeLogTokenRequest;
+import android.health.connect.changelog.ChangeLogTokenResponse;
+import android.health.connect.changelog.ChangeLogsRequest;
+import android.health.connect.changelog.ChangeLogsResponse;
+import android.health.connect.datatypes.ActiveCaloriesBurnedRecord;
+import android.health.connect.datatypes.DataOrigin;
+import android.health.connect.datatypes.Metadata;
+import android.health.connect.datatypes.Record;
+import android.health.connect.datatypes.StepsRecord;
+import android.health.connect.datatypes.WeightRecord;
+import android.health.connect.datatypes.units.Energy;
+import android.health.connect.datatypes.units.Mass;
+import android.os.Bundle;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/** Receives requests from test cases. Required to perform API calls in background. */
+public class TestAppReceiver extends BroadcastReceiver {
+    public static final String ACTION_INSERT_STEPS_RECORDS = "action.INSERT_STEPS_RECORDS";
+    public static final String ACTION_INSERT_WEIGHT_RECORDS = "action.INSERT_WEIGHT_RECORDS";
+    public static final String ACTION_READ_RECORDS_FOR_OTHER_APP =
+            "action.READ_RECORDS_FOR_OTHER_APP";
+    public static final String ACTION_AGGREGATE = "action.AGGREGATE";
+    public static final String ACTION_GET_CHANGE_LOG_TOKEN = "action.GET_CHANGE_LOG_TOKEN";
+    public static final String ACTION_GET_CHANGE_LOGS = "action.GET_CHANGE_LOGS";
+    public static final String ACTION_RESULT_SUCCESS = "action.SUCCESS";
+    public static final String ACTION_RESULT_ERROR = "action.ERROR";
+    public static final String EXTRA_RESULT_ERROR_CODE = "extra.ERROR_CODE";
+    public static final String EXTRA_RESULT_ERROR_MESSAGE = "extra.ERROR_MESSAGE";
+    public static final String EXTRA_RECORD_COUNT = "extra.RECORD_COUNT";
+    public static final String EXTRA_RECORD_IDS = "extra.RECORD_IDS";
+
+    /**
+     * This is used to represent either times for InstantRecords or start times for IntervalRecords.
+     */
+    public static final String EXTRA_TIMES = "extra.TIMES";
+
+    public static final String EXTRA_END_TIMES = "extra.END_TIMES";
+
+    /** Represents a list of values in {@code long}. */
+    public static final String EXTRA_RECORD_VALUES = "extra.RECORD_VALUES";
+
+    public static final String EXTRA_TOKEN = "extra.TOKEN";
+    public static final String EXTRA_SENDER_PACKAGE_NAME = "extra.SENDER_PACKAGE_NAME";
+    private static final String TEST_SUITE_RECEIVER =
+            "android.healthconnect.cts.utils.TestReceiver";
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        switch (intent.getAction()) {
+            case ACTION_INSERT_STEPS_RECORDS:
+                insertStepsRecords(context, intent);
+                break;
+            case ACTION_INSERT_WEIGHT_RECORDS:
+                insertWeightRecords(context, intent);
+                break;
+            case ACTION_READ_RECORDS_FOR_OTHER_APP:
+                readRecordsForOtherApp(context, intent);
+                break;
+            case ACTION_AGGREGATE:
+                aggregate(context, intent);
+                break;
+            case ACTION_GET_CHANGE_LOG_TOKEN:
+                getChangeLogToken(context, intent);
+                break;
+            case ACTION_GET_CHANGE_LOGS:
+                getChangeLogs(context, intent);
+                break;
+            default:
+                throw new IllegalStateException("Unsupported command: " + intent.getAction());
+        }
+    }
+
+    private static void insertStepsRecords(Context context, Intent intent) {
+        BlockingOutcomeReceiver<InsertRecordsResponse> outcome = new BlockingOutcomeReceiver<>();
+        getHealthConnectManager(context)
+                .insertRecords(createStepsRecords(intent), newSingleThreadExecutor(), outcome);
+        sendInsertRecordsResult(context, intent, outcome);
+    }
+
+    private static void insertWeightRecords(Context context, Intent intent) {
+        BlockingOutcomeReceiver<InsertRecordsResponse> outcome = new BlockingOutcomeReceiver<>();
+        getHealthConnectManager(context)
+                .insertRecords(createWeightRecords(intent), newSingleThreadExecutor(), outcome);
+        sendInsertRecordsResult(context, intent, outcome);
+    }
+
+    private void readRecordsForOtherApp(Context context, Intent intent) {
+        final BlockingOutcomeReceiver<ReadRecordsResponse<ActiveCaloriesBurnedRecord>> outcome =
+                new BlockingOutcomeReceiver<>();
+
+        getHealthConnectManager(context)
+                .readRecords(
+                        new ReadRecordsRequestUsingFilters.Builder<>(
+                                        ActiveCaloriesBurnedRecord.class)
+                                .addDataOrigins(
+                                        new DataOrigin.Builder()
+                                                .setPackageName(getSenderPackageName(intent))
+                                                .build())
+                                .build(),
+                        newSingleThreadExecutor(),
+                        outcome);
+
+        sendReadRecordsResult(context, intent, outcome);
+    }
+
+    private void aggregate(Context context, Intent intent) {
+        final BlockingOutcomeReceiver<AggregateRecordsResponse<Energy>> outcome =
+                new BlockingOutcomeReceiver<>();
+
+        getHealthConnectManager(context)
+                .aggregate(
+                        new AggregateRecordsRequest.Builder<Energy>(
+                                        new TimeInstantRangeFilter.Builder()
+                                                .setEndTime(Instant.now())
+                                                .setStartTime(Instant.now().minusSeconds(10))
+                                                .build())
+                                .addAggregationType(ACTIVE_CALORIES_TOTAL)
+                                .build(),
+                        newSingleThreadExecutor(),
+                        outcome);
+
+        sendResult(context, intent, outcome);
+    }
+
+    private void getChangeLogToken(Context context, Intent intent) {
+        final BlockingOutcomeReceiver<ChangeLogTokenResponse> outcome =
+                new BlockingOutcomeReceiver<>();
+
+        getHealthConnectManager(context)
+                .getChangeLogToken(
+                        new ChangeLogTokenRequest.Builder()
+                                .addRecordType(ActiveCaloriesBurnedRecord.class)
+                                .build(),
+                        newSingleThreadExecutor(),
+                        outcome);
+
+        final HealthConnectException error = outcome.getError();
+        if (error == null) {
+            final Bundle extras = new Bundle();
+            extras.putString(EXTRA_TOKEN, outcome.getResult().getToken());
+            sendSuccess(context, intent, extras);
+        } else {
+            sendError(context, intent, error);
+        }
+    }
+
+    private void getChangeLogs(Context context, Intent intent) {
+        String token = intent.getStringExtra(EXTRA_TOKEN);
+        final BlockingOutcomeReceiver<ChangeLogsResponse> outcome = new BlockingOutcomeReceiver<>();
+
+        getHealthConnectManager(context)
+                .getChangeLogs(
+                        new ChangeLogsRequest.Builder(token).build(),
+                        newSingleThreadExecutor(),
+                        outcome);
+
+        sendResult(context, intent, outcome);
+    }
+
+    private static HealthConnectManager getHealthConnectManager(Context context) {
+        return requireNonNull(context.getSystemService(HealthConnectManager.class));
+    }
+
+    private static void sendReadRecordsResult(
+            Context context,
+            Intent intent,
+            BlockingOutcomeReceiver<? extends ReadRecordsResponse<?>> outcome) {
+        final HealthConnectException error = outcome.getError();
+        if (error != null) {
+            sendError(context, intent, error);
+            return;
+        }
+
+        final Bundle extras = new Bundle();
+        extras.putInt(EXTRA_RECORD_COUNT, outcome.getResult().getRecords().size());
+        sendSuccess(context, intent, extras);
+    }
+
+    private static void sendInsertRecordsResult(
+            Context context,
+            Intent intent,
+            BlockingOutcomeReceiver<? extends InsertRecordsResponse> outcome) {
+        final HealthConnectException error = outcome.getError();
+        if (error != null) {
+            sendError(context, intent, error);
+            return;
+        }
+
+        final Bundle extras = new Bundle();
+        List<? extends Record> records = outcome.getResult().getRecords();
+        ArrayList<String> recordIds =
+                new ArrayList<>(
+                        records.stream()
+                                .map(Record::getMetadata)
+                                .map(Metadata::getId)
+                                .collect(Collectors.toList()));
+        extras.putStringArrayList(EXTRA_RECORD_IDS, recordIds);
+        extras.putInt(EXTRA_RECORD_COUNT, records.size());
+        sendSuccess(context, intent, extras);
+    }
+
+    private static void sendResult(
+            Context context, Intent intent, BlockingOutcomeReceiver<?> outcomeReceiver) {
+        final HealthConnectException error = outcomeReceiver.getError();
+        if (error != null) {
+            sendError(context, intent, error);
+            return;
+        }
+        sendSuccess(context, intent);
+    }
+
+    private static void sendSuccess(Context context, Intent intent) {
+        context.sendBroadcast(getSuccessIntent(intent));
+    }
+
+    private static void sendSuccess(Context context, Intent intent, Bundle extras) {
+        context.sendBroadcast(getSuccessIntent(intent).putExtras(extras));
+    }
+
+    private static Intent getSuccessIntent(Intent intent) {
+        return new Intent(ACTION_RESULT_SUCCESS)
+                .setClassName(getSenderPackageName(intent), TEST_SUITE_RECEIVER);
+    }
+
+    private static void sendError(Context context, Intent intent, HealthConnectException error) {
+        context.sendBroadcast(
+                new Intent(ACTION_RESULT_ERROR)
+                        .setClassName(getSenderPackageName(intent), TEST_SUITE_RECEIVER)
+                        .putExtra(EXTRA_RESULT_ERROR_CODE, error.getErrorCode())
+                        .putExtra(EXTRA_RESULT_ERROR_MESSAGE, error.getMessage()));
+    }
+
+    private static List<Record> createStepsRecords(Intent intent) {
+        List<Instant> startTimes = getTimes(intent, EXTRA_TIMES);
+        List<Instant> endTimes = getTimes(intent, EXTRA_END_TIMES);
+        long[] values = intent.getLongArrayExtra(EXTRA_RECORD_VALUES);
+
+        List<Record> result = new ArrayList<>();
+        for (int i = 0; i < startTimes.size(); i++) {
+            result.add(createStepsRecord(startTimes.get(i), endTimes.get(i), values[i]));
+        }
+        return result;
+    }
+
+    private static StepsRecord createStepsRecord(Instant startTime, Instant endTime, long steps) {
+        return new StepsRecord.Builder(new Metadata.Builder().build(), startTime, endTime, steps)
+                .build();
+    }
+
+    private static List<Record> createWeightRecords(Intent intent) {
+        List<Instant> times = getTimes(intent, EXTRA_TIMES);
+        long[] values = intent.getLongArrayExtra(EXTRA_RECORD_VALUES);
+
+        List<Record> result = new ArrayList<>();
+        for (int i = 0; i < times.size(); i++) {
+            result.add(createWeightRecord(times.get(i), values[i]));
+        }
+        return result;
+    }
+
+    private static WeightRecord createWeightRecord(Instant time, long weight) {
+        return new WeightRecord.Builder(
+                        new Metadata.Builder().build(), time, Mass.fromGrams((double) weight))
+                .build();
+    }
+
+    private static List<Instant> getTimes(Intent intent, String key) {
+        return Arrays.stream(intent.getLongArrayExtra(key))
+                .mapToObj(Instant::ofEpochMilli)
+                .collect(Collectors.toList());
+    }
+
+    private static String getSenderPackageName(Intent intent) {
+        return intent.getStringExtra(EXTRA_SENDER_PACKAGE_NAME);
+    }
+}
diff --git a/tests/integrationtests/res/raw/health-permissions-first-grant-times.xml b/tests/integrationtests/res/raw/health-permissions-first-grant-times.xml
index e54567e..5ef4670 100644
--- a/tests/integrationtests/res/raw/health-permissions-first-grant-times.xml
+++ b/tests/integrationtests/res/raw/health-permissions-first-grant-times.xml
@@ -1,4 +1,4 @@
 <?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
 <first-grant-times version="1">
-  <package name="android.healthconnect.integrationtests" first-grant-time="2023-04-09T16:54:58.104744Z" />
+  <package name="android.healthconnect.integrationtests.backuprestore" first-grant-time="2023-04-09T16:54:58.104744Z" />
 </first-grant-times>
diff --git a/tests/integrationtests/res/raw/healthconnect.db b/tests/integrationtests/res/raw/healthconnect_staged.db
similarity index 100%
rename from tests/integrationtests/res/raw/healthconnect.db
rename to tests/integrationtests/res/raw/healthconnect_staged.db
Binary files differ
diff --git a/tests/integrationtests/src/android/healthconnect/tests/backgroundread/BackgroundReadTest.java b/tests/integrationtests/src/android/healthconnect/tests/backgroundread/BackgroundReadTest.java
new file mode 100644
index 0000000..3893bde
--- /dev/null
+++ b/tests/integrationtests/src/android/healthconnect/tests/backgroundread/BackgroundReadTest.java
@@ -0,0 +1,200 @@
+/*
+ * 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.tests.backgroundread;
+
+import static android.health.connect.HealthConnectException.ERROR_SECURITY;
+import static android.health.connect.HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND;
+import static android.healthconnect.cts.utils.TestUtils.deleteAllStagedRemoteData;
+import static android.healthconnect.cts.utils.TestUtils.getDeviceConfigValue;
+import static android.healthconnect.cts.utils.TestUtils.sendCommandToTestAppReceiver;
+import static android.healthconnect.cts.utils.TestUtils.setDeviceConfigValue;
+import static android.healthconnect.test.app.TestAppReceiver.ACTION_AGGREGATE;
+import static android.healthconnect.test.app.TestAppReceiver.ACTION_GET_CHANGE_LOGS;
+import static android.healthconnect.test.app.TestAppReceiver.ACTION_GET_CHANGE_LOG_TOKEN;
+import static android.healthconnect.test.app.TestAppReceiver.ACTION_READ_RECORDS_FOR_OTHER_APP;
+import static android.healthconnect.test.app.TestAppReceiver.EXTRA_RECORD_COUNT;
+import static android.healthconnect.test.app.TestAppReceiver.EXTRA_TOKEN;
+
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.util.Objects.requireNonNull;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.health.connect.HealthConnectManager;
+import android.health.connect.InsertRecordsResponse;
+import android.health.connect.datatypes.ActiveCaloriesBurnedRecord;
+import android.health.connect.datatypes.Metadata;
+import android.health.connect.datatypes.units.Energy;
+import android.healthconnect.cts.utils.TestReceiver;
+import android.healthconnect.test.app.BlockingOutcomeReceiver;
+import android.os.Bundle;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.concurrent.Executors;
+
+@RunWith(AndroidJUnit4.class)
+public class BackgroundReadTest {
+
+    private static final String PKG_TEST_APP = "android.healthconnect.test.app";
+    private static final String FEATURE_FLAG = "background_read_enable";
+
+    private Context mContext;
+    private PackageManager mPackageManager;
+    private HealthConnectManager mManager;
+    private String mInitialFeatureFlagValue;
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        mPackageManager = mContext.getPackageManager();
+        mManager = requireNonNull(mContext.getSystemService(HealthConnectManager.class));
+        mInitialFeatureFlagValue = getDeviceConfigValue(FEATURE_FLAG);
+
+        setDeviceConfigValue(FEATURE_FLAG, "true");
+        deleteAllStagedRemoteData();
+        TestReceiver.reset();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        deleteAllStagedRemoteData();
+        setDeviceConfigValue(FEATURE_FLAG, mInitialFeatureFlagValue);
+    }
+
+    @Test
+    public void testReadRecords_inBackgroundWithoutPermission_cantReadRecordsForOtherApp() {
+        revokeBackgroundReadPermissionForTestApp();
+        insertRecords();
+
+        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);
+    }
+
+    @Test
+    public void testReadRecords_inBackgroundWithPermission_canReadRecordsForOtherApp() {
+        grantBackgroundReadPermissionForTestApp();
+        insertRecords();
+
+        sendCommandToTestAppReceiver(mContext, ACTION_READ_RECORDS_FOR_OTHER_APP);
+
+        final Bundle result = TestReceiver.getResult();
+        assertThat(result).isNotNull();
+        assertThat(result.getInt(EXTRA_RECORD_COUNT)).isGreaterThan(0);
+    }
+
+    private void insertRecords() {
+        final BlockingOutcomeReceiver<InsertRecordsResponse> outcomeReceiver =
+                new BlockingOutcomeReceiver<>();
+
+        mManager.insertRecords(
+                List.of(
+                        new ActiveCaloriesBurnedRecord.Builder(
+                                        new Metadata.Builder().build(),
+                                        Instant.now().minusSeconds(60),
+                                        Instant.now(),
+                                        Energy.fromCalories(100.0))
+                                .build()),
+                Executors.newSingleThreadExecutor(),
+                outcomeReceiver);
+
+        assertThat(outcomeReceiver.getError()).isNull();
+    }
+
+    @Test
+    public void testAggregate_inBackgroundWithoutPermission_securityError() {
+        revokeBackgroundReadPermissionForTestApp();
+
+        sendCommandToTestAppReceiver(mContext, ACTION_AGGREGATE);
+
+        assertSecurityError();
+    }
+
+    @Test
+    public void testAggregate_inBackgroundWithPermission_success() {
+        grantBackgroundReadPermissionForTestApp();
+
+        sendCommandToTestAppReceiver(mContext, ACTION_AGGREGATE);
+
+        assertSuccess();
+    }
+
+    @Test
+    public void testGetChangeLogs_inBackgroundWithoutPermission_securityError() {
+        revokeBackgroundReadPermissionForTestApp();
+
+        final Bundle extras = new Bundle();
+        extras.putString(EXTRA_TOKEN, "token");
+        sendCommandToTestAppReceiver(mContext, ACTION_GET_CHANGE_LOGS, extras);
+
+        assertSecurityError();
+    }
+
+    @Test
+    public void testGetChangeLogs_inBackgroundWithPermission_success() {
+        revokeBackgroundReadPermissionForTestApp();
+        sendCommandToTestAppReceiver(mContext, ACTION_GET_CHANGE_LOG_TOKEN);
+        final String token = requireNonNull(TestReceiver.getResult()).getString(EXTRA_TOKEN);
+        grantBackgroundReadPermissionForTestApp();
+
+        final Bundle extras = new Bundle();
+        extras.putString(EXTRA_TOKEN, token);
+        sendCommandToTestAppReceiver(mContext, ACTION_GET_CHANGE_LOGS, extras);
+
+        assertSuccess();
+    }
+
+    private void grantBackgroundReadPermissionForTestApp() {
+        runWithShellPermissionIdentity(
+                () ->
+                        mPackageManager.grantRuntimePermission(
+                                PKG_TEST_APP, READ_HEALTH_DATA_IN_BACKGROUND, mContext.getUser()));
+    }
+
+    private void revokeBackgroundReadPermissionForTestApp() {
+        runWithShellPermissionIdentity(
+                () ->
+                        mPackageManager.revokeRuntimePermission(
+                                PKG_TEST_APP, READ_HEALTH_DATA_IN_BACKGROUND, mContext.getUser()));
+    }
+
+    private void assertSecurityError() {
+        assertThat(TestReceiver.getErrorCode()).isEqualTo(ERROR_SECURITY);
+        assertThat(TestReceiver.getErrorMessage()).contains(READ_HEALTH_DATA_IN_BACKGROUND);
+    }
+
+    private void assertSuccess() {
+        assertThat(TestReceiver.getErrorCode()).isNull();
+        assertThat(TestReceiver.getErrorMessage()).isNull();
+    }
+}
diff --git a/tests/integrationtests/src/android/healthconnect/tests/backuprestore/BackupRestoreTest.java b/tests/integrationtests/src/android/healthconnect/tests/backuprestore/BackupRestoreApiTest.java
similarity index 91%
rename from tests/integrationtests/src/android/healthconnect/tests/backuprestore/BackupRestoreTest.java
rename to tests/integrationtests/src/android/healthconnect/tests/backuprestore/BackupRestoreApiTest.java
index 31853bc..8bbdbd9 100644
--- a/tests/integrationtests/src/android/healthconnect/tests/backuprestore/BackupRestoreTest.java
+++ b/tests/integrationtests/src/android/healthconnect/tests/backuprestore/BackupRestoreApiTest.java
@@ -16,6 +16,7 @@
 
 package android.healthconnect.tests.backuprestore;
 
+import static android.healthconnect.cts.utils.TestUtils.deleteAllStagedRemoteData;
 import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -35,7 +36,7 @@
 import android.health.connect.datatypes.HeightRecord;
 import android.health.connect.datatypes.Record;
 import android.health.connect.restore.StageRemoteDataException;
-import android.healthconnect.integrationtests.R;
+import android.healthconnect.integrationtests.backuprestore.R;
 import android.os.FileUtils;
 import android.os.OutcomeReceiver;
 import android.os.ParcelFileDescriptor;
@@ -54,7 +55,6 @@
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.InputStream;
-import java.lang.reflect.InvocationTargetException;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CountDownLatch;
@@ -64,7 +64,7 @@
 
 /** Integration test for the backup-restore functionality of HealthConnect service. */
 @RunWith(AndroidJUnit4.class)
-public class BackupRestoreTest {
+public class BackupRestoreApiTest {
     public static final String MANAGE_HEALTH_DATA = HealthPermissions.MANAGE_HEALTH_DATA_PERMISSION;
     private static final String TAG = "BackupRestoreIntegrationTest";
     private Context mContext;
@@ -120,7 +120,7 @@
         File backupDataDir = getBackupDataDir();
         SQLiteDatabase db =
                 SQLiteDatabase.openDatabase(
-                        new File(backupDataDir, "healthconnect.db"),
+                        new File(backupDataDir, "healthconnect_staged.db"),
                         new SQLiteDatabase.OpenParams.Builder().build());
         Cursor cursor = db.rawQuery("select * from height_record_table", null);
         ArraySet<Double> heights = new ArraySet<>();
@@ -146,25 +146,6 @@
         deleteAllStagedRemoteData();
     }
 
-    private void deleteAllStagedRemoteData()
-            throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
-        try {
-            assertThat(mService).isNotNull();
-
-            InstrumentationRegistry.getInstrumentation()
-                    .getUiAutomation()
-                    .adoptShellPermissionIdentity(
-                            "android.permission.DELETE_STAGED_HEALTH_CONNECT_REMOTE_DATA");
-            // TODO(b/241542162): Avoid using reflection as a workaround once test apis can be
-            //  run in tests.
-            mService.getClass().getMethod("deleteAllStagedRemoteData").invoke(mService);
-        } finally {
-            InstrumentationRegistry.getInstrumentation()
-                    .getUiAutomation()
-                    .dropShellPermissionIdentity();
-        }
-    }
-
     private File getBackupDataDir() {
         File backupDataDir = new File(mContext.getFilesDir(), "backup_data");
         backupDataDir.mkdirs();
@@ -173,9 +154,9 @@
 
     private void prepareDataForRestore() throws Exception {
         File backupDataDir = getBackupDataDir();
-        try (InputStream in = mContext.getResources().openRawResource(R.raw.healthconnect);
+        try (InputStream in = mContext.getResources().openRawResource(R.raw.healthconnect_staged);
                 FileOutputStream out =
-                        new FileOutputStream(new File(backupDataDir, "healthconnect.db"))) {
+                        new FileOutputStream(new File(backupDataDir, "healthconnect_staged.db"))) {
             FileUtils.copy(in, out);
             out.getFD().sync();
         }
diff --git a/tests/integrationtests/src/android/healthconnect/tests/backuprestore/BackupRestoreE2ETest.java b/tests/integrationtests/src/android/healthconnect/tests/backuprestore/BackupRestoreE2ETest.java
new file mode 100644
index 0000000..999cd0d
--- /dev/null
+++ b/tests/integrationtests/src/android/healthconnect/tests/backuprestore/BackupRestoreE2ETest.java
@@ -0,0 +1,428 @@
+/*
+ * 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.tests.backuprestore;
+
+import static android.healthconnect.cts.utils.TestUtils.deleteAllStagedRemoteData;
+import static android.healthconnect.cts.utils.TestUtils.getHealthConnectDataRestoreState;
+import static android.healthconnect.cts.utils.TestUtils.getHealthDataHistoricalAccessStartDate;
+import static android.healthconnect.cts.utils.TestUtils.grantPermission;
+import static android.healthconnect.cts.utils.TestUtils.insertRecords;
+import static android.healthconnect.cts.utils.TestUtils.readRecords;
+import static android.healthconnect.cts.utils.TestUtils.revokeAllPermissions;
+import static android.healthconnect.cts.utils.TestUtils.revokeAllPermissionsWithDelay;
+import static android.healthconnect.cts.utils.TestUtils.verifyDeleteRecords;
+
+import static com.android.compatibility.common.util.BackupUtils.LOCAL_TRANSPORT_TOKEN;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.health.connect.DeleteUsingFiltersRequest;
+import android.health.connect.ReadRecordsRequestUsingIds;
+import android.health.connect.datatypes.ActiveCaloriesBurnedRecord;
+import android.health.connect.datatypes.DataOrigin;
+import android.health.connect.datatypes.Device;
+import android.health.connect.datatypes.InstantRecord;
+import android.health.connect.datatypes.IntervalRecord;
+import android.health.connect.datatypes.Metadata;
+import android.health.connect.datatypes.Record;
+import android.health.connect.datatypes.units.Energy;
+import android.os.ParcelFileDescriptor;
+import android.os.UserHandle;
+import android.platform.test.annotations.AppModeFull;
+import android.provider.Settings;
+import android.test.InstrumentationTestCase;
+
+import androidx.annotation.NonNull;
+import androidx.test.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.BackupUtils;
+
+import java.io.InputStream;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+@AppModeFull
+public class BackupRestoreE2ETest extends InstrumentationTestCase {
+    /** Whether to print out logs in tests for debugging purposes. */
+    private static final boolean IS_DEBUGGING_TEST = false;
+
+    private static final String LOG_TAG = BackupRestoreE2ETest.class.getName();
+
+    private static final long ASSERT_TIMEOUT_MILLIS = 20_000;
+    private static final long ASSERT_RETRY_INTERVAL_MILLIS = 100;
+
+    /** A permission that HC BR APK declares. This is used to find the package name of that APK. */
+    private static final String HEALTH_CONNECT_BACKUP_INTER_AGENT_PERMISSION =
+            "android.permission.HEALTH_CONNECT_BACKUP_INTER_AGENT";
+
+    private static final String TEST_APP_1_PACKAGE_NAME = "android.healthconnect.cts.app";
+    private static final String TEST_APP_2_PACKAGE_NAME = "android.healthconnect.cts.app2";
+    private static final String TEST_APP_DECLARED_PERMISSION =
+            "android.permission.health.READ_HEIGHT";
+
+    private static final int MAX_NUMBER_OF_RECORD_PER_INSERT_REQUEST = 500;
+
+    private final BackupUtils mBackupUtils =
+            new BackupUtils() {
+                @Override
+                protected InputStream executeShellCommand(String command) {
+                    final ParcelFileDescriptor pfd =
+                            getInstrumentation().getUiAutomation().executeShellCommand(command);
+                    return new ParcelFileDescriptor.AutoCloseInputStream(pfd);
+                }
+            };
+    private final Context mContext = InstrumentationRegistry.getTargetContext();
+
+    private String mBackupRestoreApkPackageName;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mBackupRestoreApkPackageName = getBackupRestoreApkPackageName();
+        // enable backup on the test device
+        mBackupUtils.enableBackup(true);
+        // switch backup transport to local
+        mBackupUtils.setBackupTransportForUser(
+                mBackupUtils.getLocalTransportName(), UserHandle.myUserId());
+        // enable D2D backup flag so HealthConnectBackupAgent includes DB file in the backup file
+        // list
+        Settings.Secure.putString(
+                mContext.getContentResolver(),
+                "backup_local_transport_parameters",
+                "is_device_transfer=true");
+
+        deleteAllStagedRemoteData();
+        verifyDeleteRecords(new DeleteUsingFiltersRequest.Builder().build());
+    }
+
+    public void testBackupThenRestore_over2000Records_expectDataIsRestoredCorrectly()
+            throws Exception {
+        int numOfRecords = 2050;
+        List<Record> insertedRecords =
+                insertRecordsWithChunking(
+                        this::getCompleteActiveCaloriesBurnedRecord, numOfRecords);
+        assertThat(insertedRecords).hasSize(numOfRecords);
+
+        mBackupUtils.backupNowAndAssertSuccess(mBackupRestoreApkPackageName);
+
+        verifyDeleteRecords(new DeleteUsingFiltersRequest.Builder().build());
+        readAndAssertRecordsNotExistUsingIds(insertedRecords);
+
+        mBackupUtils.restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, mBackupRestoreApkPackageName);
+
+        assertWithTimeout(() -> readAndAssertRecordsExistUsingIds(insertedRecords));
+        log("Data Restore state = " + getHealthConnectDataRestoreState());
+    }
+
+    public void
+            testPermissionsControllerIsRestoredBeforeHCRestore_expectGrantTimeIsRestoredCorrectly()
+                    throws Exception {
+        // revoke all permissions for both test apps to remove all stored grant time as setup step
+        revokeAllPermissionsWithDelay(TEST_APP_1_PACKAGE_NAME, "");
+        revokeAllPermissionsWithDelay(TEST_APP_2_PACKAGE_NAME, "");
+        assertWithTimeout(
+                () ->
+                        assertThat(getHealthDataHistoricalAccessStartDate(TEST_APP_1_PACKAGE_NAME))
+                                .isNull());
+        assertWithTimeout(
+                () ->
+                        assertThat(getHealthDataHistoricalAccessStartDate(TEST_APP_2_PACKAGE_NAME))
+                                .isNull());
+        // grant a permission to test app 1 to create grant time
+        grantPermission(TEST_APP_1_PACKAGE_NAME, TEST_APP_DECLARED_PERMISSION);
+        Instant historicAccessStartDate =
+                assertWithTimeoutAndReturn(
+                        () -> {
+                            Instant result =
+                                    getHealthDataHistoricalAccessStartDate(TEST_APP_1_PACKAGE_NAME);
+                            assertThat(result).isNotNull();
+                            return result;
+                        });
+
+        // trigger backup, only test app 1 has grant time now, so the staged backup file should
+        // contains only grant time for test app 1, not test app 2
+        mBackupUtils.backupNowAndAssertSuccess(mBackupRestoreApkPackageName);
+
+        // grant permissions to stimulate the case where permission controller module is restored
+        // before HC module.
+        // Note: Currently, without permissions being granted before triggering restore, grant time
+        // would not be restored.
+        Thread.sleep(1000); // add some delay so second grant time is definitely different
+        revokeAllPermissions(TEST_APP_1_PACKAGE_NAME, "");
+        grantPermission(TEST_APP_1_PACKAGE_NAME, TEST_APP_DECLARED_PERMISSION);
+        grantPermission(TEST_APP_2_PACKAGE_NAME, TEST_APP_DECLARED_PERMISSION);
+        assertWithTimeout(
+                () ->
+                        assertThat(getHealthDataHistoricalAccessStartDate(TEST_APP_1_PACKAGE_NAME))
+                                .isNotNull());
+        Instant historicAccessStartDate2 =
+                assertWithTimeoutAndReturn(
+                        () -> {
+                            Instant result =
+                                    getHealthDataHistoricalAccessStartDate(TEST_APP_2_PACKAGE_NAME);
+                            assertThat(result).isNotNull();
+                            return result;
+                        });
+        // trigger restore, now the staged backup file which contains grant time for test app 1
+        // should override ONLY the newly created grant time of test app 1, but not the
+        // test app 2's.
+        mBackupUtils.restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, mBackupRestoreApkPackageName);
+
+        // assert that test app 1's grant time is restored correctly
+        assertWithTimeout(
+                () ->
+                        assertThat(getHealthDataHistoricalAccessStartDate(TEST_APP_1_PACKAGE_NAME))
+                                .isEqualTo(historicAccessStartDate));
+        // assert that test app 2's grant time stay the same
+        assertWithTimeout(
+                () ->
+                        assertThat(getHealthDataHistoricalAccessStartDate(TEST_APP_2_PACKAGE_NAME))
+                                .isEqualTo(historicAccessStartDate2));
+    }
+
+    private ActiveCaloriesBurnedRecord getCompleteActiveCaloriesBurnedRecord(long i) {
+        Device device =
+                new Device.Builder()
+                        .setManufacturer("google")
+                        .setModel("Pixel4a")
+                        .setType(2)
+                        .build();
+        DataOrigin dataOrigin =
+                // package name needs to match the caller's which is Context.getPackageName()
+                new DataOrigin.Builder().setPackageName(mContext.getPackageName()).build();
+        Metadata.Builder testMetadataBuilder = new Metadata.Builder();
+        testMetadataBuilder.setDevice(device).setDataOrigin(dataOrigin);
+        testMetadataBuilder.setRecordingMethod(Metadata.RECORDING_METHOD_ACTIVELY_RECORDED);
+        testMetadataBuilder.setClientRecordId("ClientRecordId" + UUID.randomUUID());
+
+        Instant now = Instant.now();
+        ZoneOffset zoneOffset = ZoneOffset.systemDefault().getRules().getOffset(now);
+        return new ActiveCaloriesBurnedRecord.Builder(
+                        testMetadataBuilder.build(),
+                        now.minusMillis(i * 10),
+                        now.minusMillis(i * 10 - 1000),
+                        Energy.fromCalories(10.0))
+                .setStartZoneOffset(zoneOffset)
+                .setEndZoneOffset(zoneOffset)
+                .build();
+    }
+
+    private static void readAndAssertRecordsExistUsingIds(List<Record> insertedRecords)
+            throws InterruptedException {
+        String logTag = "readAndAssertRecordsExistUsingIds";
+        logRecords(logTag + " INPUT", insertedRecords);
+
+        List<? extends Record> result = readRecordsUsingIds(insertedRecords);
+
+        logRecords(logTag + " OUTPUT", result);
+        assertThat(result).hasSize(insertedRecords.size());
+        assertThat(result).containsExactlyElementsIn(insertedRecords);
+    }
+
+    private static void readAndAssertRecordsNotExistUsingIds(List<Record> insertedRecords)
+            throws InterruptedException {
+        String logTag = "readAndAssertRecordsNotExistUsingIds";
+        logRecords(logTag + " INPUT", insertedRecords);
+
+        List<? extends Record> result = readRecordsUsingIds(insertedRecords);
+
+        logRecords(logTag + " OUTPUT", result);
+        assertThat(result).isEmpty();
+    }
+
+    private static List<? extends Record> readRecordsUsingIds(List<Record> insertedRecords)
+            throws InterruptedException {
+        Class<? extends Record> clazz = insertedRecords.get(0).getClass();
+        ReadRecordsRequestUsingIds.Builder<? extends Record> requestBuilder =
+                new ReadRecordsRequestUsingIds.Builder<>(clazz);
+        for (Record record : insertedRecords) {
+            requestBuilder.addId(record.getMetadata().getId());
+        }
+
+        return readRecords(requestBuilder.build());
+    }
+
+    /**
+     * Chunking is needed otherwise the insertion will fail with {@code
+     * android.os.TransactionTooLargeException: data parcel size 1xxxxxx bytes}.
+     */
+    private List<Record> insertRecordsWithChunking(RecordCreator creator, int numOfRecords)
+            throws InterruptedException {
+        List<Record> insertedRecords = new ArrayList<>();
+
+        for (int chunk = 0;
+                chunk <= numOfRecords / MAX_NUMBER_OF_RECORD_PER_INSERT_REQUEST;
+                chunk++) {
+            List<Record> recordsToInsert = new ArrayList<>();
+
+            for (int indexWithinChunk = 0;
+                    indexWithinChunk < MAX_NUMBER_OF_RECORD_PER_INSERT_REQUEST;
+                    indexWithinChunk++) {
+                int index = chunk * MAX_NUMBER_OF_RECORD_PER_INSERT_REQUEST + indexWithinChunk;
+                if (index >= numOfRecords) {
+                    break;
+                }
+
+                recordsToInsert.add(creator.create(index));
+            }
+            if (!recordsToInsert.isEmpty()) {
+                insertedRecords.addAll(insertRecords(recordsToInsert));
+            }
+        }
+
+        return insertedRecords;
+    }
+
+    private interface RecordCreator {
+        Record create(int index);
+    }
+
+    private String getBackupRestoreApkPackageName() {
+        PackageManager packageManager = mContext.getPackageManager();
+        List<PackageInfo> packageInfoList =
+                packageManager.getInstalledPackages(
+                        PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS));
+        for (PackageInfo packageInfo : packageInfoList) {
+            if (containsBackupRestoreInterAgentPermission(packageInfo.requestedPermissions)) {
+                return packageInfo.packageName;
+            }
+        }
+        throw new IllegalStateException("Backup Restore APK not found!");
+    }
+
+    private static boolean containsBackupRestoreInterAgentPermission(String[] array) {
+        if (array == null) {
+            return false;
+        }
+        for (String e : array) {
+            if (HEALTH_CONNECT_BACKUP_INTER_AGENT_PERMISSION.equals(e)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** Same as {@link #assertWithTimeout(Assertion)} except this method also returns a value. */
+    private static <T> T assertWithTimeoutAndReturn(@NonNull AssertionWithReturnValue<T> assertion)
+            throws Exception {
+        long start = System.currentTimeMillis();
+
+        while (true) {
+            try {
+                return assertion.run();
+            } catch (Throwable e) {
+                if (System.currentTimeMillis() - start < ASSERT_TIMEOUT_MILLIS) {
+                    try {
+                        Thread.sleep(ASSERT_RETRY_INTERVAL_MILLIS);
+                    } catch (InterruptedException ignored) {
+                        throw new RuntimeException(e);
+                    }
+                } else {
+                    throw e;
+                }
+            }
+        }
+    }
+
+    /**
+     * Repeatedly run {@code assertion} until either it succeeds or timed out. This is needed for
+     * cases such as waiting for async restore to be done.
+     *
+     * @param assertion The {@link Assertion assertion} to run.
+     */
+    private static void assertWithTimeout(@NonNull Assertion assertion) throws Exception {
+        assertWithTimeoutAndReturn(
+                () -> {
+                    assertion.run();
+                    return null;
+                });
+    }
+
+    private interface AssertionWithReturnValue<T> {
+        T run() throws Exception;
+    }
+
+    private interface Assertion {
+        void run() throws Exception;
+    }
+
+    private static void logRecords(String tag, List<? extends Record> records) {
+        if (!IS_DEBUGGING_TEST) {
+            return;
+        }
+        log("======================" + tag + "======================");
+        for (int index = 0; index < records.size(); index++) {
+            log("Record " + index + ":" + recordToString(records.get(index)));
+        }
+    }
+
+    private static String recordToString(Record record) {
+        StringBuilder stringBuilder = new StringBuilder();
+        appendToLog(stringBuilder, 0, record.getClass().getSimpleName());
+
+        if (record instanceof ActiveCaloriesBurnedRecord) {
+            appendToLog(
+                    stringBuilder,
+                    1,
+                    "Energy: ",
+                    ((ActiveCaloriesBurnedRecord) record).getEnergy().getInCalories());
+        }
+
+        if (record instanceof IntervalRecord) {
+            appendToLog(stringBuilder, 1, IntervalRecord.class.getSimpleName());
+            appendToLog(stringBuilder, 2, "Start Time: ", ((IntervalRecord) record).getStartTime());
+            appendToLog(stringBuilder, 2, "End Time: ", ((IntervalRecord) record).getEndTime());
+        } else if (record instanceof InstantRecord) {
+            appendToLog(stringBuilder, 1, InstantRecord.class.getSimpleName());
+            appendToLog(stringBuilder, 2, "Time: ", ((InstantRecord) record).getTime());
+        }
+
+        Metadata metadata = record.getMetadata();
+        appendToLog(stringBuilder, 1, "Metadata:");
+        appendToLog(stringBuilder, 2, "getId: ", metadata.getId());
+        appendToLog(stringBuilder, 2, "getClientRecordId: ", metadata.getClientRecordId());
+        appendToLog(stringBuilder, 2, "device model: ", metadata.getDevice().getModel());
+        appendToLog(stringBuilder, 2, "data origin: ", metadata.getDataOrigin().getPackageName());
+        appendToLog(stringBuilder, 2, "getLastModifiedTime: ", metadata.getLastModifiedTime());
+
+        return stringBuilder.toString();
+    }
+
+    private static void appendToLog(
+            StringBuilder stringBuilder, int numberOfSpacePrefix, Object... values) {
+        stringBuilder.append("\n");
+        if (numberOfSpacePrefix > 0) {
+            stringBuilder.append("-");
+            stringBuilder.append(" ".repeat(numberOfSpacePrefix));
+        }
+        for (Object value : values) {
+            stringBuilder.append(value);
+        }
+    }
+
+    private static void log(String msg) {
+        if (IS_DEBUGGING_TEST) {
+            System.out.println(LOG_TAG + ": " + msg);
+        }
+    }
+}
diff --git a/tests/integrationtests/src/android/healthconnect/tests/migration/HealthConnectPermissionsMigrationTest.java b/tests/integrationtests/src/android/healthconnect/tests/migration/HealthConnectPermissionsMigrationTest.java
index 6f00061..e4a3b63 100644
--- a/tests/integrationtests/src/android/healthconnect/tests/migration/HealthConnectPermissionsMigrationTest.java
+++ b/tests/integrationtests/src/android/healthconnect/tests/migration/HealthConnectPermissionsMigrationTest.java
@@ -19,6 +19,7 @@
 import static android.health.connect.HealthPermissions.MANAGE_HEALTH_PERMISSIONS;
 import static android.health.connect.HealthPermissions.READ_ACTIVE_CALORIES_BURNED;
 import static android.health.connect.HealthPermissions.WRITE_ACTIVE_CALORIES_BURNED;
+import static android.healthconnect.cts.utils.TestUtils.deleteAllStagedRemoteData;
 
 import static com.android.compatibility.common.util.FeatureUtil.AUTOMOTIVE_FEATURE;
 import static com.android.compatibility.common.util.FeatureUtil.hasSystemFeature;
@@ -71,12 +72,12 @@
         revokeAllHealthPermissions(DEFAULT_APP_PACKAGE, null);
         assertPermNotGrantedForApp(DEFAULT_APP_PACKAGE, READ_ACTIVE_CALORIES_BURNED);
         assertPermNotGrantedForApp(DEFAULT_APP_PACKAGE, WRITE_ACTIVE_CALORIES_BURNED);
-        PermissionsTestUtils.deleteAllStagedRemoteData();
+        deleteAllStagedRemoteData();
     }
 
     @After
     public void tearDown() {
-        PermissionsTestUtils.deleteAllStagedRemoteData();
+        deleteAllStagedRemoteData();
     }
 
     @Test
diff --git a/tests/integrationtests/src/android/healthconnect/tests/permissions/HealthConnectWithManagePermissionsTest.java b/tests/integrationtests/src/android/healthconnect/tests/permissions/HealthConnectWithManagePermissionsTest.java
index eee649d..073bb16 100644
--- a/tests/integrationtests/src/android/healthconnect/tests/permissions/HealthConnectWithManagePermissionsTest.java
+++ b/tests/integrationtests/src/android/healthconnect/tests/permissions/HealthConnectWithManagePermissionsTest.java
@@ -17,12 +17,14 @@
 package android.healthconnect.tests.permissions;
 
 import static android.health.connect.HealthPermissions.MANAGE_HEALTH_PERMISSIONS;
+import static android.healthconnect.cts.utils.TestUtils.deleteAllStagedRemoteData;
 
 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.fail;
 
 import android.Manifest;
@@ -41,6 +43,7 @@
 
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Integration tests for {@link HealthConnectManager} Permission-related APIs.
@@ -82,12 +85,12 @@
         revokePermissionViaPackageManager(DEFAULT_APP_PACKAGE, DEFAULT_PERM_2);
         assertPermNotGrantedForApp(DEFAULT_APP_PACKAGE, DEFAULT_PERM);
         assertPermNotGrantedForApp(DEFAULT_APP_PACKAGE, DEFAULT_PERM_2);
-        PermissionsTestUtils.deleteAllStagedRemoteData();
+        deleteAllStagedRemoteData();
     }
 
     @After
     public void tearDown() {
-        PermissionsTestUtils.deleteAllStagedRemoteData();
+        deleteAllStagedRemoteData();
     }
 
     @Test
@@ -238,6 +241,163 @@
     }
 
     @Test
+    public void testGetHealthPermissionsFlags_returnsFlags() {
+        int permFlags =
+                PackageManager.FLAG_PERMISSION_USER_SET
+                        | PackageManager.FLAG_PERMISSION_AUTO_REVOKED;
+
+        int perm2Flags = PackageManager.FLAG_PERMISSION_USER_FIXED;
+        updatePermissionsFlagsViaPackageManager(DEFAULT_APP_PACKAGE, DEFAULT_PERM, permFlags);
+        updatePermissionsFlagsViaPackageManager(DEFAULT_APP_PACKAGE, DEFAULT_PERM_2, perm2Flags);
+
+        Map<String, Integer> permissionsFlags =
+                getHealthPermissionsFlags(
+                        DEFAULT_APP_PACKAGE, List.of(DEFAULT_PERM, DEFAULT_PERM_2));
+
+        assertThat(permissionsFlags.keySet()).containsExactly(DEFAULT_PERM, DEFAULT_PERM_2);
+        assertFlagsSet(permissionsFlags.get(DEFAULT_PERM), permFlags);
+        assertFlagsSet(permissionsFlags.get(DEFAULT_PERM_2), perm2Flags);
+    }
+
+    @Test
+    public void testGetHealthPermissionsFlags_invalidPermission_throwsException() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        getHealthPermissionsFlags(
+                                DEFAULT_APP_PACKAGE,
+                                List.of(INVALID_PERM, DEFAULT_PERM, DEFAULT_PERM_2)));
+    }
+
+    @Test
+    public void testGetHealthPermissionsFlags_nonHealthPermission_throwsException() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        getHealthPermissionsFlags(
+                                DEFAULT_APP_PACKAGE,
+                                List.of(DEFAULT_PERM, NON_HEALTH_PERM, DEFAULT_PERM_2)));
+    }
+
+    @Test
+    public void testGetHealthPermissionsFlags_undeclaredPermissions_throwsException() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        getHealthPermissionsFlags(
+                                DEFAULT_APP_PACKAGE,
+                                List.of(DEFAULT_PERM, DEFAULT_PERM_2, UNDECLARED_PERM)));
+    }
+
+    @Test
+    public void testGetHealthPermissionsFlags_emptyPermissions_returnsEmptyMap() {
+        assertThat(getHealthPermissionsFlags(DEFAULT_APP_PACKAGE, List.of())).isEmpty();
+    }
+
+    @Test
+    public void testGetHealthPermissionsFlags_invalidPackage_throwsException() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        getHealthPermissionsFlags(
+                                INEXISTENT_APP_PACKAGE, List.of(DEFAULT_PERM, DEFAULT_PERM)));
+    }
+
+    @Test
+    public void testGetHealthPermissionsFlags_nullPackage_throwsException() {
+        assertThrows(
+                NullPointerException.class,
+                () -> getHealthPermissionsFlags(null, List.of(DEFAULT_PERM, DEFAULT_PERM)));
+    }
+
+    @Test
+    public void testGetHealthPermissionsFlags_nullPermissions_throwsException() {
+        assertThrows(
+                NullPointerException.class,
+                () -> getHealthPermissionsFlags(DEFAULT_APP_PACKAGE, null));
+    }
+
+    @Test
+    public void testMakeHealthPermissionsRequestable_resetsUserFixedFlagToZero() {
+        updatePermissionsFlagsViaPackageManager(
+                DEFAULT_APP_PACKAGE,
+                DEFAULT_PERM,
+                PackageManager.FLAG_PERMISSION_USER_SET
+                        | PackageManager.FLAG_PERMISSION_AUTO_REVOKED);
+        int permFlags = getPermissionsFlagsViaPackageManager(DEFAULT_APP_PACKAGE, DEFAULT_PERM);
+        updatePermissionsFlagsViaPackageManager(
+                DEFAULT_APP_PACKAGE, DEFAULT_PERM_2, PackageManager.FLAG_PERMISSION_USER_FIXED);
+        int perm2Flags = getPermissionsFlagsViaPackageManager(DEFAULT_APP_PACKAGE, DEFAULT_PERM_2);
+
+        makeHealthPermissionsRequestable(
+                DEFAULT_APP_PACKAGE, List.of(DEFAULT_PERM, DEFAULT_PERM_2));
+
+        int mask = PackageManager.FLAG_PERMISSION_USER_FIXED;
+        assertThat(getPermissionsFlagsViaPackageManager(DEFAULT_APP_PACKAGE, DEFAULT_PERM))
+                .isEqualTo(permFlags & ~mask);
+        assertThat(getPermissionsFlagsViaPackageManager(DEFAULT_APP_PACKAGE, DEFAULT_PERM_2))
+                .isEqualTo(perm2Flags & ~mask);
+    }
+
+    @Test
+    public void testMakeHealthPermissionsRequestable_invalidPermission_throwsException() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        makeHealthPermissionsRequestable(
+                                DEFAULT_APP_PACKAGE,
+                                List.of(INVALID_PERM, DEFAULT_PERM, DEFAULT_PERM_2)));
+    }
+
+    @Test
+    public void testMakeHealthPermissionsRequestable_nonHealthPermission_throwsException() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        makeHealthPermissionsRequestable(
+                                DEFAULT_APP_PACKAGE,
+                                List.of(DEFAULT_PERM, NON_HEALTH_PERM, DEFAULT_PERM_2)));
+    }
+
+    @Test
+    public void testMakeHealthPermissionsRequestable_undeclaredPermissions_throwsException() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        makeHealthPermissionsRequestable(
+                                DEFAULT_APP_PACKAGE,
+                                List.of(DEFAULT_PERM, DEFAULT_PERM_2, UNDECLARED_PERM)));
+    }
+
+    @Test
+    public void testMakeHealthPermissionsRequestable_emptyPermissions_doesNotThrow() {
+        makeHealthPermissionsRequestable(DEFAULT_APP_PACKAGE, List.of());
+    }
+
+    @Test
+    public void testMakeHealthPermissionsRequestable_invalidPackage_throwsException() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        makeHealthPermissionsRequestable(
+                                INEXISTENT_APP_PACKAGE, List.of(DEFAULT_PERM, DEFAULT_PERM)));
+    }
+
+    @Test
+    public void testMakeHealthPermissionsRequestable_nullPackage_throwsException() {
+        assertThrows(
+                NullPointerException.class,
+                () -> makeHealthPermissionsRequestable(null, List.of(DEFAULT_PERM, DEFAULT_PERM)));
+    }
+
+    @Test
+    public void testMakeHealthPermissionsRequestable_nullPermissions_throwsException() {
+        assertThrows(
+                NullPointerException.class,
+                () -> makeHealthPermissionsRequestable(DEFAULT_APP_PACKAGE, null));
+    }
+
+    @Test
     public void testRevokeAllHealthPermissions_appHasNoPermissionsGranted_success()
             throws Exception {
         revokeAllHealthPermissions(DEFAULT_APP_PACKAGE, /* reason= */ null);
@@ -287,7 +447,7 @@
             assertNotNull(exception);
         }
         assertPermNotGrantedForApp(DEFAULT_APP_PACKAGE, DEFAULT_PERM);
-        PermissionsTestUtils.deleteAllStagedRemoteData();
+        deleteAllStagedRemoteData();
 
         // Revoke permission
         runWithShellPermissionIdentity(
@@ -318,6 +478,19 @@
         } catch (IllegalStateException exception) {
             assertNotNull(exception);
         }
+
+        assertThrows(
+                IllegalStateException.class,
+                () ->
+                        getHealthPermissionsFlags(
+                                DEFAULT_APP_PACKAGE, List.of(DEFAULT_PERM, DEFAULT_PERM_2)));
+
+        assertThrows(
+                IllegalStateException.class,
+                () ->
+                        makeHealthPermissionsRequestable(
+                                DEFAULT_APP_PACKAGE, List.of(DEFAULT_PERM, DEFAULT_PERM_2)));
+
         runWithShellPermissionIdentity(
                 PermissionsTestUtils::finishMigration,
                 Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA);
@@ -344,6 +517,29 @@
                 Manifest.permission.REVOKE_RUNTIME_PERMISSIONS);
     }
 
+    private int getPermissionsFlagsViaPackageManager(String packageName, String permName) {
+        return runWithShellPermissionIdentity(
+                () ->
+                        mContext.getPackageManager()
+                                .getPermissionFlags(permName, packageName, mContext.getUser()),
+                Manifest.permission.GRANT_RUNTIME_PERMISSIONS);
+    }
+
+    private void updatePermissionsFlagsViaPackageManager(
+            String packageName, String permName, int flags) {
+        int mask =
+                PackageManager.FLAG_PERMISSION_USER_SET
+                        | PackageManager.FLAG_PERMISSION_USER_FIXED
+                        | PackageManager.FLAG_PERMISSION_AUTO_REVOKED;
+
+        runWithShellPermissionIdentity(
+                () ->
+                        mContext.getPackageManager()
+                                .updatePermissionFlags(
+                                        permName, packageName, mask, flags, mContext.getUser()),
+                Manifest.permission.GRANT_RUNTIME_PERMISSIONS);
+    }
+
     private void assertPermGrantedForApp(String packageName, String permName) {
         assertThat(mContext.getPackageManager().checkPermission(permName, packageName))
                 .isEqualTo(PackageManager.PERMISSION_GRANTED);
@@ -414,4 +610,37 @@
             throw cause instanceof RuntimeException ? (RuntimeException) cause : e;
         }
     }
+
+    private Map<String, Integer> getHealthPermissionsFlags(
+            String packageName, List<String> permissions) {
+        try {
+            return runWithShellPermissionIdentity(
+                    () -> mHealthConnectManager.getHealthPermissionsFlags(packageName, permissions),
+                    MANAGE_HEALTH_PERMISSIONS);
+        } catch (RuntimeException e) {
+            // runWithShellPermissionIdentity wraps and rethrows all exceptions as RuntimeException,
+            // but we need the original RuntimeException if there is one.
+            final Throwable cause = e.getCause();
+            throw cause instanceof RuntimeException ? (RuntimeException) cause : e;
+        }
+    }
+
+    private void makeHealthPermissionsRequestable(String packageName, List<String> permissions) {
+        try {
+            runWithShellPermissionIdentity(
+                    () ->
+                            mHealthConnectManager.makeHealthPermissionsRequestable(
+                                    packageName, permissions),
+                    MANAGE_HEALTH_PERMISSIONS);
+        } catch (RuntimeException e) {
+            // runWithShellPermissionIdentity wraps and rethrows all exceptions as RuntimeException,
+            // but we need the original RuntimeException if there is one.
+            final Throwable cause = e.getCause();
+            throw cause instanceof RuntimeException ? (RuntimeException) cause : e;
+        }
+    }
+
+    private static void assertFlagsSet(int actualFlags, int expectedFlags) {
+        assertThat((actualFlags & expectedFlags)).isEqualTo(expectedFlags);
+    }
 }
diff --git a/tests/integrationtests/src/android/healthconnect/tests/permissions/HealthConnectWithoutManagePermissionsTest.java b/tests/integrationtests/src/android/healthconnect/tests/permissions/HealthConnectWithoutManagePermissionsTest.java
index ec2967d..4807520 100644
--- a/tests/integrationtests/src/android/healthconnect/tests/permissions/HealthConnectWithoutManagePermissionsTest.java
+++ b/tests/integrationtests/src/android/healthconnect/tests/permissions/HealthConnectWithoutManagePermissionsTest.java
@@ -16,6 +16,7 @@
 
 package android.healthconnect.tests.permissions;
 
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.fail;
 
 import android.content.Context;
@@ -29,6 +30,8 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.List;
+
 /**
  * Integration tests for {@link HealthConnectManager} Permission-related APIs.
  *
@@ -89,4 +92,22 @@
                 "Expected SecurityException due to not holding"
                         + "android.permission.MANAGE_HEALTH_PERMISSIONS.");
     }
+
+    @Test
+    public void testGetHealthPermissionsFlags_noManageHealthPermissions_throwsSecurityException() {
+        assertThrows(
+                SecurityException.class,
+                () ->
+                        mHealthConnectManager.getHealthPermissionsFlags(
+                                DEFAULT_APP_PACKAGE, List.of()));
+    }
+
+    @Test
+    public void testMakeHealthPermissionsRequestable_noManageHealthPermissions_securityException() {
+        assertThrows(
+                SecurityException.class,
+                () ->
+                        mHealthConnectManager.makeHealthPermissionsRequestable(
+                                DEFAULT_APP_PACKAGE, List.of()));
+    }
 }
diff --git a/tests/integrationtests/src/android/healthconnect/tests/permissions/PermissionsTestUtils.java b/tests/integrationtests/src/android/healthconnect/tests/permissions/PermissionsTestUtils.java
index f521767..b581932 100644
--- a/tests/integrationtests/src/android/healthconnect/tests/permissions/PermissionsTestUtils.java
+++ b/tests/integrationtests/src/android/healthconnect/tests/permissions/PermissionsTestUtils.java
@@ -16,8 +16,6 @@
 
 package android.healthconnect.tests.permissions;
 
-import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
-
 import static com.google.common.truth.Truth.assertThat;
 
 
@@ -80,15 +78,4 @@
                 });
         assertThat(latch.await(3, TimeUnit.SECONDS)).isTrue();
     }
-
-    public static void deleteAllStagedRemoteData() {
-        Context context = ApplicationProvider.getApplicationContext();
-        HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
-        assertThat(service).isNotNull();
-        runWithShellPermissionIdentity(
-                () ->
-                        // TODO(b/241542162): Avoid reflection once TestApi can be called from CTS
-                        service.getClass().getMethod("deleteAllStagedRemoteData").invoke(service),
-                "android.permission.DELETE_STAGED_HEALTH_CONNECT_REMOTE_DATA");
-    }
 }
diff --git a/tests/integrationtests/src/android/healthconnect/tests/storage/AggregationTest.java b/tests/integrationtests/src/android/healthconnect/tests/storage/AggregationTest.java
deleted file mode 100644
index abcdf6f..0000000
--- a/tests/integrationtests/src/android/healthconnect/tests/storage/AggregationTest.java
+++ /dev/null
@@ -1,131 +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.tests.storage;
-
-import static android.health.connect.datatypes.StepsRecord.STEPS_COUNT_TOTAL;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static java.time.ZoneOffset.UTC;
-import static java.time.temporal.ChronoUnit.DAYS;
-
-import android.app.UiAutomation;
-import android.content.Context;
-import android.health.connect.AggregateRecordsGroupedByPeriodResponse;
-import android.health.connect.AggregateRecordsRequest;
-import android.health.connect.HealthConnectException;
-import android.health.connect.HealthConnectManager;
-import android.health.connect.HealthPermissions;
-import android.health.connect.LocalTimeRangeFilter;
-import android.os.OutcomeReceiver;
-
-import androidx.annotation.NonNull;
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.time.Instant;
-import java.time.LocalDateTime;
-import java.time.Period;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-
-@RunWith(AndroidJUnit4.class)
-public class AggregationTest {
-    private static final String MANAGE_HEALTH_DATA =
-            HealthPermissions.MANAGE_HEALTH_DATA_PERMISSION;
-    private static final int TIMEOUT_SECONDS = 5;
-    private Context mContext;
-    private HealthConnectManager mService;
-
-    @Before
-    public void setup() {
-        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
-        mService = mContext.getSystemService(HealthConnectManager.class);
-        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
-        uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA);
-    }
-
-    @Test
-    // TODO(b/298249754): Add this test in CTS
-    public void groupByPeriod_expectCorrectSlices() throws Exception {
-        // TODO(b/298249754): Add some test data and check it's correct
-        Instant startTime = Instant.now().minus(40, DAYS);
-        LocalDateTime startLocalTime =
-                LocalDateTime.ofInstant(Instant.ofEpochMilli(startTime.toEpochMilli()), UTC);
-        Instant endTime = startTime.plus(35, DAYS);
-        LocalDateTime endLocalTime =
-                LocalDateTime.ofInstant(Instant.ofEpochMilli(endTime.toEpochMilli()), UTC);
-
-        // Due to the Parcel implementation, we have to set local time at UTC zone
-        AggregateRecordsRequest<Long> request =
-                new AggregateRecordsRequest.Builder<Long>(
-                                new LocalTimeRangeFilter.Builder()
-                                        .setStartTime(startLocalTime)
-                                        .setEndTime(endLocalTime)
-                                        .build())
-                        .addAggregationType(STEPS_COUNT_TOTAL)
-                        .build();
-        List<AggregateRecordsGroupedByPeriodResponse<Long>> response =
-                getAggregateResponseGroupByPeriod(request, Period.ofMonths(1));
-
-        assertThat(response.size()).isEqualTo(2);
-        assertThat(response.get(0).getStartTime()).isEqualTo(startLocalTime);
-        assertThat(response.get(0).getEndTime()).isEqualTo(startLocalTime.plusMonths(1));
-        assertThat(response.get(1).getStartTime()).isEqualTo(startLocalTime.plusMonths(1));
-        assertThat(response.get(1).getEndTime()).isEqualTo(endLocalTime);
-    }
-
-    private <T> List<AggregateRecordsGroupedByPeriodResponse<T>> getAggregateResponseGroupByPeriod(
-            AggregateRecordsRequest<T> request, Period period) throws InterruptedException {
-        AtomicReference<List<AggregateRecordsGroupedByPeriodResponse<T>>> response =
-                new AtomicReference<>();
-        AtomicReference<HealthConnectException> healthConnectExceptionAtomicReference =
-                new AtomicReference<>();
-
-        CountDownLatch latch = new CountDownLatch(1);
-        mService.aggregateGroupByPeriod(
-                request,
-                period,
-                Executors.newSingleThreadExecutor(),
-                new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(List<AggregateRecordsGroupedByPeriodResponse<T>> result) {
-                        response.set(result);
-                        latch.countDown();
-                    }
-
-                    @Override
-                    public void onError(@NonNull HealthConnectException healthConnectException) {
-                        healthConnectExceptionAtomicReference.set(healthConnectException);
-                        latch.countDown();
-                    }
-                });
-        assertThat(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue();
-        if (healthConnectExceptionAtomicReference.get() != null) {
-            throw healthConnectExceptionAtomicReference.get();
-        }
-
-        return response.get();
-    }
-}
diff --git a/tests/unittests/src/android/healthconnect/HealthPermissionsTest.java b/tests/unittests/src/android/healthconnect/HealthPermissionsTest.java
index 26d5ebb..0f92754 100644
--- a/tests/unittests/src/android/healthconnect/HealthPermissionsTest.java
+++ b/tests/unittests/src/android/healthconnect/HealthPermissionsTest.java
@@ -22,20 +22,29 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
+import static org.mockito.Mockito.reset;
+
 import android.content.Context;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PermissionInfo;
 import android.health.connect.HealthConnectManager;
+import android.health.connect.HealthDataCategory;
 import android.health.connect.HealthPermissions;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 
 import java.util.Set;
 
+@RunWith(AndroidJUnit4.class)
 public class HealthPermissionsTest {
     private static final String FAIL_MESSAGE =
             "Add new health permission to ALL_EXPECTED_HEALTH_PERMISSIONS and "
@@ -47,10 +56,12 @@
     // sets.
     private static final Set<String> ALL_EXPECTED_HEALTH_PERMISSIONS =
             Set.of(
+                    HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND,
                     HealthPermissions.READ_ACTIVE_CALORIES_BURNED,
                     HealthPermissions.READ_DISTANCE,
                     HealthPermissions.READ_ELEVATION_GAINED,
                     HealthPermissions.READ_EXERCISE,
+                    HealthPermissions.READ_EXERCISE_ROUTES_ALL,
                     HealthPermissions.READ_FLOORS_CLIMBED,
                     HealthPermissions.READ_STEPS,
                     HealthPermissions.READ_TOTAL_CALORIES_BURNED,
@@ -120,13 +131,20 @@
                     HealthPermissions.WRITE_RESTING_HEART_RATE);
     private PackageManager mPackageManager;
     private Context mContext;
+    @Mock private PackageInfo mPackageInfo1;
 
     @Before
     public void setUp() {
+        MockitoAnnotations.initMocks(this);
         mContext = InstrumentationRegistry.getTargetContext();
         mPackageManager = mContext.getPackageManager();
     }
 
+    @After
+    public void tearDown() {
+        reset(mPackageInfo1);
+    }
+
     @Test
     public void testHealthGroupPermissions_noUnexpectedPermissionsDefined() throws Exception {
         PermissionInfo[] permissionInfos = getHealthPermissionInfos();
@@ -165,6 +183,78 @@
                 .isEqualTo(PackageManager.PERMISSION_GRANTED);
     }
 
+    @Test
+    public void testGetDataCategoriesWithWritePermissionsForPackage_returnsCorrectSet() {
+        mPackageInfo1.requestedPermissions =
+                new String[] {
+                    HealthPermissions.WRITE_STEPS,
+                    HealthPermissions.WRITE_SLEEP,
+                    HealthPermissions.READ_HEART_RATE,
+                    HealthPermissions.WRITE_OVULATION_TEST
+                };
+        mPackageInfo1.requestedPermissionsFlags =
+                new int[] {
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    0,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED
+                };
+
+        Set<Integer> expectedResult =
+                Set.of(HealthDataCategory.ACTIVITY, HealthDataCategory.CYCLE_TRACKING);
+        Set<Integer> actualResult =
+                HealthPermissions.getDataCategoriesWithWritePermissionsForPackage(
+                        mPackageInfo1, mContext);
+        assertThat(actualResult).isEqualTo(expectedResult);
+    }
+
+    @Test
+    public void
+            testPackageHasWriteHealthPermissionsForCategory_ifNoWritePermissions_returnsFalse() {
+        mPackageInfo1.requestedPermissions =
+                new String[] {
+                    HealthPermissions.WRITE_EXERCISE,
+                    HealthPermissions.WRITE_SLEEP,
+                    HealthPermissions.READ_HEART_RATE,
+                    HealthPermissions.WRITE_OVULATION_TEST
+                };
+        mPackageInfo1.requestedPermissionsFlags =
+                new int[] {
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    0,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED
+                };
+
+        assertThat(
+                        HealthPermissions.getPackageHasWriteHealthPermissionsForCategory(
+                                mPackageInfo1, HealthDataCategory.SLEEP, mContext))
+                .isFalse();
+    }
+
+    @Test
+    public void testPackageHasWriteHealthPermissionsForCategory_ifWritePermissions_returnsTrue() {
+        mPackageInfo1.requestedPermissions =
+                new String[] {
+                    HealthPermissions.WRITE_STEPS,
+                    HealthPermissions.WRITE_SLEEP,
+                    HealthPermissions.READ_HEART_RATE,
+                    HealthPermissions.WRITE_OVULATION_TEST
+                };
+        mPackageInfo1.requestedPermissionsFlags =
+                new int[] {
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    0,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED
+                };
+
+        assertThat(
+                        HealthPermissions.getPackageHasWriteHealthPermissionsForCategory(
+                                mPackageInfo1, HealthDataCategory.ACTIVITY, mContext))
+                .isTrue();
+    }
+
     private PermissionInfo[] getHealthPermissionInfos() throws Exception {
         String healthControllerPackageName = getControllerUiPackageName();
 
diff --git a/tests/unittests/src/com/android/server/healthconnect/FakePreferenceHelper.java b/tests/unittests/src/com/android/server/healthconnect/FakePreferenceHelper.java
new file mode 100644
index 0000000..35be904
--- /dev/null
+++ b/tests/unittests/src/com/android/server/healthconnect/FakePreferenceHelper.java
@@ -0,0 +1,43 @@
+/*
+ * 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.server.healthconnect;
+
+import com.android.server.healthconnect.storage.datatypehelpers.PreferenceHelper;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Fake impl of Preference Helper for use in testing.
+ *
+ * <p>This is an in-memory impl, and doesn't persist changes to the database.
+ */
+public class FakePreferenceHelper extends PreferenceHelper {
+
+    public FakePreferenceHelper() {
+        mPreferences = new ConcurrentHashMap<>();
+    }
+
+    @Override
+    public synchronized void insertOrReplacePreference(String key, String value) {
+        getPreferences().put(key, value);
+    }
+
+    @Override
+    public synchronized void clearCache() {
+        mPreferences.clear();
+    }
+}
diff --git a/tests/unittests/src/com/android/server/healthconnect/storage/HealthConnectDailyJobsTest.java b/tests/unittests/src/com/android/server/healthconnect/HealthConnectDailyJobsTest.java
similarity index 94%
rename from tests/unittests/src/com/android/server/healthconnect/storage/HealthConnectDailyJobsTest.java
rename to tests/unittests/src/com/android/server/healthconnect/HealthConnectDailyJobsTest.java
index 6fedd06..064c480 100644
--- a/tests/unittests/src/com/android/server/healthconnect/storage/HealthConnectDailyJobsTest.java
+++ b/tests/unittests/src/com/android/server/healthconnect/HealthConnectDailyJobsTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.healthconnect.storage;
+package com.android.server.healthconnect;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.times;
@@ -24,7 +24,6 @@
 import android.app.job.JobScheduler;
 import android.content.Context;
 
-import com.android.server.healthconnect.HealthConnectDailyJobs;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/tests/unittests/src/com/android/server/healthconnect/HealthConnectManagerServiceTest.java b/tests/unittests/src/com/android/server/healthconnect/HealthConnectManagerServiceTest.java
index 0413b7d..a66fdb9 100644
--- a/tests/unittests/src/com/android/server/healthconnect/HealthConnectManagerServiceTest.java
+++ b/tests/unittests/src/com/android/server/healthconnect/HealthConnectManagerServiceTest.java
@@ -19,6 +19,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.times;
@@ -44,6 +45,7 @@
 import com.android.server.appop.AppOpsManagerLocal;
 import com.android.server.healthconnect.backuprestore.BackupRestore;
 import com.android.server.healthconnect.migration.MigrationStateChangeJob;
+import com.android.server.healthconnect.storage.datatypehelpers.HealthDataCategoryPriorityHelper;
 import com.android.server.healthconnect.storage.datatypehelpers.PreferenceHelper;
 
 import com.google.common.truth.Truth;
@@ -64,6 +66,7 @@
             new ExtendedMockitoRule.Builder(this)
                     .mockStatic(PreferenceHelper.class)
                     .mockStatic(BackupRestore.BackupRestoreJobService.class)
+                    .mockStatic(HealthDataCategoryPriorityHelper.class)
                     .setStrictness(Strictness.LENIENT)
                     .build();
 
@@ -75,6 +78,7 @@
     @Mock private PermissionManager mPermissionManager;
     @Mock private AppOpsManagerLocal mAppOpsManagerLocal;
     @Mock private PreferenceHelper mPreferenceHelper;
+    @Mock private HealthDataCategoryPriorityHelper mHealthDataCategoryPriorityHelper;
     private HealthConnectManagerService mHealthConnectManagerService;
 
     @Before
@@ -118,7 +122,11 @@
         when(mMockTargetUser.getUserHandle()).thenReturn(UserHandle.CURRENT);
         when(mContext.getApplicationContext()).thenReturn(mContext);
         when(PreferenceHelper.getInstance()).thenReturn(mPreferenceHelper);
-
+        when(HealthDataCategoryPriorityHelper.getInstance())
+                .thenReturn(mHealthDataCategoryPriorityHelper);
+        doNothing()
+                .when(mHealthDataCategoryPriorityHelper)
+                .maybeAddInactiveAppsToPriorityList(mContext);
         mHealthConnectManagerService = new HealthConnectManagerService(mContext);
     }
 
@@ -145,7 +153,7 @@
         verify(mJobScheduler, times(1)).cancelAll();
         verify(mJobScheduler, timeout(5000).times(1)).schedule(any());
         ExtendedMockito.verify(
-                () ->
-                        BackupRestore.BackupRestoreJobService.cancelAllJobs(eq(mContext)));
+                () -> BackupRestore.BackupRestoreJobService.cancelAllJobs(eq(mContext)));
+        verify(mHealthDataCategoryPriorityHelper).maybeAddInactiveAppsToPriorityList(any());
     }
 }
diff --git a/tests/unittests/src/com/android/server/healthconnect/HealthConnectServiceImplTest.java b/tests/unittests/src/com/android/server/healthconnect/HealthConnectServiceImplTest.java
index b7d49c1..5c26019 100644
--- a/tests/unittests/src/com/android/server/healthconnect/HealthConnectServiceImplTest.java
+++ b/tests/unittests/src/com/android/server/healthconnect/HealthConnectServiceImplTest.java
@@ -110,6 +110,8 @@
                     "revokeHealthPermission",
                     "revokeAllHealthPermissions",
                     "getGrantedHealthPermissions",
+                    "getHealthPermissionsFlags",
+                    "makeHealthPermissionsRequestable",
                     "getHistoricalAccessStartDateInMilliseconds",
                     "insertRecords",
                     "aggregateRecords",
@@ -155,6 +157,7 @@
                     .build();
 
     @Mock private TransactionManager mTransactionManager;
+    @Mock private HealthConnectDeviceConfigManager mDeviceConfigManager;
     @Mock private HealthConnectPermissionHelper mHealthConnectPermissionHelper;
     @Mock private MigrationCleaner mMigrationCleaner;
     @Mock private FirstGrantTimeManager mFirstGrantTimeManager;
@@ -189,6 +192,7 @@
         mHealthConnectService =
                 new HealthConnectServiceImpl(
                         mTransactionManager,
+                        mDeviceConfigManager,
                         mHealthConnectPermissionHelper,
                         mMigrationCleaner,
                         mFirstGrantTimeManager,
diff --git a/tests/unittests/src/com/android/server/healthconnect/storage/HealthConnectUserContextTest.java b/tests/unittests/src/com/android/server/healthconnect/HealthConnectUserContextTest.java
similarity index 91%
rename from tests/unittests/src/com/android/server/healthconnect/storage/HealthConnectUserContextTest.java
rename to tests/unittests/src/com/android/server/healthconnect/HealthConnectUserContextTest.java
index daebfee..6d2178e 100644
--- a/tests/unittests/src/com/android/server/healthconnect/storage/HealthConnectUserContextTest.java
+++ b/tests/unittests/src/com/android/server/healthconnect/HealthConnectUserContextTest.java
@@ -14,13 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.server.healthconnect.storage;
+package com.android.server.healthconnect;
 
 import android.os.UserHandle;
 
 import androidx.test.platform.app.InstrumentationRegistry;
 
-import com.android.server.healthconnect.HealthConnectUserContext;
 
 import com.google.common.truth.Truth;
 
diff --git a/tests/unittests/src/com/android/server/healthconnect/backuprestore/BackupRestoreTest.java b/tests/unittests/src/com/android/server/healthconnect/backuprestore/BackupRestoreTest.java
index bd0aca3..e0e444a 100644
--- a/tests/unittests/src/com/android/server/healthconnect/backuprestore/BackupRestoreTest.java
+++ b/tests/unittests/src/com/android/server/healthconnect/backuprestore/BackupRestoreTest.java
@@ -35,11 +35,13 @@
 import static com.android.server.healthconnect.backuprestore.BackupRestore.DATA_RESTORE_STATE_KEY;
 import static com.android.server.healthconnect.backuprestore.BackupRestore.DATA_STAGING_TIMEOUT_CANCELLED_KEY;
 import static com.android.server.healthconnect.backuprestore.BackupRestore.DATA_STAGING_TIMEOUT_KEY;
+import static com.android.server.healthconnect.backuprestore.BackupRestore.INTERNAL_RESTORE_STATE_MERGING_DONE_OLD_CODE;
 import static com.android.server.healthconnect.backuprestore.BackupRestore.INTERNAL_RESTORE_STATE_MERGING_DONE;
 import static com.android.server.healthconnect.backuprestore.BackupRestore.INTERNAL_RESTORE_STATE_MERGING_IN_PROGRESS;
 import static com.android.server.healthconnect.backuprestore.BackupRestore.INTERNAL_RESTORE_STATE_STAGING_DONE;
 import static com.android.server.healthconnect.backuprestore.BackupRestore.INTERNAL_RESTORE_STATE_STAGING_IN_PROGRESS;
 import static com.android.server.healthconnect.backuprestore.BackupRestore.INTERNAL_RESTORE_STATE_WAITING_FOR_STAGING;
+import static com.android.server.healthconnect.backuprestore.BackupRestore.STAGED_DATABASE_NAME;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -72,11 +74,13 @@
 
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
 import com.android.modules.utils.testing.ExtendedMockitoRule;
+import com.android.server.healthconnect.FakePreferenceHelper;
 import com.android.server.healthconnect.migration.MigrationStateManager;
 import com.android.server.healthconnect.permission.FirstGrantTimeManager;
 import com.android.server.healthconnect.permission.GrantTimeXmlHelper;
-import com.android.server.healthconnect.storage.HealthConnectDatabase;
+import com.android.server.healthconnect.permission.UserGrantTimeState;
 import com.android.server.healthconnect.storage.TransactionManager;
+import com.android.server.healthconnect.storage.datatypehelpers.AppInfoHelper;
 import com.android.server.healthconnect.storage.datatypehelpers.PreferenceHelper;
 import com.android.server.healthconnect.utils.FilesUtil;
 
@@ -109,35 +113,49 @@
                     .mockStatic(PreferenceHelper.class)
                     .mockStatic(TransactionManager.class)
                     .mockStatic(BackupRestore.BackupRestoreJobService.class)
-                    .mockStatic(GrantTimeXmlHelper.class)
+                    .mockStatic(AppInfoHelper.class)
+                    .mockStatic(SQLiteDatabase.class)
+                    .spyStatic(GrantTimeXmlHelper.class)
+                    .spyStatic(BackupRestore.StagedDatabaseContext.class)
                     .setStrictness(Strictness.LENIENT)
                     .build();
 
     @Mock Context mServiceContext;
     @Mock private TransactionManager mTransactionManager;
-    @Mock private PreferenceHelper mPreferenceHelper;
+    @Mock private AppInfoHelper mAppInfoHelper;
+    @Mock private BackupRestore.StagedDatabaseContext mStagedDbContext;
     @Mock private FirstGrantTimeManager mFirstGrantTimeManager;
     @Mock private MigrationStateManager mMigrationStateManager;
     @Mock private Context mContext;
     @Mock private JobScheduler mJobScheduler;
     @Captor ArgumentCaptor<JobInfo> mJobInfoArgumentCaptor;
     private BackupRestore mBackupRestore;
+    private final PreferenceHelper mFakePreferenceHelper = new FakePreferenceHelper();
     private UserHandle mUserHandle = UserHandle.of(UserHandle.myUserId());
     private File mMockDataDirectory;
     private File mMockBackedDataDirectory;
+    private File mMockStagedDataDirectory;
 
     @Before
     public void setUp() throws Exception {
         mContext = InstrumentationRegistry.getInstrumentation().getContext();
         mMockDataDirectory = mContext.getDir("mock_data", Context.MODE_PRIVATE);
         mMockBackedDataDirectory = mContext.getDir("mock_backed_data", Context.MODE_PRIVATE);
-        when(Environment.getDataDirectory()).thenReturn(mMockBackedDataDirectory);
-        when(PreferenceHelper.getInstance()).thenReturn(mPreferenceHelper);
+        mMockStagedDataDirectory = mContext.getDir("mock_staged_data", Context.MODE_PRIVATE);
+        when(Environment.getDataDirectory()).thenReturn(mMockDataDirectory);
+
+        when(PreferenceHelper.getInstance()).thenReturn(mFakePreferenceHelper);
         when(TransactionManager.getInitialisedInstance()).thenReturn(mTransactionManager);
+        when(AppInfoHelper.getInstance()).thenReturn(mAppInfoHelper);
         when(mJobScheduler.forNamespace(BACKUP_RESTORE_JOBS_NAMESPACE)).thenReturn(mJobScheduler);
         when(mServiceContext.getUser()).thenReturn(mUserHandle);
         when(mServiceContext.getSystemService(JobScheduler.class)).thenReturn(mJobScheduler);
 
+        when(BackupRestore.StagedDatabaseContext.create(mServiceContext, mUserHandle))
+                .thenReturn(mStagedDbContext);
+        when(mStagedDbContext.getDatabasePath(STAGED_DATABASE_NAME))
+                .thenReturn(new File(mMockStagedDataDirectory, STAGED_DATABASE_NAME));
+
         mBackupRestore =
                 new BackupRestore(mFirstGrantTimeManager, mMigrationStateManager, mServiceContext);
     }
@@ -146,33 +164,24 @@
     public void tearDown() {
         FilesUtil.deleteDir(mMockDataDirectory);
         FilesUtil.deleteDir(mMockBackedDataDirectory);
-        clearInvocations(mPreferenceHelper);
+        FilesUtil.deleteDir(mMockStagedDataDirectory);
+        mFakePreferenceHelper.clearCache();
         clearInvocations(mTransactionManager);
     }
 
     @Test
     public void testGetAllBackupFileNames_forDeviceToDevice_returnsAllFileNames() throws Exception {
-        File dbFile = createAndGetNonEmptyFile(mMockDataDirectory, DATABASE_NAME);
-        File grantTimeFile = createAndGetNonEmptyFile(mMockDataDirectory, GRANT_TIME_FILE_NAME);
-        when(mTransactionManager.getDatabasePath()).thenReturn(dbFile);
-        when(mFirstGrantTimeManager.getFile(mUserHandle)).thenReturn(grantTimeFile);
-
         BackupFileNamesSet backupFileNamesSet = mBackupRestore.getAllBackupFileNames(true);
 
         assertThat(backupFileNamesSet).isNotNull();
         assertThat(backupFileNamesSet.getFileNames()).hasSize(2);
-        assertThat(backupFileNamesSet.getFileNames()).contains(DATABASE_NAME);
+        assertThat(backupFileNamesSet.getFileNames()).contains(STAGED_DATABASE_NAME);
         assertThat(backupFileNamesSet.getFileNames()).contains(GRANT_TIME_FILE_NAME);
     }
 
     @Test
     public void testGetAllBackupFileNames_forNonDeviceToDevice_returnsSmallFileNames()
             throws Exception {
-        File dbFile = createAndGetNonEmptyFile(mMockDataDirectory, DATABASE_NAME);
-        File grantTimeFile = createAndGetNonEmptyFile(mMockDataDirectory, GRANT_TIME_FILE_NAME);
-        when(mTransactionManager.getDatabasePath()).thenReturn(dbFile);
-        when(mFirstGrantTimeManager.getFile(mUserHandle)).thenReturn(grantTimeFile);
-
         BackupFileNamesSet backupFileNamesSet = mBackupRestore.getAllBackupFileNames(false);
 
         assertThat(backupFileNamesSet).isNotNull();
@@ -182,49 +191,51 @@
 
     @Test
     public void testGetAllBackupData_forDeviceToDevice_copiesAllData() throws Exception {
-        File dbFileToBackup = createAndGetEmptyFile(mMockDataDirectory, DATABASE_NAME);
-        File grantTimeFileToBackup =
-                createAndGetEmptyFile(mMockDataDirectory, GRANT_TIME_FILE_NAME);
-        File dbFileBacked = createAndGetEmptyFile(mMockBackedDataDirectory, DATABASE_NAME);
+        File dbFileToBackup = createAndGetNonEmptyFile(mMockDataDirectory, DATABASE_NAME);
+        File dbFileBacked = createAndGetEmptyFile(mMockBackedDataDirectory, STAGED_DATABASE_NAME);
         File grantTimeFileBacked =
                 createAndGetEmptyFile(mMockBackedDataDirectory, GRANT_TIME_FILE_NAME);
 
         when(mTransactionManager.getDatabasePath()).thenReturn(dbFileToBackup);
-        when(mFirstGrantTimeManager.getFile(mUserHandle)).thenReturn(grantTimeFileToBackup);
+        UserGrantTimeState userGrantTimeState =
+                new UserGrantTimeState(Map.of("package", Instant.now()), Map.of(), 1);
+        when(mFirstGrantTimeManager.createBackupState(mUserHandle)).thenReturn(userGrantTimeState);
 
         Map<String, ParcelFileDescriptor> pfdsByFileName = new ArrayMap<>();
         pfdsByFileName.put(
                 dbFileBacked.getName(),
-                ParcelFileDescriptor.open(dbFileBacked, ParcelFileDescriptor.MODE_READ_ONLY));
+                ParcelFileDescriptor.open(dbFileBacked, ParcelFileDescriptor.MODE_READ_WRITE));
         pfdsByFileName.put(
                 grantTimeFileBacked.getName(),
                 ParcelFileDescriptor.open(
-                        grantTimeFileBacked, ParcelFileDescriptor.MODE_READ_ONLY));
+                        grantTimeFileBacked, ParcelFileDescriptor.MODE_READ_WRITE));
 
         mBackupRestore.getAllDataForBackup(new StageRemoteDataRequest(pfdsByFileName), mUserHandle);
 
         assertThat(dbFileBacked.length()).isEqualTo(dbFileToBackup.length());
-        assertThat(grantTimeFileBacked.length()).isEqualTo(dbFileToBackup.length());
+        assertThat(GrantTimeXmlHelper.parseGrantTime(grantTimeFileBacked).toString())
+                .isEqualTo(userGrantTimeState.toString());
     }
 
     @Test
     public void testSetDataDownloadState_downloadStarted_schedulesDownloadTimeoutJob() {
         @HealthConnectManager.DataDownloadState int testDownloadStateSet = DATA_DOWNLOAD_STARTED;
-        when(mPreferenceHelper.getPreference(eq(DATA_DOWNLOAD_STATE_KEY)))
-                .thenReturn(String.valueOf(testDownloadStateSet));
-        when(mPreferenceHelper.getPreference(eq(DATA_DOWNLOAD_TIMEOUT_KEY)))
-                .thenReturn(String.valueOf(Instant.now().toEpochMilli()));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_DOWNLOAD_STATE_KEY, String.valueOf(testDownloadStateSet));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_DOWNLOAD_TIMEOUT_KEY, String.valueOf(Instant.now().toEpochMilli()));
 
         mBackupRestore.updateDataDownloadState(testDownloadStateSet);
-        verify(mPreferenceHelper)
-                .insertOrReplacePreference(
-                        eq(DATA_DOWNLOAD_STATE_KEY), eq(String.valueOf(testDownloadStateSet)));
+
         ExtendedMockito.verify(
                 () ->
                         BackupRestore.BackupRestoreJobService.schedule(
                                 eq(mServiceContext),
                                 mJobInfoArgumentCaptor.capture(),
                                 eq(mBackupRestore)));
+
+        assertThat(mFakePreferenceHelper.getPreference(DATA_DOWNLOAD_STATE_KEY))
+                .isEqualTo(String.valueOf(testDownloadStateSet));
         JobInfo jobInfo = mJobInfoArgumentCaptor.getValue();
         assertThat(jobInfo.getExtras().getString(EXTRA_JOB_NAME_KEY))
                 .isEqualTo(DATA_DOWNLOAD_TIMEOUT_KEY);
@@ -233,21 +244,20 @@
     @Test
     public void testSetDataDownloadState_downloadRetry_schedulesDownloadTimeoutJob() {
         @HealthConnectManager.DataDownloadState int testDownloadStateSet = DATA_DOWNLOAD_RETRY;
-        when(mPreferenceHelper.getPreference(eq(DATA_DOWNLOAD_STATE_KEY)))
-                .thenReturn(String.valueOf(testDownloadStateSet));
-        when(mPreferenceHelper.getPreference(eq(DATA_DOWNLOAD_TIMEOUT_KEY)))
-                .thenReturn(String.valueOf(Instant.now().toEpochMilli()));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_DOWNLOAD_STATE_KEY, String.valueOf(testDownloadStateSet));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_DOWNLOAD_TIMEOUT_KEY, String.valueOf(Instant.now().toEpochMilli()));
 
         mBackupRestore.updateDataDownloadState(testDownloadStateSet);
-        verify(mPreferenceHelper)
-                .insertOrReplacePreference(
-                        eq(DATA_DOWNLOAD_STATE_KEY), eq(String.valueOf(testDownloadStateSet)));
         ExtendedMockito.verify(
                 () ->
                         BackupRestore.BackupRestoreJobService.schedule(
                                 eq(mServiceContext),
                                 mJobInfoArgumentCaptor.capture(),
                                 eq(mBackupRestore)));
+        assertThat(mFakePreferenceHelper.getPreference(DATA_DOWNLOAD_STATE_KEY))
+                .isEqualTo(String.valueOf(testDownloadStateSet));
         JobInfo jobInfo = mJobInfoArgumentCaptor.getValue();
         assertThat(jobInfo.getExtras().getString(EXTRA_JOB_NAME_KEY))
                 .isEqualTo(DATA_DOWNLOAD_TIMEOUT_KEY);
@@ -255,23 +265,21 @@
 
     @Test
     public void testSetInternalRestoreState_waitingForStaging_schedulesStagingTimeoutJob() {
-        when(mPreferenceHelper.getPreference(eq(DATA_RESTORE_STATE_KEY)))
-                .thenReturn(String.valueOf(INTERNAL_RESTORE_STATE_WAITING_FOR_STAGING));
-        when(mPreferenceHelper.getPreference(eq(DATA_STAGING_TIMEOUT_KEY)))
-                .thenReturn(String.valueOf(Instant.now().toEpochMilli()));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_RESTORE_STATE_KEY, String.valueOf(INTERNAL_RESTORE_STATE_WAITING_FOR_STAGING));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_STAGING_TIMEOUT_KEY, String.valueOf(Instant.now().toEpochMilli()));
 
         // forcing the state because we want to this state to set even when it's already set.
         mBackupRestore.setInternalRestoreState(INTERNAL_RESTORE_STATE_WAITING_FOR_STAGING, true);
-        verify(mPreferenceHelper)
-                .insertOrReplacePreference(
-                        eq(DATA_RESTORE_STATE_KEY),
-                        eq(String.valueOf(INTERNAL_RESTORE_STATE_WAITING_FOR_STAGING)));
         ExtendedMockito.verify(
                 () ->
                         BackupRestore.BackupRestoreJobService.schedule(
                                 eq(mServiceContext),
                                 mJobInfoArgumentCaptor.capture(),
                                 eq(mBackupRestore)));
+        assertThat(mFakePreferenceHelper.getPreference(DATA_RESTORE_STATE_KEY))
+                .isEqualTo(String.valueOf(INTERNAL_RESTORE_STATE_WAITING_FOR_STAGING));
         JobInfo jobInfo = mJobInfoArgumentCaptor.getValue();
         assertThat(jobInfo.getExtras().getString(EXTRA_JOB_NAME_KEY))
                 .isEqualTo(DATA_STAGING_TIMEOUT_KEY);
@@ -279,47 +287,44 @@
 
     @Test
     public void testSetInternalRestoreState_stagingInProgress_schedulesStagingTimeoutJob() {
-        when(mPreferenceHelper.getPreference(eq(DATA_RESTORE_STATE_KEY)))
-                .thenReturn(String.valueOf(INTERNAL_RESTORE_STATE_STAGING_IN_PROGRESS));
-        when(mPreferenceHelper.getPreference(eq(DATA_STAGING_TIMEOUT_KEY)))
-                .thenReturn(String.valueOf(Instant.now().toEpochMilli()));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_RESTORE_STATE_KEY, String.valueOf(INTERNAL_RESTORE_STATE_STAGING_IN_PROGRESS));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_STAGING_TIMEOUT_KEY, String.valueOf(Instant.now().toEpochMilli()));
 
         // forcing the state because we want to this state to set even when it's already set.
         mBackupRestore.setInternalRestoreState(INTERNAL_RESTORE_STATE_STAGING_IN_PROGRESS, true);
-        verify(mPreferenceHelper)
-                .insertOrReplacePreference(
-                        eq(DATA_RESTORE_STATE_KEY),
-                        eq(String.valueOf(INTERNAL_RESTORE_STATE_STAGING_IN_PROGRESS)));
         ExtendedMockito.verify(
                 () ->
                         BackupRestore.BackupRestoreJobService.schedule(
                                 eq(mServiceContext),
                                 mJobInfoArgumentCaptor.capture(),
                                 eq(mBackupRestore)));
+        assertThat(mFakePreferenceHelper.getPreference(DATA_RESTORE_STATE_KEY))
+                .isEqualTo(String.valueOf(INTERNAL_RESTORE_STATE_STAGING_IN_PROGRESS));
         JobInfo jobInfo = mJobInfoArgumentCaptor.getValue();
         assertThat(jobInfo.getExtras().getString(EXTRA_JOB_NAME_KEY))
                 .isEqualTo(DATA_STAGING_TIMEOUT_KEY);
+
     }
 
     @Test
     public void testSetInternalRestoreState_mergingInProgress_schedulesMergingTimeoutJob() {
-        when(mPreferenceHelper.getPreference(eq(DATA_RESTORE_STATE_KEY)))
-                .thenReturn(String.valueOf(INTERNAL_RESTORE_STATE_MERGING_IN_PROGRESS));
-        when(mPreferenceHelper.getPreference(eq(DATA_MERGING_TIMEOUT_KEY)))
-                .thenReturn(String.valueOf(Instant.now().toEpochMilli()));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_RESTORE_STATE_KEY, String.valueOf(INTERNAL_RESTORE_STATE_MERGING_IN_PROGRESS));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_STAGING_TIMEOUT_KEY, String.valueOf(Instant.now().toEpochMilli()));
 
         // forcing the state because we want to this state to set even when it's already set.
         mBackupRestore.setInternalRestoreState(INTERNAL_RESTORE_STATE_MERGING_IN_PROGRESS, true);
-        verify(mPreferenceHelper)
-                .insertOrReplacePreference(
-                        eq(DATA_RESTORE_STATE_KEY),
-                        eq(String.valueOf(INTERNAL_RESTORE_STATE_MERGING_IN_PROGRESS)));
         ExtendedMockito.verify(
                 () ->
                         BackupRestore.BackupRestoreJobService.schedule(
                                 eq(mServiceContext),
                                 mJobInfoArgumentCaptor.capture(),
                                 eq(mBackupRestore)));
+        assertThat(mFakePreferenceHelper.getPreference(DATA_RESTORE_STATE_KEY))
+                .isEqualTo(String.valueOf(INTERNAL_RESTORE_STATE_MERGING_IN_PROGRESS));
         JobInfo jobInfo = mJobInfoArgumentCaptor.getValue();
         assertThat(jobInfo.getExtras().getString(EXTRA_JOB_NAME_KEY))
                 .isEqualTo(DATA_MERGING_TIMEOUT_KEY);
@@ -328,10 +333,10 @@
     @Test
     public void testScheduleAllPendingJobs_downloadStarted_schedulesDownloadTimeoutJob() {
         @HealthConnectManager.DataDownloadState int testDownloadStateSet = DATA_DOWNLOAD_STARTED;
-        when(mPreferenceHelper.getPreference(eq(DATA_DOWNLOAD_STATE_KEY)))
-                .thenReturn(String.valueOf(testDownloadStateSet));
-        when(mPreferenceHelper.getPreference(eq(DATA_DOWNLOAD_TIMEOUT_KEY)))
-                .thenReturn(String.valueOf(Instant.now().toEpochMilli()));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_DOWNLOAD_STATE_KEY, String.valueOf(testDownloadStateSet));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_DOWNLOAD_TIMEOUT_KEY, String.valueOf(Instant.now().toEpochMilli()));
 
         mBackupRestore.scheduleAllJobs();
         ExtendedMockito.verify(
@@ -348,10 +353,10 @@
     @Test
     public void testScheduleAllPendingJobs_downloadRetry_schedulesDownloadTimeoutJob() {
         @HealthConnectManager.DataDownloadState int testDownloadStateSet = DATA_DOWNLOAD_RETRY;
-        when(mPreferenceHelper.getPreference(eq(DATA_DOWNLOAD_STATE_KEY)))
-                .thenReturn(String.valueOf(testDownloadStateSet));
-        when(mPreferenceHelper.getPreference(eq(DATA_DOWNLOAD_TIMEOUT_KEY)))
-                .thenReturn(String.valueOf(Instant.now().toEpochMilli()));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_DOWNLOAD_STATE_KEY, String.valueOf(testDownloadStateSet));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_DOWNLOAD_TIMEOUT_KEY, String.valueOf(Instant.now().toEpochMilli()));
 
         mBackupRestore.scheduleAllJobs();
         ExtendedMockito.verify(
@@ -367,10 +372,10 @@
 
     @Test
     public void testScheduleAllPendingJobs_waitingForStaging_schedulesStagingTimeoutJob() {
-        when(mPreferenceHelper.getPreference(eq(DATA_RESTORE_STATE_KEY)))
-                .thenReturn(String.valueOf(INTERNAL_RESTORE_STATE_WAITING_FOR_STAGING));
-        when(mPreferenceHelper.getPreference(eq(DATA_STAGING_TIMEOUT_KEY)))
-                .thenReturn(String.valueOf(Instant.now().toEpochMilli()));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_RESTORE_STATE_KEY, String.valueOf(INTERNAL_RESTORE_STATE_WAITING_FOR_STAGING));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_STAGING_TIMEOUT_KEY, String.valueOf(Instant.now().toEpochMilli()));
 
         mBackupRestore.scheduleAllJobs();
         ExtendedMockito.verify(
@@ -386,10 +391,10 @@
 
     @Test
     public void testScheduleAllPendingJobs_stagingInProgress_schedulesStagingTimeoutJob() {
-        when(mPreferenceHelper.getPreference(eq(DATA_RESTORE_STATE_KEY)))
-                .thenReturn(String.valueOf(INTERNAL_RESTORE_STATE_STAGING_IN_PROGRESS));
-        when(mPreferenceHelper.getPreference(eq(DATA_STAGING_TIMEOUT_KEY)))
-                .thenReturn(String.valueOf(Instant.now().toEpochMilli()));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_RESTORE_STATE_KEY, String.valueOf(INTERNAL_RESTORE_STATE_STAGING_IN_PROGRESS));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_STAGING_TIMEOUT_KEY, String.valueOf(Instant.now().toEpochMilli()));
 
         mBackupRestore.scheduleAllJobs();
         ExtendedMockito.verify(
@@ -405,10 +410,10 @@
 
     @Test
     public void testScheduleAllPendingJobs_mergingInProgress_schedulesMergingTimeoutJob() {
-        when(mPreferenceHelper.getPreference(eq(DATA_RESTORE_STATE_KEY)))
-                .thenReturn(String.valueOf(INTERNAL_RESTORE_STATE_MERGING_IN_PROGRESS));
-        when(mPreferenceHelper.getPreference(eq(DATA_MERGING_TIMEOUT_KEY)))
-                .thenReturn(String.valueOf(Instant.now().toEpochMilli()));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_RESTORE_STATE_KEY, String.valueOf(INTERNAL_RESTORE_STATE_MERGING_IN_PROGRESS));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_STAGING_TIMEOUT_KEY, String.valueOf(Instant.now().toEpochMilli()));
 
         mBackupRestore.scheduleAllJobs();
         ExtendedMockito.verify(
@@ -423,33 +428,54 @@
     }
 
     @Test
-    public void testScheduleAllTimeoutJobs_stagingDone_triggersMergingJob() {
-        when(mPreferenceHelper.getPreference(eq(DATA_RESTORE_STATE_KEY)))
-                .thenReturn(String.valueOf(INTERNAL_RESTORE_STATE_STAGING_DONE));
-        when(mPreferenceHelper.getPreference(eq(DATA_MERGING_TIMEOUT_KEY)))
-                .thenReturn(String.valueOf(Instant.now().toEpochMilli()));
+    public void testScheduleAllTimeoutJobs_stagingDone_triggersMergingJob() throws Exception {
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_RESTORE_STATE_KEY, String.valueOf(INTERNAL_RESTORE_STATE_STAGING_DONE));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_STAGING_TIMEOUT_KEY, String.valueOf(Instant.now().toEpochMilli()));
+
         when(mTransactionManager.getDatabaseVersion()).thenReturn(1);
 
-        BackupRestore spyBackupRestore = spy(mBackupRestore);
-        HealthConnectDatabase mockHealthDb = mock(HealthConnectDatabase.class);
         SQLiteDatabase mockDb = mock(SQLiteDatabase.class);
         when(mockDb.getVersion()).thenReturn(1);
-        when(mockHealthDb.getReadableDatabase()).thenReturn(mockDb);
-        doReturn(mockHealthDb).when(spyBackupRestore).getStagedDatabase();
+        when(SQLiteDatabase.openDatabase(any(), any())).thenReturn(mockDb);
 
-        spyBackupRestore.scheduleAllJobs();
-        verify(mPreferenceHelper, timeout(2000))
-                .insertOrReplacePreference(
-                        eq(DATA_RESTORE_STATE_KEY),
-                        eq(String.valueOf(INTERNAL_RESTORE_STATE_MERGING_DONE)));
+        mBackupRestore.scheduleAllJobs();
+        Thread.sleep(2000);
+        assertThat(mFakePreferenceHelper.getPreference(DATA_RESTORE_STATE_KEY))
+                .isEqualTo(String.valueOf(INTERNAL_RESTORE_STATE_MERGING_DONE));
     }
 
     @Test
     public void testScheduleAllTimeoutJobs_stagingDoneAndMigration_schedulesRetryMergingJob() {
-        when(mPreferenceHelper.getPreference(eq(DATA_RESTORE_STATE_KEY)))
-                .thenReturn(String.valueOf(INTERNAL_RESTORE_STATE_STAGING_DONE));
-        when(mPreferenceHelper.getPreference(eq(DATA_MERGING_TIMEOUT_KEY)))
-                .thenReturn(String.valueOf(Instant.now().toEpochMilli()));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_RESTORE_STATE_KEY, String.valueOf(INTERNAL_RESTORE_STATE_STAGING_DONE));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_STAGING_TIMEOUT_KEY, String.valueOf(Instant.now().toEpochMilli()));
+
+        when(mMigrationStateManager.isMigrationInProgress()).thenReturn(true);
+
+        mBackupRestore.scheduleAllJobs();
+        ExtendedMockito.verify(
+                () ->
+                        BackupRestore.BackupRestoreJobService.schedule(
+                                eq(mServiceContext),
+                                mJobInfoArgumentCaptor.capture(),
+                                eq(mBackupRestore)),
+                timeout(2000));
+        JobInfo jobInfo = mJobInfoArgumentCaptor.getValue();
+        assertThat(jobInfo.getExtras().getString(EXTRA_JOB_NAME_KEY))
+                .isEqualTo(DATA_MERGING_RETRY_KEY);
+    }
+
+    @Test
+    public void testScheduleAllTimeoutJobs_mergingWithBugAndMigration_schedulesRetryMergingJob() {
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_RESTORE_STATE_KEY,
+                String.valueOf(INTERNAL_RESTORE_STATE_MERGING_DONE_OLD_CODE));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_STAGING_TIMEOUT_KEY, String.valueOf(Instant.now().toEpochMilli()));
+
         when(mMigrationStateManager.isMigrationInProgress()).thenReturn(true);
 
         mBackupRestore.scheduleAllJobs();
@@ -475,133 +501,120 @@
 
     @Test
     public void testOnStartJob_forDownloadStartedJob_executesDownloadJob() {
-        when(mPreferenceHelper.getPreference(eq(DATA_DOWNLOAD_STATE_KEY)))
-                .thenReturn(String.valueOf(DATA_DOWNLOAD_STARTED));
-        when(mPreferenceHelper.getPreference(eq(DATA_DOWNLOAD_TIMEOUT_KEY)))
-                .thenReturn(String.valueOf(Instant.now().toEpochMilli()));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_DOWNLOAD_STATE_KEY, String.valueOf(DATA_DOWNLOAD_STARTED));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_DOWNLOAD_TIMEOUT_KEY, String.valueOf(Instant.now().toEpochMilli()));
 
         PersistableBundle extras = new PersistableBundle();
         extras.putString(EXTRA_JOB_NAME_KEY, DATA_DOWNLOAD_TIMEOUT_KEY);
         mBackupRestore.handleJob(extras);
 
-        verify(mPreferenceHelper)
-                .insertOrReplacePreference(
-                        eq(DATA_DOWNLOAD_STATE_KEY), eq(String.valueOf(DATA_DOWNLOAD_FAILED)));
-        verify(mPreferenceHelper)
-                .insertOrReplacePreference(
-                        eq(DATA_RESTORE_ERROR_KEY),
-                        eq(String.valueOf(RESTORE_ERROR_FETCHING_DATA)));
-        verify(mPreferenceHelper).insertOrReplacePreference(eq(DATA_DOWNLOAD_TIMEOUT_KEY), eq(""));
-        verify(mPreferenceHelper)
-                .insertOrReplacePreference(eq(DATA_DOWNLOAD_TIMEOUT_CANCELLED_KEY), eq(""));
+        assertThat(mFakePreferenceHelper.getPreference(DATA_DOWNLOAD_STATE_KEY))
+                .isEqualTo(String.valueOf(DATA_DOWNLOAD_FAILED));
+        assertThat(mFakePreferenceHelper.getPreference(DATA_RESTORE_ERROR_KEY))
+                .isEqualTo(String.valueOf(RESTORE_ERROR_FETCHING_DATA));
+        assertThat(mFakePreferenceHelper.getPreference(DATA_DOWNLOAD_TIMEOUT_KEY))
+                .isEqualTo("");
+        assertThat(mFakePreferenceHelper.getPreference(DATA_DOWNLOAD_TIMEOUT_CANCELLED_KEY))
+                .isEqualTo("");
     }
 
     @Test
     public void testOnStartJob_forDownloadRetryJob_executesDownloadJob() {
-        when(mPreferenceHelper.getPreference(eq(DATA_DOWNLOAD_STATE_KEY)))
-                .thenReturn(String.valueOf(DATA_DOWNLOAD_RETRY));
-        when(mPreferenceHelper.getPreference(eq(DATA_DOWNLOAD_TIMEOUT_KEY)))
-                .thenReturn(String.valueOf(Instant.now().toEpochMilli()));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_DOWNLOAD_STATE_KEY, String.valueOf(DATA_DOWNLOAD_RETRY));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_DOWNLOAD_TIMEOUT_KEY, String.valueOf(Instant.now().toEpochMilli()));
 
         PersistableBundle extras = new PersistableBundle();
         extras.putString(EXTRA_JOB_NAME_KEY, DATA_DOWNLOAD_TIMEOUT_KEY);
         mBackupRestore.handleJob(extras);
 
-        verify(mPreferenceHelper)
-                .insertOrReplacePreference(
-                        eq(DATA_DOWNLOAD_STATE_KEY), eq(String.valueOf(DATA_DOWNLOAD_FAILED)));
-        verify(mPreferenceHelper)
-                .insertOrReplacePreference(
-                        eq(DATA_RESTORE_ERROR_KEY),
-                        eq(String.valueOf(RESTORE_ERROR_FETCHING_DATA)));
-        verify(mPreferenceHelper).insertOrReplacePreference(eq(DATA_DOWNLOAD_TIMEOUT_KEY), eq(""));
-        verify(mPreferenceHelper)
-                .insertOrReplacePreference(eq(DATA_DOWNLOAD_TIMEOUT_CANCELLED_KEY), eq(""));
+        assertThat(mFakePreferenceHelper.getPreference(DATA_DOWNLOAD_STATE_KEY))
+                .isEqualTo(String.valueOf(DATA_DOWNLOAD_FAILED));
+        assertThat(mFakePreferenceHelper.getPreference(DATA_RESTORE_ERROR_KEY))
+                .isEqualTo(String.valueOf(RESTORE_ERROR_FETCHING_DATA));
+        assertThat(mFakePreferenceHelper.getPreference(DATA_DOWNLOAD_TIMEOUT_KEY))
+                .isEqualTo("");
+        assertThat(mFakePreferenceHelper.getPreference(DATA_DOWNLOAD_TIMEOUT_CANCELLED_KEY))
+                .isEqualTo("");
     }
 
     @Test
     public void testOnStartJob_forWaitingStagingJob_executesStagingJob() {
-        when(mPreferenceHelper.getPreference(eq(DATA_RESTORE_STATE_KEY)))
-                .thenReturn(String.valueOf(INTERNAL_RESTORE_STATE_WAITING_FOR_STAGING));
-        when(mPreferenceHelper.getPreference(eq(DATA_STAGING_TIMEOUT_KEY)))
-                .thenReturn(String.valueOf(Instant.now().toEpochMilli()));
+       mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_RESTORE_STATE_KEY, String.valueOf(INTERNAL_RESTORE_STATE_WAITING_FOR_STAGING));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_STAGING_TIMEOUT_KEY, String.valueOf(Instant.now().toEpochMilli()));
 
         PersistableBundle extras = new PersistableBundle();
         extras.putString(EXTRA_JOB_NAME_KEY, DATA_STAGING_TIMEOUT_KEY);
         mBackupRestore.handleJob(extras);
 
-        verify(mPreferenceHelper)
-                .insertOrReplacePreference(
-                        eq(DATA_RESTORE_STATE_KEY),
-                        eq(String.valueOf(INTERNAL_RESTORE_STATE_MERGING_DONE)));
-        verify(mPreferenceHelper)
-                .insertOrReplacePreference(
-                        eq(DATA_RESTORE_ERROR_KEY), eq(String.valueOf(RESTORE_ERROR_UNKNOWN)));
-        verify(mPreferenceHelper).insertOrReplacePreference(eq(DATA_STAGING_TIMEOUT_KEY), eq(""));
-        verify(mPreferenceHelper)
-                .insertOrReplacePreference(eq(DATA_STAGING_TIMEOUT_CANCELLED_KEY), eq(""));
+        assertThat(mFakePreferenceHelper.getPreference(DATA_RESTORE_STATE_KEY))
+                .isEqualTo(String.valueOf(INTERNAL_RESTORE_STATE_MERGING_DONE));
+        assertThat(mFakePreferenceHelper.getPreference(DATA_RESTORE_ERROR_KEY))
+                .isEqualTo(String.valueOf(RESTORE_ERROR_UNKNOWN));
+        assertThat(mFakePreferenceHelper.getPreference(DATA_STAGING_TIMEOUT_KEY))
+                .isEqualTo("");
+        assertThat(mFakePreferenceHelper.getPreference(DATA_STAGING_TIMEOUT_CANCELLED_KEY))
+                .isEqualTo("");
     }
 
     @Test
     public void testOnStartJob_forStagingProgressJob_executesStagingJob() {
-        when(mPreferenceHelper.getPreference(eq(DATA_RESTORE_STATE_KEY)))
-                .thenReturn(String.valueOf(INTERNAL_RESTORE_STATE_STAGING_IN_PROGRESS));
-        when(mPreferenceHelper.getPreference(eq(DATA_STAGING_TIMEOUT_KEY)))
-                .thenReturn(String.valueOf(Instant.now().toEpochMilli()));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_RESTORE_STATE_KEY, String.valueOf(INTERNAL_RESTORE_STATE_STAGING_IN_PROGRESS));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_STAGING_TIMEOUT_KEY, String.valueOf(Instant.now().toEpochMilli()));
 
         PersistableBundle extras = new PersistableBundle();
         extras.putString(EXTRA_JOB_NAME_KEY, DATA_STAGING_TIMEOUT_KEY);
         mBackupRestore.handleJob(extras);
 
-        verify(mPreferenceHelper)
-                .insertOrReplacePreference(
-                        eq(DATA_RESTORE_STATE_KEY),
-                        eq(String.valueOf(INTERNAL_RESTORE_STATE_MERGING_DONE)));
-        verify(mPreferenceHelper)
-                .insertOrReplacePreference(
-                        eq(DATA_RESTORE_ERROR_KEY), eq(String.valueOf(RESTORE_ERROR_UNKNOWN)));
-        verify(mPreferenceHelper).insertOrReplacePreference(eq(DATA_STAGING_TIMEOUT_KEY), eq(""));
-        verify(mPreferenceHelper)
-                .insertOrReplacePreference(eq(DATA_STAGING_TIMEOUT_CANCELLED_KEY), eq(""));
+        assertThat(mFakePreferenceHelper.getPreference(DATA_RESTORE_STATE_KEY))
+                .isEqualTo(String.valueOf(INTERNAL_RESTORE_STATE_MERGING_DONE));
+        assertThat(mFakePreferenceHelper.getPreference(DATA_RESTORE_ERROR_KEY))
+                .isEqualTo(String.valueOf(RESTORE_ERROR_UNKNOWN));
+        assertThat(mFakePreferenceHelper.getPreference(DATA_STAGING_TIMEOUT_KEY))
+                .isEqualTo("");
+        assertThat(mFakePreferenceHelper.getPreference(DATA_STAGING_TIMEOUT_CANCELLED_KEY))
+                .isEqualTo("");
     }
 
     @Test
     public void testOnStartJob_forMergingProgressJob_executesMergingJob() {
-        when(mPreferenceHelper.getPreference(eq(DATA_RESTORE_STATE_KEY)))
-                .thenReturn(String.valueOf(INTERNAL_RESTORE_STATE_MERGING_IN_PROGRESS));
-        when(mPreferenceHelper.getPreference(eq(DATA_MERGING_TIMEOUT_KEY)))
-                .thenReturn(String.valueOf(Instant.now().toEpochMilli()));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_RESTORE_STATE_KEY, String.valueOf(INTERNAL_RESTORE_STATE_MERGING_IN_PROGRESS));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_STAGING_TIMEOUT_KEY, String.valueOf(Instant.now().toEpochMilli()));
 
         PersistableBundle extras = new PersistableBundle();
         extras.putString(EXTRA_JOB_NAME_KEY, DATA_MERGING_TIMEOUT_KEY);
         mBackupRestore.handleJob(extras);
 
-        verify(mPreferenceHelper)
-                .insertOrReplacePreference(
-                        eq(DATA_RESTORE_STATE_KEY),
-                        eq(String.valueOf(INTERNAL_RESTORE_STATE_MERGING_DONE)));
-        verify(mPreferenceHelper)
-                .insertOrReplacePreference(
-                        eq(DATA_RESTORE_ERROR_KEY), eq(String.valueOf(RESTORE_ERROR_UNKNOWN)));
-        verify(mPreferenceHelper).insertOrReplacePreference(eq(DATA_MERGING_TIMEOUT_KEY), eq(""));
-        verify(mPreferenceHelper)
-                .insertOrReplacePreference(eq(DATA_MERGING_TIMEOUT_CANCELLED_KEY), eq(""));
+        assertThat(mFakePreferenceHelper.getPreference(DATA_RESTORE_STATE_KEY))
+                .isEqualTo(String.valueOf(INTERNAL_RESTORE_STATE_MERGING_DONE));
+        assertThat(mFakePreferenceHelper.getPreference(DATA_RESTORE_ERROR_KEY))
+                .isEqualTo(String.valueOf(RESTORE_ERROR_UNKNOWN));
+        assertThat(mFakePreferenceHelper.getPreference(DATA_MERGING_TIMEOUT_KEY))
+                .isEqualTo("");
+        assertThat(mFakePreferenceHelper.getPreference(DATA_MERGING_TIMEOUT_CANCELLED_KEY))
+                .isEqualTo("");
     }
 
     @Test
     public void testMerge_mergingOfGrantTimesIsInvoked() {
-        when(mPreferenceHelper.getPreference(eq(DATA_RESTORE_STATE_KEY)))
-                .thenReturn(String.valueOf(INTERNAL_RESTORE_STATE_STAGING_DONE));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_RESTORE_STATE_KEY, String.valueOf(INTERNAL_RESTORE_STATE_STAGING_DONE));
         when(mTransactionManager.getDatabaseVersion()).thenReturn(1);
 
-        BackupRestore spyBackupRestore = spy(mBackupRestore);
-        HealthConnectDatabase mockHealthDb = mock(HealthConnectDatabase.class);
         SQLiteDatabase mockDb = mock(SQLiteDatabase.class);
         when(mockDb.getVersion()).thenReturn(1);
-        when(mockHealthDb.getReadableDatabase()).thenReturn(mockDb);
-        doReturn(mockHealthDb).when(spyBackupRestore).getStagedDatabase();
+        when(SQLiteDatabase.openDatabase(any(), any())).thenReturn(mockDb);
 
-        spyBackupRestore.merge();
+        mBackupRestore.merge();
         verify(mFirstGrantTimeManager).applyAndStageBackupDataForUser(eq(mUserHandle), any());
     }
 
@@ -609,18 +622,15 @@
     public void testMerge_mergingOfGrantTimes_parsesRestoredGrantTimes() {
         ArgumentCaptor<File> restoredGrantTimeFileCaptor = ArgumentCaptor.forClass(File.class);
 
-        when(mPreferenceHelper.getPreference(eq(DATA_RESTORE_STATE_KEY)))
-                .thenReturn(String.valueOf(INTERNAL_RESTORE_STATE_STAGING_DONE));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_RESTORE_STATE_KEY, String.valueOf(INTERNAL_RESTORE_STATE_STAGING_DONE));
         when(mTransactionManager.getDatabaseVersion()).thenReturn(1);
 
-        BackupRestore spyBackupRestore = spy(mBackupRestore);
-        HealthConnectDatabase mockHealthDb = mock(HealthConnectDatabase.class);
         SQLiteDatabase mockDb = mock(SQLiteDatabase.class);
         when(mockDb.getVersion()).thenReturn(1);
-        when(mockHealthDb.getReadableDatabase()).thenReturn(mockDb);
-        doReturn(mockHealthDb).when(spyBackupRestore).getStagedDatabase();
+        when(SQLiteDatabase.openDatabase(any(), any())).thenReturn(mockDb);
 
-        spyBackupRestore.merge();
+        mBackupRestore.merge();
         ExtendedMockito.verify(
                 () -> GrantTimeXmlHelper.parseGrantTime(restoredGrantTimeFileCaptor.capture()));
         assertThat(restoredGrantTimeFileCaptor.getValue()).isNotNull();
@@ -629,61 +639,32 @@
     }
 
     @Test
-    public void testMerge_whenErrorVersionDiff_mergingOfGrantTimesIsNotInvoked() {
-        when(mPreferenceHelper.getPreference(eq(DATA_RESTORE_STATE_KEY)))
-                .thenReturn(String.valueOf(INTERNAL_RESTORE_STATE_STAGING_DONE));
+    public void testMerge_whenModuleVersionBehind_setsVersionDiffError() throws IOException {
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_RESTORE_STATE_KEY, String.valueOf(INTERNAL_RESTORE_STATE_STAGING_DONE));
         when(mTransactionManager.getDatabaseVersion()).thenReturn(1);
 
-        BackupRestore spyBackupRestore = spy(mBackupRestore);
-        HealthConnectDatabase mockHealthDb = mock(HealthConnectDatabase.class);
         SQLiteDatabase mockDb = mock(SQLiteDatabase.class);
         when(mockDb.getVersion()).thenReturn(2);
-        when(mockHealthDb.getReadableDatabase()).thenReturn(mockDb);
-        doReturn(mockHealthDb).when(spyBackupRestore).getStagedDatabase();
+        when(SQLiteDatabase.openDatabase(any(), any())).thenReturn(mockDb);
 
-        spyBackupRestore.merge();
-        verify(mFirstGrantTimeManager, never())
-                .applyAndStageBackupDataForUser(eq(mUserHandle), any());
-    }
-
-    @Test
-    public void testMerge_whenModuleVersionBehind_setsVersionDiffError() {
-        when(mPreferenceHelper.getPreference(eq(DATA_RESTORE_STATE_KEY)))
-                .thenReturn(String.valueOf(INTERNAL_RESTORE_STATE_STAGING_DONE));
-        when(mTransactionManager.getDatabaseVersion()).thenReturn(1);
-
-        BackupRestore spyBackupRestore = spy(mBackupRestore);
-        HealthConnectDatabase mockHealthDb = mock(HealthConnectDatabase.class);
-        SQLiteDatabase mockDb = mock(SQLiteDatabase.class);
-        when(mockDb.getVersion()).thenReturn(2);
-        when(mockHealthDb.getReadableDatabase()).thenReturn(mockDb);
-        doReturn(mockHealthDb).when(spyBackupRestore).getStagedDatabase();
-
-        spyBackupRestore.merge();
-        verify(mPreferenceHelper)
-                .insertOrReplacePreference(
-                        eq(DATA_RESTORE_ERROR_KEY), eq(String.valueOf(RESTORE_ERROR_VERSION_DIFF)));
-    }
-
-    @Test
-    public void testMerge_whenMigrationInProgress_doesNotMergeGrantTimes() {
-        when(mPreferenceHelper.getPreference(eq(DATA_RESTORE_STATE_KEY)))
-                .thenReturn(String.valueOf(INTERNAL_RESTORE_STATE_STAGING_DONE));
-        when(mTransactionManager.getDatabaseVersion()).thenReturn(1);
-
-        when(mMigrationStateManager.isMigrationInProgress()).thenReturn(true);
+        when(mStagedDbContext.getDatabasePath(STAGED_DATABASE_NAME))
+                .thenReturn(createAndGetEmptyFile(mMockStagedDataDirectory, STAGED_DATABASE_NAME));
 
         mBackupRestore.merge();
+        assertThat(mFakePreferenceHelper.getPreference(DATA_RESTORE_ERROR_KEY))
+                .isEqualTo(String.valueOf(RESTORE_ERROR_VERSION_DIFF));
         verify(mFirstGrantTimeManager, never())
                 .applyAndStageBackupDataForUser(eq(mUserHandle), any());
+
     }
 
     @Test
     public void testMerge_whenMigrationInProgress_schedulesRetryMergingJob() {
-        when(mPreferenceHelper.getPreference(eq(DATA_RESTORE_STATE_KEY)))
-                .thenReturn(String.valueOf(INTERNAL_RESTORE_STATE_STAGING_DONE));
-        when(mPreferenceHelper.getPreference(eq(DATA_MERGING_TIMEOUT_KEY)))
-                .thenReturn(String.valueOf(Instant.now().toEpochMilli()));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_RESTORE_STATE_KEY, String.valueOf(INTERNAL_RESTORE_STATE_STAGING_DONE));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_MERGING_TIMEOUT_KEY, String.valueOf(Instant.now().toEpochMilli()));
         when(mTransactionManager.getDatabaseVersion()).thenReturn(1);
 
         when(mMigrationStateManager.isMigrationInProgress()).thenReturn(true);
@@ -699,12 +680,14 @@
         JobInfo jobInfo = mJobInfoArgumentCaptor.getValue();
         assertThat(jobInfo.getExtras().getString(EXTRA_JOB_NAME_KEY))
                 .isEqualTo(DATA_MERGING_RETRY_KEY);
+        verify(mFirstGrantTimeManager, never())
+                .applyAndStageBackupDataForUser(eq(mUserHandle), any());
     }
 
     @Test
     public void testShouldAttemptMerging_whenInStagingDone_returnsTrue() {
-        when(mPreferenceHelper.getPreference(eq(DATA_RESTORE_STATE_KEY)))
-                .thenReturn(String.valueOf(INTERNAL_RESTORE_STATE_STAGING_DONE));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_RESTORE_STATE_KEY, String.valueOf(INTERNAL_RESTORE_STATE_STAGING_DONE));
 
         boolean result = mBackupRestore.shouldAttemptMerging();
         assertThat(result).isTrue();
@@ -712,8 +695,18 @@
 
     @Test
     public void testShouldAttemptMerging_whenInMergingProgress_returnsTrue() {
-        when(mPreferenceHelper.getPreference(eq(DATA_RESTORE_STATE_KEY)))
-                .thenReturn(String.valueOf(INTERNAL_RESTORE_STATE_MERGING_IN_PROGRESS));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_RESTORE_STATE_KEY, String.valueOf(INTERNAL_RESTORE_STATE_MERGING_IN_PROGRESS));
+
+        boolean result = mBackupRestore.shouldAttemptMerging();
+        assertThat(result).isTrue();
+    }
+
+    @Test
+    public void testShouldAttemptMerging_whenInMergingDoneWithBug_returnsTrue() {
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_RESTORE_STATE_KEY,
+                String.valueOf(INTERNAL_RESTORE_STATE_MERGING_DONE_OLD_CODE));
 
         boolean result = mBackupRestore.shouldAttemptMerging();
         assertThat(result).isTrue();
@@ -721,8 +714,9 @@
 
     @Test
     public void testShouldAttemptMerging_whenInMergingDone_returnsFalse() {
-        when(mPreferenceHelper.getPreference(eq(DATA_RESTORE_STATE_KEY)))
-                .thenReturn(String.valueOf(INTERNAL_RESTORE_STATE_MERGING_DONE));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_RESTORE_STATE_KEY,
+                String.valueOf(INTERNAL_RESTORE_STATE_MERGING_DONE));
 
         boolean result = mBackupRestore.shouldAttemptMerging();
         assertThat(result).isFalse();
@@ -730,8 +724,8 @@
 
     @Test
     public void testShouldAttemptMerging_whenInStagingProgress_returnsFalse() {
-        when(mPreferenceHelper.getPreference(eq(DATA_RESTORE_STATE_KEY)))
-                .thenReturn(String.valueOf(INTERNAL_RESTORE_STATE_STAGING_IN_PROGRESS));
+        mFakePreferenceHelper.insertOrReplacePreference(
+                DATA_RESTORE_STATE_KEY, String.valueOf(INTERNAL_RESTORE_STATE_STAGING_IN_PROGRESS));
 
         boolean result = mBackupRestore.shouldAttemptMerging();
         assertThat(result).isFalse();
diff --git a/tests/unittests/src/com/android/server/healthconnect/logging/DailyLoggingServiceTest.java b/tests/unittests/src/com/android/server/healthconnect/logging/DailyLoggingServiceTest.java
index df60f6f..06f0b4f 100644
--- a/tests/unittests/src/com/android/server/healthconnect/logging/DailyLoggingServiceTest.java
+++ b/tests/unittests/src/com/android/server/healthconnect/logging/DailyLoggingServiceTest.java
@@ -38,10 +38,12 @@
 import com.android.modules.utils.testing.ExtendedMockitoRule;
 import com.android.server.healthconnect.logging.DailyLoggingService;
 import com.android.server.healthconnect.storage.TransactionManager;
+import com.android.server.healthconnect.storage.datatypehelpers.AccessLogsHelper;
 import com.android.server.healthconnect.storage.datatypehelpers.BloodPressureRecordHelper;
 import com.android.server.healthconnect.storage.datatypehelpers.ChangeLogsHelper;
 import com.android.server.healthconnect.storage.datatypehelpers.HeartRateRecordHelper;
 import com.android.server.healthconnect.storage.datatypehelpers.HeightRecordHelper;
+import com.android.server.healthconnect.storage.datatypehelpers.PreferenceHelper;
 import com.android.server.healthconnect.storage.datatypehelpers.SpeedRecordHelper;
 import com.android.server.healthconnect.storage.datatypehelpers.StepsRecordHelper;
 import com.android.server.healthconnect.storage.datatypehelpers.TotalCaloriesBurnedRecordHelper;
@@ -54,6 +56,8 @@
 import org.mockito.Mock;
 import org.mockito.quality.Strictness;
 
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
 import java.util.List;
 
 public class DailyLoggingServiceTest {
@@ -65,6 +69,8 @@
                     .mockStatic(DatabaseUtils.class)
                     .mockStatic(TransactionManager.class)
                     .mockStatic(HealthConnectManager.class)
+                    .mockStatic(AccessLogsHelper.class)
+                    .mockStatic(PreferenceHelper.class)
                     .setStrictness(Strictness.LENIENT)
                     .build();
 
@@ -74,9 +80,13 @@
     @Mock private PackageInfo mPackageInfoConnectedApp;
     @Mock private PackageInfo mPackageInfoNotHoldingPermission;
     @Mock private PackageInfo mPackageInfoNotConnectedApp;
+    @Mock private AccessLogsHelper mAccessLogsHelper;
+    @Mock private PreferenceHelper mPreferenceHelper;
     private final UserHandle mCurrentUser = Process.myUserHandle();
     private static final String HEALTH_PERMISSION = "HEALTH_PERMISSION";
     private static final String NOT_HEALTH_PERMISSION = "NOT_HEALTH_PERMISSION";
+    private static final String USER_MOST_RECENT_ACCESS_LOG_TIME =
+            "USER_MOST_RECENT_ACCESS_LOG_TIME";
 
     @Before
     public void mockStatsLog() {
@@ -99,6 +109,9 @@
         mPackageInfoNotConnectedApp.requestedPermissions = new String[] {HEALTH_PERMISSION};
         mPackageInfoNotConnectedApp.requestedPermissionsFlags =
                 new int[] {PackageInfo.REQUESTED_PERMISSION_NEVER_FOR_LOCATION};
+
+        when(AccessLogsHelper.getInstance()).thenReturn(mAccessLogsHelper);
+        when(PreferenceHelper.getInstance()).thenReturn(mPreferenceHelper);
     }
 
     @Test
@@ -150,13 +163,19 @@
                         .getPackageManager()
                         .getInstalledPackages(any()))
                 .thenReturn(List.of(mPackageInfoConnectedApp, mPackageInfoNotHoldingPermission));
+        when(mAccessLogsHelper.getLatestAccessLogTimeStamp())
+                .thenReturn(subtractDaysFromInstantNow(/* numberOfDays= */ 0));
+        when(mPreferenceHelper.getPreference(USER_MOST_RECENT_ACCESS_LOG_TIME))
+                .thenReturn(String.valueOf(subtractDaysFromInstantNow(/* numberOfDays= */ 0)));
 
         DailyLoggingService.logDailyMetrics(mContext, mCurrentUser);
 
         // Makes sure we do not have count any app that does not have Health Connect permission
         // declared in the manifest as a connected or an available app.
         ExtendedMockito.verify(
-                () -> HealthFitnessStatsLog.write(eq(HEALTH_CONNECT_USAGE_STATS), eq(1), eq(1)),
+                () ->
+                        HealthFitnessStatsLog.write(
+                                eq(HEALTH_CONNECT_USAGE_STATS), eq(1), eq(1), eq(true)),
                 times(1));
     }
 
@@ -167,11 +186,17 @@
                         .getPackageManager()
                         .getInstalledPackages(any()))
                 .thenReturn(List.of(mPackageInfoConnectedApp));
+        when(mAccessLogsHelper.getLatestAccessLogTimeStamp())
+                .thenReturn(subtractDaysFromInstantNow(/* numberOfDays= */ 0));
+        when(mPreferenceHelper.getPreference(USER_MOST_RECENT_ACCESS_LOG_TIME))
+                .thenReturn(String.valueOf(subtractDaysFromInstantNow(/* numberOfDays= */ 0)));
 
         DailyLoggingService.logDailyMetrics(mContext, mCurrentUser);
 
         ExtendedMockito.verify(
-                () -> HealthFitnessStatsLog.write(eq(HEALTH_CONNECT_USAGE_STATS), eq(1), eq(1)),
+                () ->
+                        HealthFitnessStatsLog.write(
+                                eq(HEALTH_CONNECT_USAGE_STATS), eq(1), eq(1), eq(true)),
                 times(1));
     }
 
@@ -182,11 +207,17 @@
                         .getPackageManager()
                         .getInstalledPackages(any()))
                 .thenReturn(List.of(mPackageInfoNotConnectedApp, mPackageInfoNotConnectedApp));
+        when(mAccessLogsHelper.getLatestAccessLogTimeStamp())
+                .thenReturn(subtractDaysFromInstantNow(/* numberOfDays= */ 31));
+        when(mPreferenceHelper.getPreference(USER_MOST_RECENT_ACCESS_LOG_TIME))
+                .thenReturn(String.valueOf(subtractDaysFromInstantNow(/* numberOfDays= */ 31)));
 
         DailyLoggingService.logDailyMetrics(mContext, mCurrentUser);
 
         ExtendedMockito.verify(
-                () -> HealthFitnessStatsLog.write(eq(HEALTH_CONNECT_USAGE_STATS), eq(0), eq(2)),
+                () ->
+                        HealthFitnessStatsLog.write(
+                                eq(HEALTH_CONNECT_USAGE_STATS), eq(0), eq(2), eq(false)),
                 times(1));
     }
 
@@ -197,11 +228,51 @@
                         .getPackageManager()
                         .getInstalledPackages(any()))
                 .thenReturn(List.of(mPackageInfoNotHoldingPermission));
+        when(mAccessLogsHelper.getLatestAccessLogTimeStamp())
+                .thenReturn(subtractDaysFromInstantNow(/* numberOfDays= */ 31));
+        when(mPreferenceHelper.getPreference(USER_MOST_RECENT_ACCESS_LOG_TIME))
+                .thenReturn(String.valueOf(subtractDaysFromInstantNow(/* numberOfDays= */ 31)));
 
         DailyLoggingService.logDailyMetrics(mContext, mCurrentUser);
 
         ExtendedMockito.verify(
-                () -> HealthFitnessStatsLog.write(eq(HEALTH_CONNECT_USAGE_STATS), eq(0), eq(0)),
+                () ->
+                        HealthFitnessStatsLog.write(
+                                eq(HEALTH_CONNECT_USAGE_STATS), eq(0), eq(0), eq(false)),
                 times(1));
     }
+
+    @Test
+    public void testDailyUsageStatsLogs_healthConnectAccessedPreviousDay_userMonthlyActive() {
+        when(mAccessLogsHelper.getLatestAccessLogTimeStamp())
+                .thenReturn(subtractDaysFromInstantNow(/* numberOfDays= */ 1));
+        when(mPreferenceHelper.getPreference(USER_MOST_RECENT_ACCESS_LOG_TIME))
+                .thenReturn(String.valueOf(subtractDaysFromInstantNow(/* numberOfDays= */ 1)));
+        DailyLoggingService.logDailyMetrics(mContext, mCurrentUser);
+
+        ExtendedMockito.verify(
+                () ->
+                        HealthFitnessStatsLog.write(
+                                eq(HEALTH_CONNECT_USAGE_STATS), eq(0), eq(0), eq(true)),
+                times(1));
+    }
+
+    @Test
+    public void testDailyUsageStatsLogs_healthConnectAccessed31DaysAgo_userNotMonthlyActive() {
+        when(mAccessLogsHelper.getLatestAccessLogTimeStamp())
+                .thenReturn(subtractDaysFromInstantNow(/* numberOfDays= */ 31));
+        when(mPreferenceHelper.getPreference(USER_MOST_RECENT_ACCESS_LOG_TIME))
+                .thenReturn(String.valueOf(subtractDaysFromInstantNow(/* numberOfDays= */ 31)));
+        DailyLoggingService.logDailyMetrics(mContext, mCurrentUser);
+
+        ExtendedMockito.verify(
+                () ->
+                        HealthFitnessStatsLog.write(
+                                eq(HEALTH_CONNECT_USAGE_STATS), eq(0), eq(0), eq(false)),
+                times(1));
+    }
+
+    private long subtractDaysFromInstantNow(int numberOfDays) {
+        return Instant.now().minus(numberOfDays, ChronoUnit.DAYS).toEpochMilli();
+    }
 }
diff --git a/tests/unittests/src/com/android/server/healthconnect/logging/HealthConnectServiceLoggerTest.java b/tests/unittests/src/com/android/server/healthconnect/logging/HealthConnectServiceLoggerTest.java
index ed2ccbf..2321762 100644
--- a/tests/unittests/src/com/android/server/healthconnect/logging/HealthConnectServiceLoggerTest.java
+++ b/tests/unittests/src/com/android/server/healthconnect/logging/HealthConnectServiceLoggerTest.java
@@ -22,6 +22,9 @@
 import static android.health.HealthFitnessStatsLog.HEALTH_CONNECT_API_CALLED__API_STATUS__ERROR;
 import static android.health.HealthFitnessStatsLog.HEALTH_CONNECT_API_CALLED__API_STATUS__STATUS_UNKNOWN;
 import static android.health.HealthFitnessStatsLog.HEALTH_CONNECT_API_CALLED__API_STATUS__SUCCESS;
+import static android.health.HealthFitnessStatsLog.HEALTH_CONNECT_API_CALLED__CALLER_FOREGROUND_STATE__BACKGROUND;
+import static android.health.HealthFitnessStatsLog.HEALTH_CONNECT_API_CALLED__CALLER_FOREGROUND_STATE__FOREGROUND;
+import static android.health.HealthFitnessStatsLog.HEALTH_CONNECT_API_CALLED__CALLER_FOREGROUND_STATE__UNSPECIFIED;
 import static android.health.HealthFitnessStatsLog.HEALTH_CONNECT_API_CALLED__RATE_LIMIT__NOT_USED;
 import static android.health.HealthFitnessStatsLog.HEALTH_CONNECT_API_CALLED__RATE_LIMIT__RATE_LIMIT_BACKGROUND_15_MIN_ABOVE_3000;
 import static android.health.HealthFitnessStatsLog.HEALTH_CONNECT_API_CALLED__RATE_LIMIT__RATE_LIMIT_BACKGROUND_24_HRS_BW_3000_TO_4000;
@@ -53,6 +56,13 @@
 
 public class HealthConnectServiceLoggerTest {
 
+    private static final int CALLER_FOREGROUND_STATE_UNSPECIFIED =
+            HEALTH_CONNECT_API_CALLED__CALLER_FOREGROUND_STATE__UNSPECIFIED;
+    private static final int CALLER_FOREGROUND_STATE_FOREGROUND =
+            HEALTH_CONNECT_API_CALLED__CALLER_FOREGROUND_STATE__FOREGROUND;
+    private static final int CALLER_FOREGROUND_STATE_BACKGROUND =
+            HEALTH_CONNECT_API_CALLED__CALLER_FOREGROUND_STATE__BACKGROUND;
+
     @Rule
     public final ExtendedMockitoRule mExtendedMockitoRule =
             new ExtendedMockitoRule.Builder(this).mockStatic(HealthFitnessStatsLog.class).build();
@@ -67,7 +77,7 @@
                 () ->
                         HealthFitnessStatsLog.write(
                                 anyInt(), anyInt(), anyInt(), anyInt(), anyLong(), anyInt(),
-                                anyInt()),
+                                anyInt(), anyInt()),
                 times(0));
     }
 
@@ -86,7 +96,8 @@
                                 eq(0),
                                 anyLong(),
                                 eq(0),
-                                eq(RateLimitingRanges.NOT_USED)),
+                                eq(RateLimitingRanges.NOT_USED),
+                                eq(CALLER_FOREGROUND_STATE_UNSPECIFIED)),
                 times(1));
     }
 
@@ -108,7 +119,8 @@
                                 eq(0),
                                 anyLong(),
                                 eq(0),
-                                eq(RateLimitingRanges.FOREGROUND_15_MIN_BW_3000_TO_4000)),
+                                eq(RateLimitingRanges.FOREGROUND_15_MIN_BW_3000_TO_4000),
+                                eq(CALLER_FOREGROUND_STATE_UNSPECIFIED)),
                 times(1));
     }
 
@@ -130,7 +142,8 @@
                                 eq(0),
                                 anyLong(),
                                 eq(0),
-                                eq(RateLimitingRanges.BACKGROUND_15_MIN_ABOVE_3000)),
+                                eq(RateLimitingRanges.BACKGROUND_15_MIN_ABOVE_3000),
+                                eq(CALLER_FOREGROUND_STATE_UNSPECIFIED)),
                 times(1));
     }
 
@@ -152,7 +165,8 @@
                                 eq(0),
                                 anyLong(),
                                 eq(0),
-                                eq(RateLimitingRanges.BACKGROUND_24_HRS_BW_3000_TO_4000)),
+                                eq(RateLimitingRanges.BACKGROUND_24_HRS_BW_3000_TO_4000),
+                                eq(CALLER_FOREGROUND_STATE_UNSPECIFIED)),
                 times(1));
     }
 
@@ -174,7 +188,8 @@
                                 eq(0),
                                 anyLong(),
                                 eq(0),
-                                eq(RateLimitingRanges.FOREGROUND_24_HRS_BW_3000_TO_4000)),
+                                eq(RateLimitingRanges.FOREGROUND_24_HRS_BW_3000_TO_4000),
+                                eq(CALLER_FOREGROUND_STATE_UNSPECIFIED)),
                 times(1));
     }
 
@@ -196,7 +211,8 @@
                                 eq(0),
                                 anyLong(),
                                 eq(0),
-                                eq(RateLimitingRanges.FOREGROUND_15_MIN_BW_3000_TO_4000)),
+                                eq(RateLimitingRanges.FOREGROUND_15_MIN_BW_3000_TO_4000),
+                                eq(CALLER_FOREGROUND_STATE_UNSPECIFIED)),
                 times(1));
     }
 
@@ -218,7 +234,8 @@
                                 eq(0),
                                 anyLong(),
                                 eq(0),
-                                eq(RateLimitingRanges.BACKGROUND_15_MIN_ABOVE_3000)),
+                                eq(RateLimitingRanges.BACKGROUND_15_MIN_ABOVE_3000),
+                                eq(CALLER_FOREGROUND_STATE_UNSPECIFIED)),
                 times(1));
     }
 
@@ -240,7 +257,8 @@
                                 eq(0),
                                 anyLong(),
                                 eq(0),
-                                eq(RateLimitingRanges.BACKGROUND_24_HRS_BW_3000_TO_4000)),
+                                eq(RateLimitingRanges.BACKGROUND_24_HRS_BW_3000_TO_4000),
+                                eq(CALLER_FOREGROUND_STATE_UNSPECIFIED)),
                 times(1));
     }
 
@@ -262,7 +280,8 @@
                                 eq(0),
                                 anyLong(),
                                 eq(0),
-                                eq(RateLimitingRanges.FOREGROUND_24_HRS_BW_3000_TO_4000)),
+                                eq(RateLimitingRanges.FOREGROUND_24_HRS_BW_3000_TO_4000),
+                                eq(CALLER_FOREGROUND_STATE_UNSPECIFIED)),
                 times(1));
     }
 
@@ -284,7 +303,8 @@
                                 eq(0),
                                 anyLong(),
                                 eq(0),
-                                eq(RateLimitingRanges.NOT_USED)),
+                                eq(RateLimitingRanges.NOT_USED),
+                                eq(CALLER_FOREGROUND_STATE_UNSPECIFIED)),
                 times(1));
     }
 
@@ -306,7 +326,8 @@
                                 eq(2),
                                 anyLong(),
                                 eq(0),
-                                eq(RateLimitingRanges.NOT_USED)),
+                                eq(RateLimitingRanges.NOT_USED),
+                                eq(CALLER_FOREGROUND_STATE_UNSPECIFIED)),
                 times(1));
     }
 
@@ -328,7 +349,8 @@
                                 eq(0),
                                 anyLong(),
                                 eq(10),
-                                eq(RateLimitingRanges.NOT_USED)),
+                                eq(RateLimitingRanges.NOT_USED),
+                                eq(CALLER_FOREGROUND_STATE_UNSPECIFIED)),
                 times(1));
     }
 
@@ -352,7 +374,8 @@
                                 eq(0),
                                 anyLong(),
                                 eq(10),
-                                eq(RateLimitingRanges.BACKGROUND_15_MIN_ABOVE_3000)),
+                                eq(RateLimitingRanges.BACKGROUND_15_MIN_ABOVE_3000),
+                                eq(CALLER_FOREGROUND_STATE_UNSPECIFIED)),
                 times(1));
     }
 
@@ -376,10 +399,53 @@
                                 eq(1),
                                 anyLong(),
                                 eq(10),
-                                eq(RateLimitingRanges.BACKGROUND_15_MIN_ABOVE_3000)),
+                                eq(RateLimitingRanges.BACKGROUND_15_MIN_ABOVE_3000),
+                                eq(CALLER_FOREGROUND_STATE_UNSPECIFIED)),
                 times(1));
     }
 
+    @Test
+    public void testCallerForegroundState() {
+        new HealthConnectServiceLogger.Builder(false, ApiMethods.API_METHOD_UNKNOWN)
+                .setCallerForegroundState(true)
+                .build()
+                .log();
+
+        // then
+        ExtendedMockito.verify(
+                () ->
+                        HealthFitnessStatsLog.write(
+                                eq(HEALTH_CONNECT_API_CALLED),
+                                eq(HEALTH_CONNECT_API_CALLED__API_METHOD__API_METHOD_UNKNOWN),
+                                eq(HEALTH_CONNECT_API_CALLED__API_STATUS__STATUS_UNKNOWN),
+                                eq(0),
+                                anyLong(),
+                                eq(0),
+                                eq(RateLimitingRanges.NOT_USED),
+                                eq(CALLER_FOREGROUND_STATE_FOREGROUND)));
+    }
+
+    @Test
+    public void testCallerBackgroundState() {
+        new HealthConnectServiceLogger.Builder(false, ApiMethods.API_METHOD_UNKNOWN)
+                .setCallerForegroundState(false)
+                .build()
+                .log();
+
+        // then
+        ExtendedMockito.verify(
+                () ->
+                        HealthFitnessStatsLog.write(
+                                eq(HEALTH_CONNECT_API_CALLED),
+                                eq(HEALTH_CONNECT_API_CALLED__API_METHOD__API_METHOD_UNKNOWN),
+                                eq(HEALTH_CONNECT_API_CALLED__API_STATUS__STATUS_UNKNOWN),
+                                eq(0),
+                                anyLong(),
+                                eq(0),
+                                eq(RateLimitingRanges.NOT_USED),
+                                eq(CALLER_FOREGROUND_STATE_BACKGROUND)));
+    }
+
     private static final class RateLimitingRanges {
 
         private static final int NOT_USED = HEALTH_CONNECT_API_CALLED__RATE_LIMIT__NOT_USED;
diff --git a/tests/unittests/src/com/android/server/healthconnect/permission/PermissionPackageChangesOrchestratorTest.java b/tests/unittests/src/com/android/server/healthconnect/permission/PermissionPackageChangesOrchestratorTest.java
index 3821a70..5965570 100644
--- a/tests/unittests/src/com/android/server/healthconnect/permission/PermissionPackageChangesOrchestratorTest.java
+++ b/tests/unittests/src/com/android/server/healthconnect/permission/PermissionPackageChangesOrchestratorTest.java
@@ -35,6 +35,7 @@
 import androidx.test.core.app.ApplicationProvider;
 
 import com.android.modules.utils.testing.ExtendedMockitoRule;
+import com.android.server.healthconnect.HealthConnectDeviceConfigManager;
 import com.android.server.healthconnect.storage.TransactionManager;
 import com.android.server.healthconnect.storage.datatypehelpers.HealthDataCategoryPriorityHelper;
 
@@ -52,6 +53,7 @@
     public final ExtendedMockitoRule mExtendedMockitoRule =
             new ExtendedMockitoRule.Builder(this)
                     .mockStatic(TransactionManager.class)
+                    .mockStatic(HealthConnectDeviceConfigManager.class)
                     .mockStatic(HealthDataCategoryPriorityHelper.class)
                     .setStrictness(Strictness.LENIENT)
                     .build();
@@ -64,6 +66,7 @@
     @Mock private HealthPermissionIntentAppsTracker mTracker;
     @Mock private FirstGrantTimeManager mFirstGrantTimeManager;
     @Mock private TransactionManager mTransactionManager;
+    @Mock private HealthConnectDeviceConfigManager mHealthConnectDeviceConfigManager;
     @Mock private UserHandle mUserHandle;
 
     @Mock private HealthDataCategoryPriorityHelper mHealthDataCategoryPriorityHelper;
@@ -73,7 +76,8 @@
         when(HealthDataCategoryPriorityHelper.getInstance())
                 .thenReturn(mHealthDataCategoryPriorityHelper);
         when(TransactionManager.getInitialisedInstance()).thenReturn(mTransactionManager);
-
+        when(HealthConnectDeviceConfigManager.getInitialisedInstance())
+                .thenReturn(mHealthConnectDeviceConfigManager);
         mContext = ApplicationProvider.getApplicationContext();
         mCurrentUid = mContext.getPackageManager().getPackageUid(SELF_PACKAGE_NAME, 0);
         mOrchestrator =
@@ -113,11 +117,15 @@
     }
 
     @Test
-    public void testPackageRemoved_removesFromPriorityList() {
+    public void testPackageRemoved_removesFromPriorityList_whenNewAggregationOff() {
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(false);
         mOrchestrator.onReceive(
                 mContext,
                 buildPackageIntent(Intent.ACTION_PACKAGE_REMOVED, /* isReplaced= */ false));
-        assertThat(mHealthDataCategoryPriorityHelper.getPriorityOrder(HealthDataCategory.SLEEP))
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.getPriorityOrder(
+                                HealthDataCategory.SLEEP, mContext))
                 .isEmpty();
     }
 
diff --git a/tests/unittests/src/com/android/server/healthconnect/storage/TransactionManagerExerciseRoutesTest.java b/tests/unittests/src/com/android/server/healthconnect/storage/TransactionManagerExerciseRoutesTest.java
new file mode 100644
index 0000000..f17187b
--- /dev/null
+++ b/tests/unittests/src/com/android/server/healthconnect/storage/TransactionManagerExerciseRoutesTest.java
@@ -0,0 +1,330 @@
+/*
+ * 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.server.healthconnect.storage;
+
+import static com.android.server.healthconnect.storage.datatypehelpers.TransactionTestUtils.createExerciseSessionRecordWithRoute;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.Manifest;
+import android.health.connect.HealthPermissions;
+import android.health.connect.ReadRecordsRequestUsingFilters;
+import android.health.connect.TimeInstantRangeFilter;
+import android.health.connect.datatypes.ExerciseSessionRecord;
+import android.health.connect.datatypes.RecordTypeIdentifier;
+import android.health.connect.internal.datatypes.ExerciseSessionRecordInternal;
+import android.health.connect.internal.datatypes.RecordInternal;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.healthconnect.HealthConnectDeviceConfigManager;
+import com.android.server.healthconnect.HealthConnectUserContext;
+import com.android.server.healthconnect.storage.datatypehelpers.DatabaseHelper;
+import com.android.server.healthconnect.storage.datatypehelpers.HealthConnectDatabaseTestRule;
+import com.android.server.healthconnect.storage.datatypehelpers.TransactionTestUtils;
+import com.android.server.healthconnect.storage.request.ReadTransactionRequest;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+@RunWith(AndroidJUnit4.class)
+public class TransactionManagerExerciseRoutesTest {
+    private static final String TEST_PACKAGE_NAME = "package.name";
+    private static final String FOO_PACKAGE_NAME = "package.foo";
+    private static final String BAR_PACKAGE_NAME = "package.bar";
+    private static final String UNKNOWN_PACKAGE_NAME = "package.unknown";
+    private static final Map<String, Boolean> NO_EXTRA_PERMS = Map.of();
+    @Rule public final HealthConnectDatabaseTestRule testRule = new HealthConnectDatabaseTestRule();
+
+    private TransactionTestUtils mTransactionTestUtils;
+    private TransactionManager mTransactionManager;
+
+    @Before
+    public void setup() {
+        InstrumentationRegistry.getInstrumentation()
+                .getUiAutomation()
+                .adoptShellPermissionIdentity(Manifest.permission.READ_DEVICE_CONFIG);
+        HealthConnectUserContext context = testRule.getUserContext();
+        mTransactionManager = TransactionManager.getInstance(context);
+        mTransactionTestUtils = new TransactionTestUtils(context, mTransactionManager);
+        mTransactionTestUtils.insertApp(TEST_PACKAGE_NAME);
+        mTransactionTestUtils.insertApp(FOO_PACKAGE_NAME);
+        mTransactionTestUtils.insertApp(BAR_PACKAGE_NAME);
+        HealthConnectDeviceConfigManager.initializeInstance(context);
+    }
+
+    @After
+    public void tearDown() {
+        DatabaseHelper.clearAllData(mTransactionManager);
+        TransactionManager.clearInstance();
+    }
+
+    @Test
+    public void readRecordsByIds_doesNotReturnRoutesOfOtherApps() {
+        ExerciseSessionRecordInternal fooSession =
+                createExerciseSessionRecordWithRoute(Instant.ofEpochSecond(10000));
+        ExerciseSessionRecordInternal barSession =
+                createExerciseSessionRecordWithRoute(Instant.ofEpochSecond(11000));
+        ExerciseSessionRecordInternal ownSession =
+                createExerciseSessionRecordWithRoute(Instant.ofEpochSecond(12000));
+        String fooUuid = mTransactionTestUtils.insertRecords(FOO_PACKAGE_NAME, fooSession).get(0);
+        String barUuid = mTransactionTestUtils.insertRecords(BAR_PACKAGE_NAME, barSession).get(0);
+        String ownUuid = mTransactionTestUtils.insertRecords(TEST_PACKAGE_NAME, ownSession).get(0);
+        List<UUID> allUuids = Stream.of(fooUuid, barUuid, ownUuid).map(UUID::fromString).toList();
+        ReadTransactionRequest request =
+                new ReadTransactionRequest(
+                        TEST_PACKAGE_NAME,
+                        ImmutableMap.of(
+                                RecordTypeIdentifier.RECORD_TYPE_EXERCISE_SESSION, allUuids),
+                        /* startDateAccess= */ 0,
+                        NO_EXTRA_PERMS);
+
+        List<RecordInternal<?>> returnedRecords = mTransactionManager.readRecordsByIds(request);
+
+        Map<String, ExerciseSessionRecordInternal> idToSessionMap =
+                returnedRecords.stream()
+                        .collect(
+                                Collectors.toMap(
+                                        record -> record.getUuid().toString(),
+                                        ExerciseSessionRecordInternal.class::cast));
+        assertThat(idToSessionMap.get(fooUuid).getRoute()).isNull();
+        assertThat(idToSessionMap.get(barUuid).getRoute()).isNull();
+        assertThat(idToSessionMap.get(ownUuid).getRoute()).isEqualTo(ownSession.getRoute());
+        assertThat(idToSessionMap.get(fooUuid).hasRoute()).isTrue();
+        assertThat(idToSessionMap.get(barUuid).hasRoute()).isTrue();
+        assertThat(idToSessionMap.get(ownUuid).hasRoute()).isTrue();
+    }
+
+    @Test
+    public void readRecordsByIds_unknownApp_doesNotReturnRoute() {
+        ExerciseSessionRecordInternal session =
+                createExerciseSessionRecordWithRoute(Instant.ofEpochSecond(12000));
+        UUID uuid =
+                UUID.fromString(
+                        mTransactionTestUtils.insertRecords(TEST_PACKAGE_NAME, session).get(0));
+        ReadTransactionRequest request =
+                new ReadTransactionRequest(
+                        UNKNOWN_PACKAGE_NAME,
+                        ImmutableMap.of(
+                                RecordTypeIdentifier.RECORD_TYPE_EXERCISE_SESSION, List.of(uuid)),
+                        /* startDateAccess= */ 0,
+                        NO_EXTRA_PERMS);
+
+        List<RecordInternal<?>> returnedRecords = mTransactionManager.readRecordsByIds(request);
+
+        assertThat(returnedRecords).hasSize(1);
+        ExerciseSessionRecordInternal returnedRecord =
+                (ExerciseSessionRecordInternal) returnedRecords.get(0);
+        assertThat(returnedRecord.hasRoute()).isTrue();
+        assertThat(returnedRecord.getRoute()).isNull();
+    }
+
+    @Test
+    public void readRecordsByIds_nullPackageName_doesNotReturnRoute() {
+        ExerciseSessionRecordInternal session =
+                createExerciseSessionRecordWithRoute(Instant.ofEpochSecond(12000));
+        UUID uuid =
+                UUID.fromString(
+                        mTransactionTestUtils.insertRecords(TEST_PACKAGE_NAME, session).get(0));
+        ReadTransactionRequest request =
+                new ReadTransactionRequest(
+                        null,
+                        ImmutableMap.of(
+                                RecordTypeIdentifier.RECORD_TYPE_EXERCISE_SESSION, List.of(uuid)),
+                        /* startDateAccess= */ 0,
+                        NO_EXTRA_PERMS);
+
+        List<RecordInternal<?>> returnedRecords = mTransactionManager.readRecordsByIds(request);
+
+        assertThat(returnedRecords).hasSize(1);
+        ExerciseSessionRecordInternal returnedRecord =
+                (ExerciseSessionRecordInternal) returnedRecords.get(0);
+        assertThat(returnedRecord.hasRoute()).isTrue();
+        assertThat(returnedRecord.getRoute()).isNull();
+    }
+
+    @Test
+    public void readRecordsByIds_unknownApp_withReadRoutePermission_returnsRoute() {
+        ExerciseSessionRecordInternal session =
+                createExerciseSessionRecordWithRoute(Instant.ofEpochSecond(12000));
+        UUID uuid =
+                UUID.fromString(
+                        mTransactionTestUtils.insertRecords(TEST_PACKAGE_NAME, session).get(0));
+        ReadTransactionRequest request =
+                new ReadTransactionRequest(
+                        UNKNOWN_PACKAGE_NAME,
+                        ImmutableMap.of(
+                                RecordTypeIdentifier.RECORD_TYPE_EXERCISE_SESSION, List.of(uuid)),
+                        /* startDateAccess= */ 0,
+                        Map.of(HealthPermissions.READ_EXERCISE_ROUTE, true));
+
+        List<RecordInternal<?>> returnedRecords = mTransactionManager.readRecordsByIds(request);
+
+        assertThat(returnedRecords).hasSize(1);
+        ExerciseSessionRecordInternal returnedRecord =
+                (ExerciseSessionRecordInternal) returnedRecords.get(0);
+        assertThat(returnedRecord.hasRoute()).isTrue();
+        assertThat(returnedRecord.getRoute()).isEqualTo(session.getRoute());
+    }
+
+    @Test
+    public void readRecordsAndPageToken_byFilters_doesNotReturnRoutesOfOtherApps() {
+        ExerciseSessionRecordInternal fooSession =
+                createExerciseSessionRecordWithRoute(Instant.ofEpochSecond(10000));
+        ExerciseSessionRecordInternal barSession =
+                createExerciseSessionRecordWithRoute(Instant.ofEpochSecond(11000));
+        ExerciseSessionRecordInternal ownSession =
+                createExerciseSessionRecordWithRoute(Instant.ofEpochSecond(12000));
+        String fooUuid = mTransactionTestUtils.insertRecords(FOO_PACKAGE_NAME, fooSession).get(0);
+        String barUuid = mTransactionTestUtils.insertRecords(BAR_PACKAGE_NAME, barSession).get(0);
+        String ownUuid = mTransactionTestUtils.insertRecords(TEST_PACKAGE_NAME, ownSession).get(0);
+        ReadTransactionRequest request =
+                new ReadTransactionRequest(
+                        TEST_PACKAGE_NAME,
+                        new ReadRecordsRequestUsingFilters.Builder<>(ExerciseSessionRecord.class)
+                                .setTimeRangeFilter(
+                                        new TimeInstantRangeFilter.Builder()
+                                                .setStartTime(Instant.EPOCH)
+                                                .setEndTime(Instant.ofEpochSecond(100000))
+                                                .build())
+                                .build()
+                                .toReadRecordsRequestParcel(),
+                        /* startDateAccessMillis= */ 0,
+                        /* enforceSelfRead= */ false,
+                        NO_EXTRA_PERMS);
+
+        List<RecordInternal<?>> returnedRecords =
+                mTransactionManager.readRecordsAndPageToken(request).first;
+
+        Map<String, ExerciseSessionRecordInternal> idToSessionMap =
+                returnedRecords.stream()
+                        .collect(
+                                Collectors.toMap(
+                                        record -> record.getUuid().toString(),
+                                        ExerciseSessionRecordInternal.class::cast));
+
+        assertThat(idToSessionMap.get(fooUuid).getRoute()).isNull();
+        assertThat(idToSessionMap.get(barUuid).getRoute()).isNull();
+        assertThat(idToSessionMap.get(ownUuid).getRoute()).isEqualTo(ownSession.getRoute());
+        assertThat(idToSessionMap.get(fooUuid).hasRoute()).isTrue();
+        assertThat(idToSessionMap.get(barUuid).hasRoute()).isTrue();
+        assertThat(idToSessionMap.get(ownUuid).hasRoute()).isTrue();
+    }
+
+    @Test
+    public void readRecordsAndPageToken_byFilters_unknownApp_doesNotReturnRoute() {
+        ExerciseSessionRecordInternal session =
+                createExerciseSessionRecordWithRoute(Instant.ofEpochSecond(12000));
+        mTransactionTestUtils.insertRecords(TEST_PACKAGE_NAME, session);
+        ReadTransactionRequest request =
+                new ReadTransactionRequest(
+                        UNKNOWN_PACKAGE_NAME,
+                        new ReadRecordsRequestUsingFilters.Builder<>(ExerciseSessionRecord.class)
+                                .setTimeRangeFilter(
+                                        new TimeInstantRangeFilter.Builder()
+                                                .setStartTime(Instant.EPOCH)
+                                                .setEndTime(Instant.ofEpochSecond(100000))
+                                                .build())
+                                .build()
+                                .toReadRecordsRequestParcel(),
+                        /* startDateAccessMillis= */ 0,
+                        /* enforceSelfRead= */ false,
+                        NO_EXTRA_PERMS);
+
+        List<RecordInternal<?>> returnedRecords =
+                mTransactionManager.readRecordsAndPageToken(request).first;
+
+        assertThat(returnedRecords).hasSize(1);
+        ExerciseSessionRecordInternal returnedRecord =
+                (ExerciseSessionRecordInternal) returnedRecords.get(0);
+        assertThat(returnedRecord.hasRoute()).isTrue();
+        assertThat(returnedRecord.getRoute()).isNull();
+    }
+
+    @Test
+    public void readRecordsAndPageToken_byFilters_nullPackageName_doesNotReturnRoute() {
+        ExerciseSessionRecordInternal session =
+                createExerciseSessionRecordWithRoute(Instant.ofEpochSecond(12000));
+        mTransactionTestUtils.insertRecords(TEST_PACKAGE_NAME, session);
+        ReadTransactionRequest request =
+                new ReadTransactionRequest(
+                        null,
+                        new ReadRecordsRequestUsingFilters.Builder<>(ExerciseSessionRecord.class)
+                                .setTimeRangeFilter(
+                                        new TimeInstantRangeFilter.Builder()
+                                                .setStartTime(Instant.EPOCH)
+                                                .setEndTime(Instant.ofEpochSecond(100000))
+                                                .build())
+                                .build()
+                                .toReadRecordsRequestParcel(),
+                        /* startDateAccessMillis= */ 0,
+                        /* enforceSelfRead= */ false,
+                        NO_EXTRA_PERMS);
+
+        List<RecordInternal<?>> returnedRecords =
+                mTransactionManager.readRecordsAndPageToken(request).first;
+
+        assertThat(returnedRecords).hasSize(1);
+        ExerciseSessionRecordInternal returnedRecord =
+                (ExerciseSessionRecordInternal) returnedRecords.get(0);
+        assertThat(returnedRecord.hasRoute()).isTrue();
+        assertThat(returnedRecord.getRoute()).isNull();
+    }
+
+    @Test
+    public void readRecordsAndPageToken_byFilters_withReadRoutePermission_returnsRoute() {
+        ExerciseSessionRecordInternal session =
+                createExerciseSessionRecordWithRoute(Instant.ofEpochSecond(12000));
+        mTransactionTestUtils.insertRecords(TEST_PACKAGE_NAME, session);
+        ReadTransactionRequest request =
+                new ReadTransactionRequest(
+                        UNKNOWN_PACKAGE_NAME,
+                        new ReadRecordsRequestUsingFilters.Builder<>(ExerciseSessionRecord.class)
+                                .setTimeRangeFilter(
+                                        new TimeInstantRangeFilter.Builder()
+                                                .setStartTime(Instant.EPOCH)
+                                                .setEndTime(Instant.ofEpochSecond(100000))
+                                                .build())
+                                .build()
+                                .toReadRecordsRequestParcel(),
+                        /* startDateAccessMillis= */ 0,
+                        /* enforceSelfRead= */ false,
+                        Map.of(HealthPermissions.READ_EXERCISE_ROUTE, true));
+
+        List<RecordInternal<?>> returnedRecords =
+                mTransactionManager.readRecordsAndPageToken(request).first;
+
+        assertThat(returnedRecords).hasSize(1);
+        ExerciseSessionRecordInternal returnedRecord =
+                (ExerciseSessionRecordInternal) returnedRecords.get(0);
+        assertThat(returnedRecord.hasRoute()).isTrue();
+        assertThat(returnedRecord.getRoute()).isEqualTo(session.getRoute());
+    }
+}
diff --git a/tests/unittests/src/com/android/server/healthconnect/storage/TransactionManagerTest.java b/tests/unittests/src/com/android/server/healthconnect/storage/TransactionManagerTest.java
new file mode 100644
index 0000000..e3e8ab7
--- /dev/null
+++ b/tests/unittests/src/com/android/server/healthconnect/storage/TransactionManagerTest.java
@@ -0,0 +1,198 @@
+/*
+ * 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.server.healthconnect.storage;
+
+import static com.android.server.healthconnect.storage.datatypehelpers.TransactionTestUtils.createBloodPressureRecord;
+import static com.android.server.healthconnect.storage.datatypehelpers.TransactionTestUtils.createStepsRecord;
+import static com.android.server.healthconnect.storage.datatypehelpers.TransactionTestUtils.getReadTransactionRequest;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.health.connect.ReadRecordsRequestUsingFilters;
+import android.health.connect.ReadRecordsRequestUsingIds;
+import android.health.connect.TimeInstantRangeFilter;
+import android.health.connect.datatypes.BloodPressureRecord;
+import android.health.connect.datatypes.RecordTypeIdentifier;
+import android.health.connect.datatypes.StepsRecord;
+import android.health.connect.internal.datatypes.RecordInternal;
+import android.util.Pair;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.healthconnect.HealthConnectUserContext;
+import com.android.server.healthconnect.storage.datatypehelpers.DatabaseHelper;
+import com.android.server.healthconnect.storage.datatypehelpers.HealthConnectDatabaseTestRule;
+import com.android.server.healthconnect.storage.datatypehelpers.TransactionTestUtils;
+import com.android.server.healthconnect.storage.request.ReadTransactionRequest;
+import com.android.server.healthconnect.storage.utils.PageTokenUtil;
+import com.android.server.healthconnect.storage.utils.PageTokenWrapper;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.UUID;
+
+@RunWith(AndroidJUnit4.class)
+public class TransactionManagerTest {
+    private static final String TEST_PACKAGE_NAME = "package.name";
+
+    @Rule public final HealthConnectDatabaseTestRule testRule = new HealthConnectDatabaseTestRule();
+
+    private TransactionTestUtils mTransactionTestUtils;
+    private TransactionManager mTransactionManager;
+
+    @Before
+    public void setup() {
+        HealthConnectUserContext context = testRule.getUserContext();
+        mTransactionManager = TransactionManager.getInstance(context);
+        mTransactionTestUtils = new TransactionTestUtils(context, mTransactionManager);
+        mTransactionTestUtils.insertApp(TEST_PACKAGE_NAME);
+    }
+
+    @After
+    public void tearDown() {
+        DatabaseHelper.clearAllData(mTransactionManager);
+        TransactionManager.clearInstance();
+    }
+
+    @Test
+    public void readRecordsById_returnsAllRecords() {
+        long timeMillis = 456;
+        String uuid =
+                mTransactionTestUtils
+                        .insertRecords(
+                                TEST_PACKAGE_NAME,
+                                createBloodPressureRecord(timeMillis, 120.0, 80.0))
+                        .get(0);
+
+        ReadRecordsRequestUsingIds<BloodPressureRecord> request =
+                new ReadRecordsRequestUsingIds.Builder<>(BloodPressureRecord.class)
+                        .addId(uuid)
+                        .build();
+        ReadTransactionRequest readTransactionRequest =
+                getReadTransactionRequest(request.toReadRecordsRequestParcel());
+
+        List<RecordInternal<?>> records =
+                mTransactionManager.readRecordsByIds(readTransactionRequest);
+        assertThat(records).hasSize(1);
+        assertThat(records.get(0).getUuid()).isEqualTo(UUID.fromString(uuid));
+    }
+
+    @Test
+    public void readRecordsById_multipleRecordTypes_returnsAllRecords() {
+        long startTimeMillis = 123;
+        long endTimeMillis = 456;
+        List<String> uuids =
+                mTransactionTestUtils.insertRecords(
+                        TEST_PACKAGE_NAME,
+                        createStepsRecord(startTimeMillis, endTimeMillis, 100),
+                        createBloodPressureRecord(endTimeMillis, 120.0, 80.0));
+
+        List<UUID> stepsUuids = ImmutableList.of(UUID.fromString(uuids.get(0)));
+        List<UUID> bloodPressureUuids = ImmutableList.of(UUID.fromString(uuids.get(1)));
+        ReadTransactionRequest request =
+                getReadTransactionRequest(
+                        ImmutableMap.of(
+                                RecordTypeIdentifier.RECORD_TYPE_STEPS,
+                                stepsUuids,
+                                RecordTypeIdentifier.RECORD_TYPE_BLOOD_PRESSURE,
+                                bloodPressureUuids));
+
+        List<RecordInternal<?>> records = mTransactionManager.readRecordsByIds(request);
+        assertThat(records).hasSize(2);
+        assertThat(records.get(0).getUuid()).isEqualTo(UUID.fromString(uuids.get(0)));
+        assertThat(records.get(1).getUuid()).isEqualTo(UUID.fromString(uuids.get(1)));
+    }
+
+    @Test
+    public void readRecordsById_readByFilterRequest_throws() {
+        ReadRecordsRequestUsingFilters<StepsRecord> request =
+                new ReadRecordsRequestUsingFilters.Builder<>(StepsRecord.class)
+                        .setTimeRangeFilter(
+                                new TimeInstantRangeFilter.Builder()
+                                        .setStartTime(Instant.EPOCH)
+                                        .setEndTime(Instant.ofEpochMilli(1000))
+                                        .build())
+                        .setPageSize(1)
+                        .build();
+        ReadTransactionRequest readTransactionRequest =
+                getReadTransactionRequest(request.toReadRecordsRequestParcel());
+        Throwable thrown =
+                assertThrows(
+                        IllegalArgumentException.class,
+                        () -> mTransactionManager.readRecordsByIds(readTransactionRequest));
+        assertThat(thrown).hasMessageThat().contains("Expect read by id request");
+    }
+
+    @Test
+    public void readRecordsAndPageToken_returnsRecordsAndPageToken() {
+        List<String> uuids =
+                mTransactionTestUtils.insertRecords(
+                        TEST_PACKAGE_NAME,
+                        createStepsRecord(400, 500, 100),
+                        createStepsRecord(500, 600, 100));
+
+        ReadRecordsRequestUsingFilters<StepsRecord> request =
+                new ReadRecordsRequestUsingFilters.Builder<>(StepsRecord.class)
+                        .setTimeRangeFilter(
+                                new TimeInstantRangeFilter.Builder()
+                                        .setStartTime(Instant.EPOCH)
+                                        .setEndTime(Instant.ofEpochMilli(1000))
+                                        .build())
+                        .setPageSize(1)
+                        .build();
+        long expectedToken =
+                PageTokenUtil.encode(
+                        PageTokenWrapper.of(
+                                /* isAscending= */ true, /* timeMillis= */ 500, /* offset= */ 0));
+
+        ReadTransactionRequest readTransactionRequest =
+                getReadTransactionRequest(request.toReadRecordsRequestParcel());
+        Pair<List<RecordInternal<?>>, Long> result =
+                mTransactionManager.readRecordsAndPageToken(readTransactionRequest);
+        List<RecordInternal<?>> records = result.first;
+        assertThat(records).hasSize(1);
+        assertThat(result.first.get(0).getUuid()).isEqualTo(UUID.fromString(uuids.get(0)));
+        assertThat(result.second).isEqualTo(expectedToken);
+    }
+
+    @Test
+    public void readRecordsAndPageToken_readByIdRequest_throws() {
+        ReadRecordsRequestUsingIds<BloodPressureRecord> request =
+                new ReadRecordsRequestUsingIds.Builder<>(BloodPressureRecord.class)
+                        .addId(UUID.randomUUID().toString())
+                        .build();
+        ReadTransactionRequest readTransactionRequest =
+                getReadTransactionRequest(request.toReadRecordsRequestParcel());
+
+        Throwable thrown =
+                assertThrows(
+                        IllegalArgumentException.class,
+                        () -> mTransactionManager.readRecordsAndPageToken(readTransactionRequest));
+        assertThat(thrown).hasMessageThat().contains("Expect read by filter request");
+    }
+}
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 fb7c87e..8d4b270 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
@@ -23,20 +23,38 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import static java.util.Objects.requireNonNull;
 
+import android.content.Context;
+import android.content.pm.PackageInfo;
 import android.database.Cursor;
+import android.health.connect.HealthConnectManager;
 import android.health.connect.HealthDataCategory;
+import android.health.connect.HealthPermissions;
+import android.health.connect.datatypes.RecordTypeIdentifier;
+import android.os.UserHandle;
 
+import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.modules.utils.testing.ExtendedMockitoRule;
+import com.android.server.healthconnect.HealthConnectDeviceConfigManager;
 import com.android.server.healthconnect.TestUtils;
+import com.android.server.healthconnect.permission.HealthConnectPermissionHelper;
+import com.android.server.healthconnect.permission.PackageInfoUtils;
 import com.android.server.healthconnect.storage.TransactionManager;
+import com.android.server.healthconnect.storage.request.DeleteTableRequest;
 import com.android.server.healthconnect.storage.request.UpsertTableRequest;
+import com.android.server.healthconnect.storage.utils.StorageUtils;
 
 import org.junit.After;
 import org.junit.Before;
@@ -45,10 +63,15 @@
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentMatcher;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.quality.Strictness;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 @RunWith(AndroidJUnit4.class)
 public class HealthDataCategoryPriorityHelperTest {
@@ -72,131 +95,1492 @@
             new ExtendedMockitoRule.Builder(this)
                     .mockStatic(TransactionManager.class)
                     .mockStatic(AppInfoHelper.class)
+                    .mockStatic(HealthConnectDeviceConfigManager.class)
+                    .mockStatic(PackageInfoUtils.class)
+                    .mockStatic(PreferenceHelper.class)
+                    .mockStatic(HealthConnectManager.class)
                     .setStrictness(Strictness.LENIENT)
                     .build();
 
     @Mock private Cursor mCursor;
     @Mock private TransactionManager mTransactionManager;
     @Mock private AppInfoHelper mAppInfoHelper;
+    @Mock private HealthConnectDeviceConfigManager mHealthConnectDeviceConfigManager;
+    @Mock private HealthConnectPermissionHelper mHealthConnectPermissionHelper;
+    @Mock private PackageInfoUtils mPackageInfoUtils;
+    @Mock private PackageInfo mPackageInfo1;
+    @Mock private PackageInfo mPackageInfo2;
+    @Mock private PackageInfo mPackageInfo3;
+    @Mock private PreferenceHelper mPreferenceHelper;
     private HealthDataCategoryPriorityHelper mHealthDataCategoryPriorityHelper;
+    private Context mContext;
 
     @Before
     public void setUp() throws Exception {
         when(TransactionManager.getInitialisedInstance()).thenReturn(mTransactionManager);
         when(mTransactionManager.read(any())).thenReturn(mCursor);
+        when(mTransactionManager.getCurrentUserHandle()).thenReturn(UserHandle.CURRENT);
         when(AppInfoHelper.getInstance()).thenReturn(mAppInfoHelper);
-        when(mCursor.moveToNext()).thenReturn(true, false);
+        when(mAppInfoHelper.getOrInsertAppInfoId(eq(APP_PACKAGE_NAME), any()))
+                .thenReturn(APP_PACKAGE_ID);
+        when(mAppInfoHelper.getOrInsertAppInfoId(eq(APP_PACKAGE_NAME_2), any()))
+                .thenReturn(APP_PACKAGE_ID_2);
+        when(mAppInfoHelper.getOrInsertAppInfoId(eq(APP_PACKAGE_NAME_3), any()))
+                .thenReturn(APP_PACKAGE_ID_3);
+        when(mAppInfoHelper.getOrInsertAppInfoId(eq(APP_PACKAGE_NAME_4), any()))
+                .thenReturn(APP_PACKAGE_ID_4);
+        when(mAppInfoHelper.getAppInfoId(APP_PACKAGE_NAME)).thenReturn(APP_PACKAGE_ID);
+        when(mAppInfoHelper.getAppInfoId(APP_PACKAGE_NAME_2)).thenReturn(APP_PACKAGE_ID_2);
+        when(mAppInfoHelper.getAppInfoId(APP_PACKAGE_NAME_3)).thenReturn(APP_PACKAGE_ID_3);
+        when(mAppInfoHelper.getAppInfoId(APP_PACKAGE_NAME_4)).thenReturn(APP_PACKAGE_ID_4);
+        when(mAppInfoHelper.getPackageName(APP_PACKAGE_ID)).thenReturn(APP_PACKAGE_NAME);
+        when(mAppInfoHelper.getPackageName(APP_PACKAGE_ID_2)).thenReturn(APP_PACKAGE_NAME_2);
+        when(mAppInfoHelper.getPackageName(APP_PACKAGE_ID_3)).thenReturn(APP_PACKAGE_NAME_3);
+        when(mAppInfoHelper.getPackageName(APP_PACKAGE_ID_4)).thenReturn(APP_PACKAGE_NAME_4);
+        when(PackageInfoUtils.getInstance()).thenReturn(mPackageInfoUtils);
         when(mCursor.getColumnIndex(eq(HEALTH_DATA_CATEGORY_COLUMN_NAME)))
                 .thenReturn(HEALTH_DATA_CATEGORY_COLUMN_INDEX);
         when(mCursor.getColumnIndex(eq(APP_ID_PRIORITY_ORDER_COLUMN_NAME)))
                 .thenReturn(APP_ID_PRIORITY_ORDER_COLUMN_INDEX);
-
+        when(HealthConnectDeviceConfigManager.getInitialisedInstance())
+                .thenReturn(mHealthConnectDeviceConfigManager);
+        when(PreferenceHelper.getInstance()).thenReturn(mPreferenceHelper);
         mHealthDataCategoryPriorityHelper = HealthDataCategoryPriorityHelper.getInstance();
         // Clear data in case the singleton is already initialised.
         mHealthDataCategoryPriorityHelper.clearData(mTransactionManager);
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
     }
 
     @After
     public void tearDown() throws Exception {
         TestUtils.waitForAllScheduledTasksToComplete();
+        reset(mPackageInfo1, mPackageInfo2, mPackageInfo3);
         mHealthDataCategoryPriorityHelper.clearData(mTransactionManager);
+        clearInvocations(mPreferenceHelper);
+        clearInvocations(mTransactionManager);
+        clearInvocations(mHealthConnectDeviceConfigManager);
     }
 
     @Test
-    public void testSetPriority_additionalPackagesDifferentOrder_newPackagesRemoved() {
-        List<String> newPriorityOrder =
-                List.of(
-                        APP_PACKAGE_NAME_4,
-                        APP_PACKAGE_NAME_3,
-                        APP_PACKAGE_NAME_2,
-                        APP_PACKAGE_NAME);
+    public void testAppendToPriorityList_ifAppInList_doesNotAddToList() {
+        Map<Integer, List<Long>> priorityList = new HashMap<>();
+        priorityList.put(
+                HealthDataCategory.BODY_MEASUREMENTS, List.of(APP_PACKAGE_ID, APP_PACKAGE_ID_2));
+        setupPriorityList(priorityList);
 
-        List<Long> newPriorityOrderId = new ArrayList<>();
-        newPriorityOrderId.add(APP_PACKAGE_ID_4);
-        newPriorityOrderId.add(APP_PACKAGE_ID_3);
-        newPriorityOrderId.add(APP_PACKAGE_ID_2);
-        newPriorityOrderId.add(APP_PACKAGE_ID);
+        when(mAppInfoHelper.getOrInsertAppInfoId(any(), any())).thenReturn(APP_PACKAGE_ID);
+        mHealthDataCategoryPriorityHelper.appendToPriorityList(
+                APP_PACKAGE_NAME, HealthDataCategory.BODY_MEASUREMENTS, mContext, true);
 
-        when(mCursor.getInt(eq(HEALTH_DATA_CATEGORY_COLUMN_INDEX)))
-                .thenReturn(HealthDataCategory.BODY_MEASUREMENTS);
-        when(mCursor.getString(eq(APP_ID_PRIORITY_ORDER_COLUMN_INDEX)))
-                .thenReturn(flattenLongList(List.of(APP_PACKAGE_ID, APP_PACKAGE_ID_2)));
-        when(mAppInfoHelper.getAppInfoIds(eq(newPriorityOrder))).thenReturn(newPriorityOrderId);
-
-        mHealthDataCategoryPriorityHelper.setPriorityOrder(
-                HealthDataCategory.BODY_MEASUREMENTS, newPriorityOrder);
-
-        verifyPriorityUpdate(List.of(APP_PACKAGE_ID_2, APP_PACKAGE_ID));
+        verify(mTransactionManager, never()).insertOrReplace(any());
     }
 
     @Test
-    public void testSetPriority_reducedPackagesDifferentOrder_oldPackagesAdded() {
-        List<String> newPriorityOrder = List.of(APP_PACKAGE_NAME_2);
+    public void testAppendToPriorityList_activeDefaultApp_addsToTopOfList() {
+        Map<Integer, List<Long>> priorityList = new HashMap<>();
+        priorityList.put(
+                HealthDataCategory.BODY_MEASUREMENTS, List.of(APP_PACKAGE_ID, APP_PACKAGE_ID_2));
+        setupPriorityList(priorityList);
 
-        List<Long> newPriorityOrderId = new ArrayList<>();
-        newPriorityOrderId.add(APP_PACKAGE_ID_2);
+        HealthDataCategoryPriorityHelper spy = Mockito.spy(HealthDataCategoryPriorityHelper.class);
+        doReturn(true).when(spy).isDefaultApp(eq(APP_PACKAGE_NAME_4), any());
+        when(mAppInfoHelper.getOrInsertAppInfoId(any(), any())).thenReturn(APP_PACKAGE_ID_4);
+        spy.appendToPriorityList(
+                APP_PACKAGE_NAME_4, HealthDataCategory.BODY_MEASUREMENTS, mContext, false);
 
-        when(mCursor.getInt(eq(HEALTH_DATA_CATEGORY_COLUMN_INDEX)))
-                .thenReturn(HealthDataCategory.BODY_MEASUREMENTS);
-        when(mCursor.getString(eq(APP_ID_PRIORITY_ORDER_COLUMN_INDEX)))
-                .thenReturn(flattenLongList(List.of(APP_PACKAGE_ID, APP_PACKAGE_ID_2)));
-        when(mAppInfoHelper.getAppInfoIds(eq(newPriorityOrder))).thenReturn(newPriorityOrderId);
-
-        mHealthDataCategoryPriorityHelper.setPriorityOrder(
-                HealthDataCategory.BODY_MEASUREMENTS, newPriorityOrder);
-
-        verifyPriorityUpdate(List.of(APP_PACKAGE_ID_2, APP_PACKAGE_ID));
+        List<Long> expectedPriorityOrder =
+                List.of(APP_PACKAGE_ID_4, APP_PACKAGE_ID, APP_PACKAGE_ID_2);
+        verify(mTransactionManager)
+                .insertOrReplace(argThat(new UpsertRequestMatcher(expectedPriorityOrder)));
+        assertThat(spy.getAppIdPriorityOrder(HealthDataCategory.BODY_MEASUREMENTS))
+                .isEqualTo(expectedPriorityOrder);
     }
 
     @Test
-    public void testSetPriority_samePackagesDifferentOrder_newPrioritySaved() {
-        List<String> newPriorityOrder =
-                List.of(
-                        APP_PACKAGE_NAME_4,
-                        APP_PACKAGE_NAME_3,
-                        APP_PACKAGE_NAME_2,
-                        APP_PACKAGE_NAME);
+    public void testAppendToPriorityList_ifNonDefaultApp_addsToBottomOfList() {
+        Map<Integer, List<Long>> priorityList = new HashMap<>();
+        priorityList.put(
+                HealthDataCategory.BODY_MEASUREMENTS, List.of(APP_PACKAGE_ID, APP_PACKAGE_ID_2));
+        setupPriorityList(priorityList);
 
-        List<Long> newPriorityOrderId = new ArrayList<>();
-        newPriorityOrderId.add(APP_PACKAGE_ID_4);
-        newPriorityOrderId.add(APP_PACKAGE_ID_3);
-        newPriorityOrderId.add(APP_PACKAGE_ID_2);
-        newPriorityOrderId.add(APP_PACKAGE_ID);
+        HealthDataCategoryPriorityHelper spy = Mockito.spy(HealthDataCategoryPriorityHelper.class);
+        doReturn(false).when(spy).isDefaultApp(eq(APP_PACKAGE_NAME_4), any());
+        when(mAppInfoHelper.getOrInsertAppInfoId(any(), any())).thenReturn(APP_PACKAGE_ID_4);
+        spy.appendToPriorityList(
+                APP_PACKAGE_NAME_4, HealthDataCategory.BODY_MEASUREMENTS, mContext, false);
 
-        when(mCursor.getInt(eq(HEALTH_DATA_CATEGORY_COLUMN_INDEX)))
-                .thenReturn(HealthDataCategory.BODY_MEASUREMENTS);
-        when(mCursor.getString(eq(APP_ID_PRIORITY_ORDER_COLUMN_INDEX)))
+        List<Long> expectedPriorityOrder =
+                List.of(APP_PACKAGE_ID, APP_PACKAGE_ID_2, APP_PACKAGE_ID_4);
+        verify(mTransactionManager)
+                .insertOrReplace(argThat(new UpsertRequestMatcher(expectedPriorityOrder)));
+        assertThat(spy.getAppIdPriorityOrder(HealthDataCategory.BODY_MEASUREMENTS))
+                .isEqualTo(expectedPriorityOrder);
+    }
+
+    @Test
+    public void testAppendToPriorityList_inactiveDefaultApp_addsToBottomOfList() {
+        Map<Integer, List<Long>> priorityList = new HashMap<>();
+        priorityList.put(
+                HealthDataCategory.BODY_MEASUREMENTS, List.of(APP_PACKAGE_ID, APP_PACKAGE_ID_2));
+        setupPriorityList(priorityList);
+
+        HealthDataCategoryPriorityHelper spy = Mockito.spy(HealthDataCategoryPriorityHelper.class);
+        doReturn(true).when(spy).isDefaultApp(eq(APP_PACKAGE_NAME_4), any());
+        when(mAppInfoHelper.getOrInsertAppInfoId(any(), any())).thenReturn(APP_PACKAGE_ID_4);
+        spy.appendToPriorityList(
+                APP_PACKAGE_NAME_4, HealthDataCategory.BODY_MEASUREMENTS, mContext, true);
+
+        List<Long> expectedPriorityOrder =
+                List.of(APP_PACKAGE_ID, APP_PACKAGE_ID_2, APP_PACKAGE_ID_4);
+        verify(mTransactionManager)
+                .insertOrReplace(argThat(new UpsertRequestMatcher(expectedPriorityOrder)));
+        assertThat(spy.getAppIdPriorityOrder(HealthDataCategory.BODY_MEASUREMENTS))
+                .isEqualTo(expectedPriorityOrder);
+    }
+
+    @Test
+    public void testMaybeRemoveAppFromPriorityList_ifWritePermissionsForApp_doesNotRemoveApp() {
+        Map<Integer, List<Long>> priorityList = new HashMap<>();
+        priorityList.put(
+                HealthDataCategory.BODY_MEASUREMENTS, List.of(APP_PACKAGE_ID, APP_PACKAGE_ID_2));
+        priorityList.put(HealthDataCategory.ACTIVITY, List.of(APP_PACKAGE_ID_3, APP_PACKAGE_ID));
+        setupPriorityList(priorityList);
+
+        when(mHealthConnectPermissionHelper.getGrantedHealthPermissions(
+                        eq(APP_PACKAGE_NAME), any()))
                 .thenReturn(
-                        flattenLongList(
-                                List.of(
-                                        APP_PACKAGE_ID,
-                                        APP_PACKAGE_ID_2,
-                                        APP_PACKAGE_ID_3,
-                                        APP_PACKAGE_ID_4)));
+                        List.of(HealthPermissions.READ_DISTANCE, HealthPermissions.WRITE_STEPS));
+        HealthDataCategoryPriorityHelper spy = Mockito.spy(HealthDataCategoryPriorityHelper.class);
+        spy.maybeRemoveAppFromPriorityList(
+                APP_PACKAGE_NAME,
+                HealthDataCategory.ACTIVITY,
+                mHealthConnectPermissionHelper,
+                UserHandle.CURRENT);
+
+        verify(mTransactionManager, never()).insertOrReplace(any());
+    }
+
+    @Test
+    public void testNewMaybeRemoveAppFromPriorityList_ifDataForApp_doesNotRemoveApp() {
+        Map<Integer, List<Long>> priorityList = new HashMap<>();
+        priorityList.put(
+                HealthDataCategory.BODY_MEASUREMENTS, List.of(APP_PACKAGE_ID, APP_PACKAGE_ID_2));
+        priorityList.put(HealthDataCategory.ACTIVITY, List.of(APP_PACKAGE_ID_3, APP_PACKAGE_ID));
+        setupPriorityList(priorityList);
+
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(true);
+        when(mHealthConnectPermissionHelper.getGrantedHealthPermissions(
+                        eq(APP_PACKAGE_NAME), any()))
+                .thenReturn(List.of(HealthPermissions.READ_DISTANCE));
+        HealthDataCategoryPriorityHelper spy = Mockito.spy(HealthDataCategoryPriorityHelper.class);
+        doReturn(true)
+                .when(spy)
+                .appHasDataInCategory(APP_PACKAGE_NAME, HealthDataCategory.ACTIVITY);
+        spy.maybeRemoveAppFromPriorityList(
+                APP_PACKAGE_NAME,
+                HealthDataCategory.ACTIVITY,
+                mHealthConnectPermissionHelper,
+                UserHandle.CURRENT);
+
+        verify(mTransactionManager, never()).insertOrReplace(any());
+    }
+
+    @Test
+    public void testNewMaybeRemoveAppFromPriorityList_ifNoDataForApp_removesApp() {
+        Map<Integer, List<Long>> priorityList = new HashMap<>();
+        priorityList.put(
+                HealthDataCategory.BODY_MEASUREMENTS, List.of(APP_PACKAGE_ID, APP_PACKAGE_ID_2));
+        priorityList.put(HealthDataCategory.ACTIVITY, List.of(APP_PACKAGE_ID_3, APP_PACKAGE_ID));
+        setupPriorityList(priorityList);
+
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(true);
+        when(mHealthConnectPermissionHelper.getGrantedHealthPermissions(
+                        eq(APP_PACKAGE_NAME), any()))
+                .thenReturn(List.of(HealthPermissions.READ_DISTANCE));
+        when(mAppInfoHelper.getAppInfoId(APP_PACKAGE_NAME)).thenReturn(APP_PACKAGE_ID);
+        HealthDataCategoryPriorityHelper spy = Mockito.spy(HealthDataCategoryPriorityHelper.class);
+        doReturn(false)
+                .when(spy)
+                .appHasDataInCategory(APP_PACKAGE_NAME, HealthDataCategory.ACTIVITY);
+        spy.maybeRemoveAppFromPriorityList(
+                APP_PACKAGE_NAME,
+                HealthDataCategory.ACTIVITY,
+                mHealthConnectPermissionHelper,
+                UserHandle.CURRENT);
+
+        List<Long> expectedPriorityOrder = List.of(APP_PACKAGE_ID_3);
+        verify(mTransactionManager)
+                .insertOrReplace(argThat(new UpsertRequestMatcher(expectedPriorityOrder)));
+        assertThat(spy.getAppIdPriorityOrder(HealthDataCategory.ACTIVITY))
+                .isEqualTo(expectedPriorityOrder);
+    }
+
+    @Test
+    public void testOldMaybeRemoveAppFromPriorityList_ifNoWritePermissionsForApp_removesApp() {
+        Map<Integer, List<Long>> priorityList = new HashMap<>();
+        priorityList.put(HealthDataCategory.ACTIVITY, List.of(APP_PACKAGE_ID_3, APP_PACKAGE_ID));
+        setupPriorityList(priorityList);
+
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(false);
+        when(mHealthConnectPermissionHelper.getGrantedHealthPermissions(
+                        eq(APP_PACKAGE_NAME), any()))
+                .thenReturn(List.of(HealthPermissions.READ_DISTANCE));
+        when(mAppInfoHelper.getAppInfoId(APP_PACKAGE_NAME)).thenReturn(APP_PACKAGE_ID);
+        HealthDataCategoryPriorityHelper spy = Mockito.spy(HealthDataCategoryPriorityHelper.class);
+        doReturn(true)
+                .when(spy)
+                .appHasDataInCategory(APP_PACKAGE_NAME, HealthDataCategory.ACTIVITY);
+        spy.maybeRemoveAppFromPriorityList(
+                APP_PACKAGE_NAME,
+                HealthDataCategory.ACTIVITY,
+                mHealthConnectPermissionHelper,
+                UserHandle.CURRENT);
+
+        List<Long> expectedPriorityOrder = List.of(APP_PACKAGE_ID_3);
+        verify(mTransactionManager)
+                .insertOrReplace(argThat(new UpsertRequestMatcher(expectedPriorityOrder)));
+        assertThat(spy.getAppIdPriorityOrder(HealthDataCategory.ACTIVITY))
+                .isEqualTo(expectedPriorityOrder);
+    }
+
+    @Test
+    public void testUpdateHealthDataPriority_ifWritePermissionsForApp_doesNotRemoveApp() {
+        Map<Integer, List<Long>> priorityList = new HashMap<>();
+        priorityList.put(HealthDataCategory.ACTIVITY, List.of(APP_PACKAGE_ID_3, APP_PACKAGE_ID));
+        setupPriorityList(priorityList);
+
+        when(mPackageInfoUtils.getPackageInfoWithPermissionsAsUser(any(), any(), any()))
+                .thenReturn(mPackageInfo1);
+        mPackageInfo1.packageName = APP_PACKAGE_NAME;
+        mPackageInfo1.requestedPermissions =
+                new String[] {
+                    HealthPermissions.WRITE_SLEEP,
+                    HealthPermissions.WRITE_STEPS,
+                    HealthPermissions.READ_HEART_RATE,
+                    HealthPermissions.WRITE_OVULATION_TEST
+                };
+        when(HealthConnectManager.isHealthPermission(any(), any())).thenReturn(true);
+        mPackageInfo1.requestedPermissionsFlags =
+                new int[] {0, PackageInfo.REQUESTED_PERMISSION_GRANTED, 0, 0};
+        mHealthDataCategoryPriorityHelper.updateHealthDataPriority(
+                new String[] {APP_PACKAGE_NAME}, UserHandle.CURRENT, mContext);
+
+        List<Long> expectedPriorityOrder = List.of(APP_PACKAGE_ID_3, APP_PACKAGE_ID);
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.getAppIdPriorityOrder(
+                                HealthDataCategory.ACTIVITY))
+                .isEqualTo(expectedPriorityOrder);
+    }
+
+    @Test
+    public void testNewUpdateHealthDataPriority_ifDataForApp_doesNotRemoveApp() {
+        Map<Integer, List<Long>> priorityList = new HashMap<>();
+        priorityList.put(HealthDataCategory.ACTIVITY, List.of(APP_PACKAGE_ID_3, APP_PACKAGE_ID));
+        setupPriorityList(priorityList);
+
+        when(mPackageInfoUtils.getPackageInfoWithPermissionsAsUser(any(), any(), any()))
+                .thenReturn(mPackageInfo1);
+        mPackageInfo1.packageName = APP_PACKAGE_NAME;
+        mPackageInfo1.requestedPermissions =
+                new String[] {
+                    HealthPermissions.WRITE_SLEEP,
+                    HealthPermissions.WRITE_STEPS,
+                    HealthPermissions.READ_HEART_RATE,
+                    HealthPermissions.WRITE_OVULATION_TEST
+                };
+        mPackageInfo1.requestedPermissionsFlags = new int[] {0, 0, 0, 0};
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(true);
+        HealthDataCategoryPriorityHelper spy = Mockito.spy(HealthDataCategoryPriorityHelper.class);
+        doReturn(true)
+                .when(spy)
+                .appHasDataInCategory(APP_PACKAGE_NAME, HealthDataCategory.ACTIVITY);
+        spy.updateHealthDataPriority(new String[] {APP_PACKAGE_NAME}, UserHandle.CURRENT, mContext);
+
+        verify(mTransactionManager, never()).insertOrReplace(any());
+    }
+
+    @Test
+    public void testNewUpdateHealthDataPriority_ifNoDataForApp_removesApp() {
+        Map<Integer, List<Long>> priorityList = new HashMap<>();
+        priorityList.put(HealthDataCategory.ACTIVITY, List.of(APP_PACKAGE_ID_3, APP_PACKAGE_ID));
+        setupPriorityList(priorityList);
+
+        when(mPackageInfoUtils.getPackageInfoWithPermissionsAsUser(any(), any(), any()))
+                .thenReturn(mPackageInfo1);
+        mPackageInfo1.packageName = APP_PACKAGE_NAME;
+        mPackageInfo1.requestedPermissions =
+                new String[] {
+                    HealthPermissions.WRITE_SLEEP,
+                    HealthPermissions.WRITE_STEPS,
+                    HealthPermissions.READ_HEART_RATE,
+                    HealthPermissions.WRITE_OVULATION_TEST
+                };
+        mPackageInfo1.requestedPermissionsFlags = new int[] {0, 0, 0, 0};
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(true);
+        when(mAppInfoHelper.getAppInfoId(APP_PACKAGE_NAME)).thenReturn(APP_PACKAGE_ID);
+        HealthDataCategoryPriorityHelper spy = Mockito.spy(HealthDataCategoryPriorityHelper.class);
+        doReturn(false)
+                .when(spy)
+                .appHasDataInCategory(APP_PACKAGE_NAME, HealthDataCategory.ACTIVITY);
+
+        spy.updateHealthDataPriority(new String[] {APP_PACKAGE_NAME}, UserHandle.CURRENT, mContext);
+        List<Long> expectedPriorityOrder = List.of(APP_PACKAGE_ID_3);
+        verify(mTransactionManager)
+                .insertOrReplace(argThat(new UpsertRequestMatcher(expectedPriorityOrder)));
+        assertThat(spy.getAppIdPriorityOrder(HealthDataCategory.ACTIVITY))
+                .isEqualTo(expectedPriorityOrder);
+    }
+
+    @Test
+    public void testOldUpdateHealthDataPriority_ifNoWritePermissionsForApp_removesApp() {
+        Map<Integer, List<Long>> priorityList = new HashMap<>();
+        priorityList.put(HealthDataCategory.ACTIVITY, List.of(APP_PACKAGE_ID_3, APP_PACKAGE_ID));
+        setupPriorityList(priorityList);
+
+        when(mPackageInfoUtils.getPackageInfoWithPermissionsAsUser(any(), any(), any()))
+                .thenReturn(mPackageInfo1);
+        mPackageInfo1.packageName = APP_PACKAGE_NAME;
+        mPackageInfo1.requestedPermissions =
+                new String[] {
+                    HealthPermissions.WRITE_SLEEP,
+                    HealthPermissions.WRITE_STEPS,
+                    HealthPermissions.READ_HEART_RATE,
+                    HealthPermissions.WRITE_OVULATION_TEST
+                };
+        mPackageInfo1.requestedPermissionsFlags = new int[] {0, 0, 0, 0};
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(false);
+        when(mAppInfoHelper.getAppInfoId(APP_PACKAGE_NAME)).thenReturn(APP_PACKAGE_ID);
+        HealthDataCategoryPriorityHelper spy = Mockito.spy(HealthDataCategoryPriorityHelper.class);
+        doReturn(true)
+                .when(spy)
+                .appHasDataInCategory(APP_PACKAGE_NAME, HealthDataCategory.ACTIVITY);
+
+        spy.updateHealthDataPriority(new String[] {APP_PACKAGE_NAME}, UserHandle.CURRENT, mContext);
+
+        List<Long> expectedPriorityOrder = List.of(APP_PACKAGE_ID_3);
+        verify(mTransactionManager)
+                .insertOrReplace(argThat(new UpsertRequestMatcher(expectedPriorityOrder)));
+        assertThat(spy.getAppIdPriorityOrder(HealthDataCategory.ACTIVITY))
+                .isEqualTo(expectedPriorityOrder);
+    }
+
+    @Test
+    public void testOldMaybeRemoveAppWithoutWritePermissionsFromPriorityList_removesApp() {
+        Map<Integer, List<Long>> priorityList = new HashMap<>();
+        priorityList.put(HealthDataCategory.ACTIVITY, List.of(APP_PACKAGE_ID_3, APP_PACKAGE_ID));
+        setupPriorityList(priorityList);
+
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(false);
+        when(mAppInfoHelper.getAppInfoId(APP_PACKAGE_NAME)).thenReturn(APP_PACKAGE_ID);
+        HealthDataCategoryPriorityHelper spy = Mockito.spy(HealthDataCategoryPriorityHelper.class);
+        spy.maybeRemoveAppWithoutWritePermissionsFromPriorityList(APP_PACKAGE_NAME);
+
+        List<Long> expectedPriorityOrder = List.of(APP_PACKAGE_ID_3);
+        verify(mTransactionManager)
+                .insertOrReplace(argThat(new UpsertRequestMatcher(expectedPriorityOrder)));
+        assertThat(spy.getAppIdPriorityOrder(HealthDataCategory.ACTIVITY))
+                .isEqualTo(expectedPriorityOrder);
+    }
+
+    @Test
+    public void
+            testNewMaybeRemoveAppWithoutWritePermissionsFromPriorityList_ifNoDataForApp_removesApp() {
+        Map<Integer, List<Long>> priorityList = new HashMap<>();
+        priorityList.put(HealthDataCategory.ACTIVITY, List.of(APP_PACKAGE_ID_3, APP_PACKAGE_ID));
+        setupPriorityList(priorityList);
+
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(true);
+        when(mAppInfoHelper.getAppInfoId(APP_PACKAGE_NAME)).thenReturn(APP_PACKAGE_ID);
+        HealthDataCategoryPriorityHelper spy = Mockito.spy(HealthDataCategoryPriorityHelper.class);
+        doReturn(false)
+                .when(spy)
+                .appHasDataInCategory(APP_PACKAGE_NAME, HealthDataCategory.ACTIVITY);
+        spy.maybeRemoveAppWithoutWritePermissionsFromPriorityList(APP_PACKAGE_NAME);
+
+        List<Long> expectedPriorityOrder = List.of(APP_PACKAGE_ID_3);
+        verify(mTransactionManager)
+                .insertOrReplace(argThat(new UpsertRequestMatcher(expectedPriorityOrder)));
+        assertThat(spy.getAppIdPriorityOrder(HealthDataCategory.ACTIVITY))
+                .isEqualTo(expectedPriorityOrder);
+    }
+
+    @Test
+    public void
+            testNewMaybeRemoveAppWithoutWritePermissionsFromPriorityList_ifDataForApp_doesNotRemoveApp() {
+        Map<Integer, List<Long>> priorityList = new HashMap<>();
+        priorityList.put(HealthDataCategory.ACTIVITY, List.of(APP_PACKAGE_ID_3, APP_PACKAGE_ID));
+        setupPriorityList(priorityList);
+
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(true);
+        when(mAppInfoHelper.getAppInfoId(APP_PACKAGE_NAME)).thenReturn(APP_PACKAGE_ID);
+        HealthDataCategoryPriorityHelper spy = Mockito.spy(HealthDataCategoryPriorityHelper.class);
+        doReturn(true)
+                .when(spy)
+                .appHasDataInCategory(APP_PACKAGE_NAME, HealthDataCategory.ACTIVITY);
+        spy.maybeRemoveAppWithoutWritePermissionsFromPriorityList(APP_PACKAGE_NAME);
+
+        verify(mTransactionManager, never()).insertOrReplace(any());
+    }
+
+    @Test
+    public void testOldGetPriorityOrder_doesNotReSyncPriority() {
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(false);
+        when(mAppInfoHelper.getPackageNames(any()))
+                .thenReturn(List.of(APP_PACKAGE_NAME, APP_PACKAGE_NAME_2));
+        HealthDataCategoryPriorityHelper spy = Mockito.spy(HealthDataCategoryPriorityHelper.class);
+
+        spy.getPriorityOrder(HealthDataCategory.ACTIVITY, mContext);
+        verify(spy, never()).reSyncHealthDataPriorityTable(mContext);
+    }
+
+    @Test
+    public void testNewGetPriorityOrder_callsReSyncPriority() {
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(true);
+        when(mAppInfoHelper.getPackageNames(any()))
+                .thenReturn(List.of(APP_PACKAGE_NAME, APP_PACKAGE_NAME_2));
+        HealthDataCategoryPriorityHelper spy = Mockito.spy(HealthDataCategoryPriorityHelper.class);
+        doNothing().when(spy).reSyncHealthDataPriorityTable(any());
+
+        spy.getPriorityOrder(HealthDataCategory.ACTIVITY, mContext);
+        verify(spy, times(1)).reSyncHealthDataPriorityTable(mContext);
+    }
+
+    @Test
+    public void testGetPriorityOrder_returnsCorrectPriorityForCategory() {
+        Map<Integer, List<Long>> priorityList = new HashMap<>();
+        priorityList.put(HealthDataCategory.ACTIVITY, List.of(APP_PACKAGE_ID_3, APP_PACKAGE_ID));
+        setupPriorityList(priorityList);
+
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(false);
+        when(mAppInfoHelper.getPackageNames(any()))
+                .thenReturn(List.of(APP_PACKAGE_NAME_3, APP_PACKAGE_NAME));
+
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.getPriorityOrder(
+                                HealthDataCategory.ACTIVITY, mContext))
+                .isEqualTo(List.of(APP_PACKAGE_NAME_3, APP_PACKAGE_NAME));
+    }
+
+    @Test
+    public void testNewSetPriority_additionalPackages_addsToPriorityList() {
+        List<String> newPriorityOrder =
+                List.of(
+                        APP_PACKAGE_NAME_4,
+                        APP_PACKAGE_NAME_3,
+                        APP_PACKAGE_NAME_2,
+                        APP_PACKAGE_NAME);
+
+        List<Long> newPriorityOrderId = new ArrayList<>();
+        newPriorityOrderId.add(APP_PACKAGE_ID_4);
+        newPriorityOrderId.add(APP_PACKAGE_ID_3);
+        newPriorityOrderId.add(APP_PACKAGE_ID_2);
+        newPriorityOrderId.add(APP_PACKAGE_ID);
+
+        Map<Integer, List<Long>> priorityList = new HashMap<>();
+        priorityList.put(
+                HealthDataCategory.BODY_MEASUREMENTS, List.of(APP_PACKAGE_ID, APP_PACKAGE_ID_2));
+        setupPriorityList(priorityList);
+
         when(mAppInfoHelper.getAppInfoIds(eq(newPriorityOrder))).thenReturn(newPriorityOrderId);
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(true);
+        mHealthDataCategoryPriorityHelper.setPriorityOrder(
+                HealthDataCategory.BODY_MEASUREMENTS, newPriorityOrder);
+
+        verifyPriorityUpdate(
+                List.of(APP_PACKAGE_ID_4, APP_PACKAGE_ID_3, APP_PACKAGE_ID_2, APP_PACKAGE_ID),
+                HealthDataCategory.BODY_MEASUREMENTS);
+    }
+
+    @Test
+    public void testNewSetPriority_fewerPackages_removesFromPriorityList() {
+        List<String> newPriorityOrder = List.of(APP_PACKAGE_NAME_2, APP_PACKAGE_NAME);
+
+        List<Long> newPriorityOrderId = new ArrayList<>();
+        newPriorityOrderId.add(APP_PACKAGE_ID_2);
+        newPriorityOrderId.add(APP_PACKAGE_ID);
+        Map<Integer, List<Long>> priorityList = new HashMap<>();
+        priorityList.put(
+                HealthDataCategory.BODY_MEASUREMENTS,
+                List.of(APP_PACKAGE_ID, APP_PACKAGE_ID_2, APP_PACKAGE_ID_3, APP_PACKAGE_ID_4));
+        setupPriorityList(priorityList);
+
+        when(mAppInfoHelper.getAppInfoIds(eq(newPriorityOrder))).thenReturn(newPriorityOrderId);
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(true);
 
         mHealthDataCategoryPriorityHelper.setPriorityOrder(
                 HealthDataCategory.BODY_MEASUREMENTS, newPriorityOrder);
 
         verifyPriorityUpdate(
-                List.of(APP_PACKAGE_ID_4, APP_PACKAGE_ID_3, APP_PACKAGE_ID_2, APP_PACKAGE_ID));
+                List.of(APP_PACKAGE_ID_2, APP_PACKAGE_ID), HealthDataCategory.BODY_MEASUREMENTS);
     }
 
-    private void verifyPriorityUpdate(List<Long> priorityOrder) {
-        verify(mTransactionManager).insertOrReplace(argThat(new RequestMatcher(priorityOrder)));
+    @Test
+    public void testNewSetPriority_samePackages_reordersPriorityList() {
+        List<String> newPriorityOrder =
+                List.of(
+                        APP_PACKAGE_NAME_3,
+                        APP_PACKAGE_NAME_2,
+                        APP_PACKAGE_NAME,
+                        APP_PACKAGE_NAME_4);
+
+        List<Long> newPriorityOrderId = new ArrayList<>();
+        newPriorityOrderId.add(APP_PACKAGE_ID_3);
+        newPriorityOrderId.add(APP_PACKAGE_ID_2);
+        newPriorityOrderId.add(APP_PACKAGE_ID);
+        newPriorityOrderId.add(APP_PACKAGE_ID_4);
+
+        Map<Integer, List<Long>> priorityList = new HashMap<>();
+        priorityList.put(
+                HealthDataCategory.BODY_MEASUREMENTS,
+                List.of(APP_PACKAGE_ID, APP_PACKAGE_ID_2, APP_PACKAGE_ID_3, APP_PACKAGE_ID_4));
+        setupPriorityList(priorityList);
+
+        when(mAppInfoHelper.getAppInfoIds(eq(newPriorityOrder))).thenReturn(newPriorityOrderId);
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(true);
+        mHealthDataCategoryPriorityHelper.setPriorityOrder(
+                HealthDataCategory.BODY_MEASUREMENTS, newPriorityOrder);
+
+        verifyPriorityUpdate(
+                List.of(APP_PACKAGE_ID_3, APP_PACKAGE_ID_2, APP_PACKAGE_ID, APP_PACKAGE_ID_4),
+                HealthDataCategory.BODY_MEASUREMENTS);
+    }
+
+    @Test
+    public void testOldSetPriority_additionalPackagesDifferentOrder_newPackagesRemoved() {
+        List<String> newPriorityOrder =
+                List.of(
+                        APP_PACKAGE_NAME_4,
+                        APP_PACKAGE_NAME_3,
+                        APP_PACKAGE_NAME_2,
+                        APP_PACKAGE_NAME);
+
+        List<Long> newPriorityOrderId = new ArrayList<>();
+        newPriorityOrderId.add(APP_PACKAGE_ID_4);
+        newPriorityOrderId.add(APP_PACKAGE_ID_3);
+        newPriorityOrderId.add(APP_PACKAGE_ID_2);
+        newPriorityOrderId.add(APP_PACKAGE_ID);
+
+        Map<Integer, List<Long>> priorityList = new HashMap<>();
+        priorityList.put(
+                HealthDataCategory.BODY_MEASUREMENTS, List.of(APP_PACKAGE_ID, APP_PACKAGE_ID_2));
+        setupPriorityList(priorityList);
+
+        when(mAppInfoHelper.getAppInfoIds(eq(newPriorityOrder))).thenReturn(newPriorityOrderId);
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(false);
+        mHealthDataCategoryPriorityHelper.setPriorityOrder(
+                HealthDataCategory.BODY_MEASUREMENTS, newPriorityOrder);
+
+        verifyPriorityUpdate(
+                List.of(APP_PACKAGE_ID_2, APP_PACKAGE_ID), HealthDataCategory.BODY_MEASUREMENTS);
+    }
+
+    @Test
+    public void testOldSetPriority_reducedPackagesDifferentOrder_oldPackagesAdded() {
+        List<String> newPriorityOrder = List.of(APP_PACKAGE_NAME_2);
+
+        List<Long> newPriorityOrderId = new ArrayList<>();
+        newPriorityOrderId.add(APP_PACKAGE_ID_2);
+
+        Map<Integer, List<Long>> priorityList = new HashMap<>();
+        priorityList.put(
+                HealthDataCategory.BODY_MEASUREMENTS, List.of(APP_PACKAGE_ID, APP_PACKAGE_ID_2));
+        setupPriorityList(priorityList);
+
+        when(mAppInfoHelper.getAppInfoIds(eq(newPriorityOrder))).thenReturn(newPriorityOrderId);
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(false);
+        mHealthDataCategoryPriorityHelper.setPriorityOrder(
+                HealthDataCategory.BODY_MEASUREMENTS, newPriorityOrder);
+
+        verifyPriorityUpdate(
+                List.of(APP_PACKAGE_ID_2, APP_PACKAGE_ID), HealthDataCategory.BODY_MEASUREMENTS);
+    }
+
+    @Test
+    public void testOldSetPriority_samePackagesDifferentOrder_newPrioritySaved() {
+        List<String> newPriorityOrder =
+                List.of(
+                        APP_PACKAGE_NAME_4,
+                        APP_PACKAGE_NAME_3,
+                        APP_PACKAGE_NAME_2,
+                        APP_PACKAGE_NAME);
+
+        List<Long> newPriorityOrderId = new ArrayList<>();
+        newPriorityOrderId.add(APP_PACKAGE_ID_4);
+        newPriorityOrderId.add(APP_PACKAGE_ID_3);
+        newPriorityOrderId.add(APP_PACKAGE_ID_2);
+        newPriorityOrderId.add(APP_PACKAGE_ID);
+
+        Map<Integer, List<Long>> priorityList = new HashMap<>();
+        priorityList.put(
+                HealthDataCategory.BODY_MEASUREMENTS,
+                List.of(APP_PACKAGE_ID, APP_PACKAGE_ID_2, APP_PACKAGE_ID_3, APP_PACKAGE_ID_4));
+        setupPriorityList(priorityList);
+
+        when(mAppInfoHelper.getAppInfoIds(eq(newPriorityOrder))).thenReturn(newPriorityOrderId);
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(false);
+        mHealthDataCategoryPriorityHelper.setPriorityOrder(
+                HealthDataCategory.BODY_MEASUREMENTS, newPriorityOrder);
+
+        verifyPriorityUpdate(
+                List.of(APP_PACKAGE_ID_4, APP_PACKAGE_ID_3, APP_PACKAGE_ID_2, APP_PACKAGE_ID),
+                HealthDataCategory.BODY_MEASUREMENTS);
+    }
+
+    @Test
+    public void testOldReSyncHealthDataPriorityTable_addsNewApps_withWritePermission() {
+        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));
+        priorityList.put(
+                HealthDataCategory.BODY_MEASUREMENTS,
+                List.of(APP_PACKAGE_ID, APP_PACKAGE_ID_2, APP_PACKAGE_ID_3, APP_PACKAGE_ID_4));
+        priorityList.put(HealthDataCategory.SLEEP, List.of(APP_PACKAGE_ID_4, 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));
+        recordTypesToContributorPackages.put(
+                RecordTypeIdentifier.RECORD_TYPE_DISTANCE, Set.of(APP_PACKAGE_NAME_3));
+        recordTypesToContributorPackages.put(
+                RecordTypeIdentifier.RECORD_TYPE_NUTRITION, Set.of(APP_PACKAGE_NAME_2));
+        recordTypesToContributorPackages.put(
+                RecordTypeIdentifier.RECORD_TYPE_SLEEP_SESSION,
+                Set.of(APP_PACKAGE_NAME, APP_PACKAGE_NAME_3, APP_PACKAGE_NAME_2));
+        when(mAppInfoHelper.getRecordTypesToContributingPackagesMap())
+                .thenReturn(recordTypesToContributorPackages);
+
+        // Setup apps with write permissions
+        mPackageInfo1.packageName = APP_PACKAGE_NAME;
+        mPackageInfo1.requestedPermissions =
+                new String[] {
+                    HealthPermissions.WRITE_SLEEP,
+                    HealthPermissions.WRITE_STEPS,
+                    HealthPermissions.READ_HEART_RATE,
+                    HealthPermissions.WRITE_OVULATION_TEST
+                };
+        mPackageInfo1.requestedPermissionsFlags =
+                new int[] {
+                    0,
+                    0,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED
+                };
+
+        mPackageInfo2.packageName = APP_PACKAGE_NAME_2;
+        mPackageInfo2.requestedPermissions =
+                new String[] {
+                    HealthPermissions.WRITE_SLEEP,
+                    HealthPermissions.WRITE_NUTRITION,
+                    HealthPermissions.WRITE_HEART_RATE,
+                    HealthPermissions.READ_BLOOD_GLUCOSE
+                };
+        mPackageInfo2.requestedPermissionsFlags =
+                new int[] {
+                    0,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED
+                };
+        when(HealthConnectManager.isHealthPermission(any(), any())).thenReturn(true);
+        when(mPackageInfoUtils.getPackagesHoldingHealthPermissions(any(), any()))
+                .thenReturn(List.of(mPackageInfo1, mPackageInfo2));
+
+        mHealthDataCategoryPriorityHelper.reSyncHealthDataPriorityTable(mContext);
 
         assertThat(
                         mHealthDataCategoryPriorityHelper.getAppIdPriorityOrder(
+                                HealthDataCategory.CYCLE_TRACKING))
+                .isEqualTo(List.of(APP_PACKAGE_ID));
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.getAppIdPriorityOrder(
+                                HealthDataCategory.NUTRITION))
+                .isEqualTo(List.of(APP_PACKAGE_ID_2));
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.getAppIdPriorityOrder(
+                                HealthDataCategory.VITALS))
+                .isEqualTo(List.of(APP_PACKAGE_ID_2));
+    }
+
+    @Test
+    public void testOldReSyncHealthDataPriorityTable_removesApps_withoutWritePermission() {
+        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));
+        priorityList.put(
+                HealthDataCategory.BODY_MEASUREMENTS,
+                List.of(APP_PACKAGE_ID, APP_PACKAGE_ID_2, APP_PACKAGE_ID_3, APP_PACKAGE_ID_4));
+        priorityList.put(HealthDataCategory.SLEEP, List.of(APP_PACKAGE_ID_4, 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));
+        recordTypesToContributorPackages.put(
+                RecordTypeIdentifier.RECORD_TYPE_DISTANCE, Set.of(APP_PACKAGE_NAME_3));
+        recordTypesToContributorPackages.put(
+                RecordTypeIdentifier.RECORD_TYPE_NUTRITION, Set.of(APP_PACKAGE_NAME_2));
+        recordTypesToContributorPackages.put(
+                RecordTypeIdentifier.RECORD_TYPE_SLEEP_SESSION,
+                Set.of(APP_PACKAGE_NAME, APP_PACKAGE_NAME_3, APP_PACKAGE_NAME_2));
+        when(mAppInfoHelper.getRecordTypesToContributingPackagesMap())
+                .thenReturn(recordTypesToContributorPackages);
+
+        // Setup apps with write permissions
+        mPackageInfo1.packageName = APP_PACKAGE_NAME;
+        mPackageInfo1.requestedPermissions =
+                new String[] {
+                    HealthPermissions.WRITE_SLEEP,
+                    HealthPermissions.WRITE_STEPS,
+                    HealthPermissions.READ_HEART_RATE,
+                    HealthPermissions.WRITE_OVULATION_TEST
+                };
+        mPackageInfo1.requestedPermissionsFlags =
+                new int[] {
+                    0,
+                    0,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED
+                };
+
+        mPackageInfo2.packageName = APP_PACKAGE_NAME_2;
+        mPackageInfo2.requestedPermissions =
+                new String[] {
+                    HealthPermissions.WRITE_SLEEP,
+                    HealthPermissions.WRITE_NUTRITION,
+                    HealthPermissions.WRITE_HEART_RATE,
+                    HealthPermissions.READ_BLOOD_GLUCOSE
+                };
+        mPackageInfo2.requestedPermissionsFlags =
+                new int[] {
+                    0,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED
+                };
+        when(HealthConnectManager.isHealthPermission(any(), any())).thenReturn(true);
+        when(mPackageInfoUtils.getPackagesHoldingHealthPermissions(any(), any()))
+                .thenReturn(List.of(mPackageInfo1, mPackageInfo2));
+
+        mHealthDataCategoryPriorityHelper.reSyncHealthDataPriorityTable(mContext);
+        verify(mTransactionManager)
+                .delete(argThat(new DeleteRequestMatcher(HealthDataCategory.ACTIVITY)));
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.getAppIdPriorityOrder(
+                                HealthDataCategory.ACTIVITY))
+                .isEqualTo(List.of());
+
+        verify(mTransactionManager)
+                .delete(argThat(new DeleteRequestMatcher(HealthDataCategory.BODY_MEASUREMENTS)));
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.getAppIdPriorityOrder(
                                 HealthDataCategory.BODY_MEASUREMENTS))
+                .isEqualTo(List.of());
+
+        verify(mTransactionManager)
+                .delete(argThat(new DeleteRequestMatcher(HealthDataCategory.SLEEP)));
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.getAppIdPriorityOrder(
+                                HealthDataCategory.SLEEP))
+                .isEqualTo(List.of());
+    }
+
+    @Test
+    public void testNewReSyncHealthDataPriorityTable_newWritePermission_doesNotUpdateTable() {
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(true);
+        // Setup current priority list
+        Map<Integer, List<Long>> priorityList = new HashMap<>();
+        priorityList.put(HealthDataCategory.ACTIVITY, List.of(APP_PACKAGE_ID));
+        priorityList.put(
+                HealthDataCategory.BODY_MEASUREMENTS,
+                List.of(APP_PACKAGE_ID, APP_PACKAGE_ID_2, APP_PACKAGE_ID_3, APP_PACKAGE_ID_4));
+        priorityList.put(HealthDataCategory.SLEEP, List.of(APP_PACKAGE_ID_4, 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_2));
+        recordTypesToContributorPackages.put(
+                RecordTypeIdentifier.RECORD_TYPE_DISTANCE, Set.of(APP_PACKAGE_NAME_3));
+        recordTypesToContributorPackages.put(
+                RecordTypeIdentifier.RECORD_TYPE_NUTRITION, Set.of(APP_PACKAGE_NAME_2));
+        recordTypesToContributorPackages.put(
+                RecordTypeIdentifier.RECORD_TYPE_SLEEP_SESSION,
+                Set.of(APP_PACKAGE_NAME, APP_PACKAGE_NAME_3, APP_PACKAGE_NAME_2));
+        when(mAppInfoHelper.getRecordTypesToContributingPackagesMap())
+                .thenReturn(recordTypesToContributorPackages);
+
+        // Setup apps with write permissions
+        mPackageInfo1.packageName = APP_PACKAGE_NAME;
+        mPackageInfo1.requestedPermissions =
+                new String[] {
+                    HealthPermissions.WRITE_SLEEP,
+                    HealthPermissions.WRITE_STEPS,
+                    HealthPermissions.READ_HEART_RATE,
+                    HealthPermissions.WRITE_OVULATION_TEST
+                };
+        mPackageInfo1.requestedPermissionsFlags =
+                new int[] {
+                    0,
+                    0,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED
+                };
+
+        mPackageInfo2.packageName = APP_PACKAGE_NAME_2;
+        mPackageInfo2.requestedPermissions =
+                new String[] {
+                    HealthPermissions.WRITE_SLEEP,
+                    HealthPermissions.WRITE_NUTRITION,
+                    HealthPermissions.WRITE_HEART_RATE,
+                    HealthPermissions.READ_BLOOD_GLUCOSE
+                };
+        mPackageInfo2.requestedPermissionsFlags =
+                new int[] {
+                    0,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED
+                };
+        when(HealthConnectManager.isHealthPermission(any(), any())).thenReturn(true);
+        when(mPackageInfoUtils.getPackagesHoldingHealthPermissions(any(), any()))
+                .thenReturn(List.of(mPackageInfo1, mPackageInfo2));
+
+        mHealthDataCategoryPriorityHelper.reSyncHealthDataPriorityTable(mContext);
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.getAppIdPriorityOrder(
+                                HealthDataCategory.VITALS))
+                .isEqualTo(List.of());
+    }
+
+    @Test
+    public void testNewReSyncHealthDataPriorityTable_ifNoData_removesApps() {
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(true);
+        // Setup current priority list
+        Map<Integer, List<Long>> priorityList = new HashMap<>();
+        priorityList.put(HealthDataCategory.ACTIVITY, List.of(APP_PACKAGE_ID));
+        priorityList.put(
+                HealthDataCategory.BODY_MEASUREMENTS,
+                List.of(APP_PACKAGE_ID, APP_PACKAGE_ID_2, APP_PACKAGE_ID_3, APP_PACKAGE_ID_4));
+        priorityList.put(HealthDataCategory.SLEEP, List.of(APP_PACKAGE_ID_4, 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_2));
+        recordTypesToContributorPackages.put(
+                RecordTypeIdentifier.RECORD_TYPE_DISTANCE, Set.of(APP_PACKAGE_NAME_3));
+        recordTypesToContributorPackages.put(
+                RecordTypeIdentifier.RECORD_TYPE_NUTRITION, Set.of(APP_PACKAGE_NAME_2));
+        recordTypesToContributorPackages.put(
+                RecordTypeIdentifier.RECORD_TYPE_SLEEP_SESSION,
+                Set.of(APP_PACKAGE_NAME, APP_PACKAGE_NAME_3, APP_PACKAGE_NAME_2));
+        when(mAppInfoHelper.getRecordTypesToContributingPackagesMap())
+                .thenReturn(recordTypesToContributorPackages);
+
+        // Setup apps with write permissions
+        mPackageInfo1.packageName = APP_PACKAGE_NAME;
+        mPackageInfo1.requestedPermissions =
+                new String[] {
+                    HealthPermissions.WRITE_SLEEP,
+                    HealthPermissions.WRITE_STEPS,
+                    HealthPermissions.READ_HEART_RATE,
+                    HealthPermissions.WRITE_OVULATION_TEST
+                };
+        mPackageInfo1.requestedPermissionsFlags =
+                new int[] {
+                    0,
+                    0,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED
+                };
+
+        mPackageInfo2.packageName = APP_PACKAGE_NAME_2;
+        mPackageInfo2.requestedPermissions =
+                new String[] {
+                    HealthPermissions.WRITE_SLEEP,
+                    HealthPermissions.WRITE_NUTRITION,
+                    HealthPermissions.WRITE_HEART_RATE,
+                    HealthPermissions.READ_BLOOD_GLUCOSE
+                };
+        mPackageInfo2.requestedPermissionsFlags =
+                new int[] {
+                    0,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED
+                };
+        when(HealthConnectManager.isHealthPermission(any(), any())).thenReturn(true);
+        when(mPackageInfoUtils.getPackagesHoldingHealthPermissions(any(), any()))
+                .thenReturn(List.of(mPackageInfo1, mPackageInfo2));
+
+        mHealthDataCategoryPriorityHelper.reSyncHealthDataPriorityTable(mContext);
+        verify(mTransactionManager)
+                .delete(argThat(new DeleteRequestMatcher(HealthDataCategory.ACTIVITY)));
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.getAppIdPriorityOrder(
+                                HealthDataCategory.ACTIVITY))
+                .isEqualTo(List.of());
+
+        verify(mTransactionManager)
+                .delete(argThat(new DeleteRequestMatcher(HealthDataCategory.BODY_MEASUREMENTS)));
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.getAppIdPriorityOrder(
+                                HealthDataCategory.BODY_MEASUREMENTS))
+                .isEqualTo(List.of());
+
+        verifyPriorityUpdate(List.of(APP_PACKAGE_ID_2), HealthDataCategory.SLEEP);
+    }
+
+    @Test
+    public void testNewReSyncHealthDataPriorityTable_ifDataForApps_doesNotRemoveApps() {
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(true);
+        // Setup current priority list
+        Map<Integer, List<Long>> priorityList = new HashMap<>();
+        priorityList.put(HealthDataCategory.ACTIVITY, List.of(APP_PACKAGE_ID));
+        priorityList.put(
+                HealthDataCategory.BODY_MEASUREMENTS,
+                List.of(APP_PACKAGE_ID, APP_PACKAGE_ID_2, APP_PACKAGE_ID_3, APP_PACKAGE_ID_4));
+        priorityList.put(HealthDataCategory.SLEEP, List.of(APP_PACKAGE_ID_4, 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));
+        recordTypesToContributorPackages.put(
+                RecordTypeIdentifier.RECORD_TYPE_DISTANCE, Set.of(APP_PACKAGE_NAME_3));
+        recordTypesToContributorPackages.put(
+                RecordTypeIdentifier.RECORD_TYPE_NUTRITION, Set.of(APP_PACKAGE_NAME_2));
+        recordTypesToContributorPackages.put(
+                RecordTypeIdentifier.RECORD_TYPE_SLEEP_SESSION,
+                Set.of(APP_PACKAGE_NAME, APP_PACKAGE_NAME_3, APP_PACKAGE_NAME_2));
+        when(mAppInfoHelper.getRecordTypesToContributingPackagesMap())
+                .thenReturn(recordTypesToContributorPackages);
+
+        // Setup apps with write permissions
+        mPackageInfo1.packageName = APP_PACKAGE_NAME;
+        mPackageInfo1.requestedPermissions =
+                new String[] {
+                    HealthPermissions.WRITE_SLEEP,
+                    HealthPermissions.WRITE_STEPS,
+                    HealthPermissions.READ_HEART_RATE,
+                    HealthPermissions.WRITE_OVULATION_TEST
+                };
+        mPackageInfo1.requestedPermissionsFlags =
+                new int[] {
+                    0,
+                    0,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED
+                };
+
+        mPackageInfo2.packageName = APP_PACKAGE_NAME_2;
+        mPackageInfo2.requestedPermissions =
+                new String[] {
+                    HealthPermissions.WRITE_SLEEP,
+                    HealthPermissions.WRITE_NUTRITION,
+                    HealthPermissions.WRITE_HEART_RATE,
+                    HealthPermissions.READ_BLOOD_GLUCOSE
+                };
+        mPackageInfo2.requestedPermissionsFlags =
+                new int[] {
+                    0,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED
+                };
+        when(HealthConnectManager.isHealthPermission(any(), any())).thenReturn(true);
+        when(mPackageInfoUtils.getPackagesHoldingHealthPermissions(any(), any()))
+                .thenReturn(List.of(mPackageInfo1, mPackageInfo2));
+
+        mHealthDataCategoryPriorityHelper.reSyncHealthDataPriorityTable(mContext);
+        verify(mTransactionManager, never())
+                .insertOrReplace(argThat(new UpsertRequestMatcher(List.of(APP_PACKAGE_ID))));
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.getAppIdPriorityOrder(
+                                HealthDataCategory.ACTIVITY))
+                .isEqualTo(List.of(APP_PACKAGE_ID));
+
+        verify(mTransactionManager)
+                .delete(argThat(new DeleteRequestMatcher(HealthDataCategory.BODY_MEASUREMENTS)));
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.getAppIdPriorityOrder(
+                                HealthDataCategory.BODY_MEASUREMENTS))
+                .isEqualTo(List.of());
+
+        verifyPriorityUpdate(List.of(APP_PACKAGE_ID_2), HealthDataCategory.SLEEP);
+    }
+
+    @Test
+    public void testMaybeAddInactiveAppsToPriorityList_ifPreferenceNotSet_addsToList() {
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(true);
+        when(mPreferenceHelper.getPreference(any())).thenReturn(null);
+
+        // 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));
+        recordTypesToContributorPackages.put(
+                RecordTypeIdentifier.RECORD_TYPE_DISTANCE, Set.of(APP_PACKAGE_NAME_3));
+        recordTypesToContributorPackages.put(
+                RecordTypeIdentifier.RECORD_TYPE_NUTRITION, Set.of(APP_PACKAGE_NAME_2));
+        recordTypesToContributorPackages.put(
+                RecordTypeIdentifier.RECORD_TYPE_HEART_RATE,
+                Set.of(APP_PACKAGE_NAME, 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_SLEEP,
+                    HealthPermissions.WRITE_STEPS,
+                    HealthPermissions.READ_HEART_RATE,
+                    HealthPermissions.WRITE_OVULATION_TEST
+                };
+        mPackageInfo1.requestedPermissionsFlags =
+                new int[] {
+                    0,
+                    0,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED
+                };
+
+        mPackageInfo2.packageName = APP_PACKAGE_NAME_2;
+        mPackageInfo2.requestedPermissions =
+                new String[] {
+                    HealthPermissions.WRITE_SLEEP,
+                    HealthPermissions.WRITE_NUTRITION,
+                    HealthPermissions.WRITE_HEART_RATE,
+                    HealthPermissions.READ_BLOOD_GLUCOSE
+                };
+        mPackageInfo2.requestedPermissionsFlags =
+                new int[] {
+                    0,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED
+                };
+        when(HealthConnectManager.isHealthPermission(any(), any())).thenReturn(true);
+        when(mPackageInfoUtils.getPackagesHoldingHealthPermissions(any(), any()))
+                .thenReturn(List.of(mPackageInfo1, mPackageInfo2));
+        // Inactive apps {
+        //   Activity -> {APP_PACKAGE_NAME, APP_PACKAGE_NAME_2, APP_PACKAGE_NAME_3}
+        //   Vitals -> {APP_PACKAGE_NAME, APP_PACKAGE_NAME_3}
+
+        // Setup current priority list
+        Map<Integer, List<Long>> priorityList = new HashMap<>();
+        priorityList.put(
+                HealthDataCategory.BODY_MEASUREMENTS,
+                List.of(APP_PACKAGE_ID, APP_PACKAGE_ID_2, APP_PACKAGE_ID_3, APP_PACKAGE_ID_4));
+        priorityList.put(HealthDataCategory.ACTIVITY, List.of(APP_PACKAGE_ID_4));
+        priorityList.put(HealthDataCategory.SLEEP, List.of(APP_PACKAGE_ID_4));
+        setupPriorityList(priorityList);
+
+        mHealthDataCategoryPriorityHelper.maybeAddInactiveAppsToPriorityList(mContext);
+        verifyPriorityUpdate(
+                List.of(APP_PACKAGE_ID_4, APP_PACKAGE_ID, APP_PACKAGE_ID_2, APP_PACKAGE_ID_3),
+                HealthDataCategory.ACTIVITY);
+        verifyPriorityUpdate(List.of(APP_PACKAGE_ID, APP_PACKAGE_ID_3), HealthDataCategory.VITALS);
+    }
+
+    @Test
+    public void testMaybeAddInactiveAppsToPriorityList_ifPreferenceExists_doesNotAdd() {
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(true);
+        when(mPreferenceHelper.getPreference(any())).thenReturn(String.valueOf(true));
+        mHealthDataCategoryPriorityHelper.maybeAddInactiveAppsToPriorityList(mContext);
+        verify(mPreferenceHelper, never()).insertOrReplacePreference(any(), any());
+        verify(mTransactionManager, never()).insertOrReplace(any());
+    }
+
+    @Test
+    public void testMaybeAddInactiveAppsToPriorityList_ifOldAggregation_doesNotAdd() {
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(false);
+        mHealthDataCategoryPriorityHelper.maybeAddInactiveAppsToPriorityList(mContext);
+        verify(mPreferenceHelper, never()).getPreference(any());
+        verify(mPreferenceHelper, never()).insertOrReplacePreference(any(), any());
+        verify(mTransactionManager, never()).insertOrReplace(any());
+    }
+
+    @Test
+    public void testAppHasDataInCategory_forAppsWithDataInCategory_returnsTrue() {
+        Map<Integer, Set<String>> recordTypesToContributingPackagesMap = new HashMap<>();
+        recordTypesToContributingPackagesMap.put(
+                RecordTypeIdentifier.RECORD_TYPE_STEPS,
+                Set.of(APP_PACKAGE_NAME, APP_PACKAGE_NAME_2));
+        recordTypesToContributingPackagesMap.put(
+                RecordTypeIdentifier.RECORD_TYPE_DISTANCE, Set.of(APP_PACKAGE_NAME_3));
+        recordTypesToContributingPackagesMap.put(
+                RecordTypeIdentifier.RECORD_TYPE_SLEEP_SESSION, Set.of(APP_PACKAGE_NAME_3));
+        recordTypesToContributingPackagesMap.put(
+                RecordTypeIdentifier.RECORD_TYPE_BODY_FAT, Set.of(APP_PACKAGE_NAME_4));
+        when(mAppInfoHelper.getRecordTypesToContributingPackagesMap())
+                .thenReturn(recordTypesToContributingPackagesMap);
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.appHasDataInCategory(
+                                APP_PACKAGE_NAME, HealthDataCategory.ACTIVITY))
+                .isTrue();
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.appHasDataInCategory(
+                                APP_PACKAGE_NAME_2, HealthDataCategory.ACTIVITY))
+                .isTrue();
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.appHasDataInCategory(
+                                APP_PACKAGE_NAME_3, HealthDataCategory.ACTIVITY))
+                .isTrue();
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.appHasDataInCategory(
+                                APP_PACKAGE_NAME_3, HealthDataCategory.SLEEP))
+                .isTrue();
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.appHasDataInCategory(
+                                APP_PACKAGE_NAME_4, HealthDataCategory.BODY_MEASUREMENTS))
+                .isTrue();
+    }
+
+    @Test
+    public void testAppHasDataInCategory_forAppsWithoutDataInCategory_returnsFalse() {
+        Map<Integer, Set<String>> recordTypesToContributingPackagesMap = new HashMap<>();
+        recordTypesToContributingPackagesMap.put(
+                RecordTypeIdentifier.RECORD_TYPE_NUTRITION,
+                Set.of(APP_PACKAGE_NAME, APP_PACKAGE_NAME_2));
+        recordTypesToContributingPackagesMap.put(
+                RecordTypeIdentifier.RECORD_TYPE_HYDRATION, Set.of(APP_PACKAGE_NAME_3));
+        recordTypesToContributingPackagesMap.put(
+                RecordTypeIdentifier.RECORD_TYPE_HEART_RATE, Set.of(APP_PACKAGE_NAME_3));
+        recordTypesToContributingPackagesMap.put(
+                RecordTypeIdentifier.RECORD_TYPE_MENSTRUATION_FLOW,
+                Set.of(APP_PACKAGE_NAME_4, APP_PACKAGE_NAME_3, APP_PACKAGE_NAME_2));
+        when(mAppInfoHelper.getRecordTypesToContributingPackagesMap())
+                .thenReturn(recordTypesToContributingPackagesMap);
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.appHasDataInCategory(
+                                APP_PACKAGE_NAME_3, HealthDataCategory.ACTIVITY))
+                .isFalse();
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.appHasDataInCategory(
+                                APP_PACKAGE_NAME_4, HealthDataCategory.NUTRITION))
+                .isFalse();
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.appHasDataInCategory(
+                                APP_PACKAGE_NAME, HealthDataCategory.VITALS))
+                .isFalse();
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.appHasDataInCategory(
+                                APP_PACKAGE_NAME_2, HealthDataCategory.VITALS))
+                .isFalse();
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.appHasDataInCategory(
+                                APP_PACKAGE_NAME_4, HealthDataCategory.VITALS))
+                .isFalse();
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.appHasDataInCategory(
+                                APP_PACKAGE_NAME, HealthDataCategory.CYCLE_TRACKING))
+                .isFalse();
+    }
+
+    @Test
+    public void testAppHasDataInCategory_ifContributingPackagesMapEmpty_returnsFalse() {
+        Map<Integer, Set<String>> recordTypesToContributingPackagesMap = new HashMap<>();
+
+        when(mAppInfoHelper.getRecordTypesToContributingPackagesMap())
+                .thenReturn(recordTypesToContributingPackagesMap);
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.appHasDataInCategory(
+                                APP_PACKAGE_NAME, HealthDataCategory.SLEEP))
+                .isFalse();
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.appHasDataInCategory(
+                                APP_PACKAGE_NAME, HealthDataCategory.VITALS))
+                .isFalse();
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.appHasDataInCategory(
+                                APP_PACKAGE_NAME_4, HealthDataCategory.NUTRITION))
+                .isFalse();
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.appHasDataInCategory(
+                                APP_PACKAGE_NAME, HealthDataCategory.VITALS))
+                .isFalse();
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.appHasDataInCategory(
+                                APP_PACKAGE_NAME_2, HealthDataCategory.VITALS))
+                .isFalse();
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.appHasDataInCategory(
+                                APP_PACKAGE_NAME_4, HealthDataCategory.VITALS))
+                .isFalse();
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.appHasDataInCategory(
+                                APP_PACKAGE_NAME, HealthDataCategory.CYCLE_TRACKING))
+                .isFalse();
+    }
+
+    @Test
+    public void testGetDataCategoriesWithDataForPackage_returnsCorrectCategories() {
+        Map<Integer, Set<String>> recordTypesToContributorPackages = new HashMap<>();
+        recordTypesToContributorPackages.put(
+                RecordTypeIdentifier.RECORD_TYPE_STEPS,
+                Set.of(APP_PACKAGE_NAME, APP_PACKAGE_NAME_2));
+        recordTypesToContributorPackages.put(
+                RecordTypeIdentifier.RECORD_TYPE_DISTANCE, Set.of(APP_PACKAGE_NAME_3));
+        recordTypesToContributorPackages.put(
+                RecordTypeIdentifier.RECORD_TYPE_NUTRITION, Set.of(APP_PACKAGE_NAME_2));
+        recordTypesToContributorPackages.put(
+                RecordTypeIdentifier.RECORD_TYPE_HEART_RATE,
+                Set.of(APP_PACKAGE_NAME, APP_PACKAGE_NAME_3));
+        when(mAppInfoHelper.getRecordTypesToContributingPackagesMap())
+                .thenReturn(recordTypesToContributorPackages);
+
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.getDataCategoriesWithDataForPackage(
+                                APP_PACKAGE_NAME))
+                .isEqualTo(Set.of(HealthDataCategory.ACTIVITY, HealthDataCategory.VITALS));
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.getDataCategoriesWithDataForPackage(
+                                APP_PACKAGE_NAME_2))
+                .isEqualTo(Set.of(HealthDataCategory.ACTIVITY, HealthDataCategory.NUTRITION));
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.getDataCategoriesWithDataForPackage(
+                                APP_PACKAGE_NAME_3))
+                .isEqualTo(Set.of(HealthDataCategory.VITALS, HealthDataCategory.ACTIVITY));
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.getDataCategoriesWithDataForPackage(
+                                APP_PACKAGE_NAME_4))
+                .isEqualTo(Set.of());
+    }
+
+    @Test
+    public void testGetAllContributorApps_returnsJustAppsWithData() {
+        Map<Integer, Set<String>> recordTypesToContributorPackages = new HashMap<>();
+        recordTypesToContributorPackages.put(
+                RecordTypeIdentifier.RECORD_TYPE_STEPS,
+                Set.of(APP_PACKAGE_NAME, APP_PACKAGE_NAME_2));
+        recordTypesToContributorPackages.put(
+                RecordTypeIdentifier.RECORD_TYPE_DISTANCE, Set.of(APP_PACKAGE_NAME_3));
+        recordTypesToContributorPackages.put(
+                RecordTypeIdentifier.RECORD_TYPE_NUTRITION, Set.of(APP_PACKAGE_NAME_2));
+        recordTypesToContributorPackages.put(
+                RecordTypeIdentifier.RECORD_TYPE_HEART_RATE,
+                Set.of(APP_PACKAGE_NAME, APP_PACKAGE_NAME_3));
+        when(mAppInfoHelper.getRecordTypesToContributingPackagesMap())
+                .thenReturn(recordTypesToContributorPackages);
+
+        Map<Integer, Set<String>> expectedResult = new HashMap<>();
+        expectedResult.put(
+                HealthDataCategory.ACTIVITY,
+                Set.of(APP_PACKAGE_NAME, APP_PACKAGE_NAME_2, APP_PACKAGE_NAME_3));
+        expectedResult.put(HealthDataCategory.NUTRITION, Set.of(APP_PACKAGE_NAME_2));
+        expectedResult.put(HealthDataCategory.VITALS, Set.of(APP_PACKAGE_NAME, APP_PACKAGE_NAME_3));
+        assertThat(mHealthDataCategoryPriorityHelper.getAllContributorApps())
+                .isEqualTo(expectedResult);
+    }
+
+    @Test
+    public void testGetAllInactiveApps_returnsApps_withDataAndNoWritePermissions() {
+        // 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));
+        recordTypesToContributorPackages.put(
+                RecordTypeIdentifier.RECORD_TYPE_DISTANCE, Set.of(APP_PACKAGE_NAME_3));
+        recordTypesToContributorPackages.put(
+                RecordTypeIdentifier.RECORD_TYPE_NUTRITION, Set.of(APP_PACKAGE_NAME_2));
+        recordTypesToContributorPackages.put(
+                RecordTypeIdentifier.RECORD_TYPE_HEART_RATE,
+                Set.of(APP_PACKAGE_NAME, 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_SLEEP,
+                    HealthPermissions.WRITE_STEPS,
+                    HealthPermissions.READ_HEART_RATE,
+                    HealthPermissions.WRITE_OVULATION_TEST
+                };
+        mPackageInfo1.requestedPermissionsFlags =
+                new int[] {
+                    0,
+                    0,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED
+                };
+
+        mPackageInfo2.packageName = APP_PACKAGE_NAME_2;
+        mPackageInfo2.requestedPermissions =
+                new String[] {
+                    HealthPermissions.WRITE_SLEEP,
+                    HealthPermissions.WRITE_NUTRITION,
+                    HealthPermissions.WRITE_HEART_RATE,
+                    HealthPermissions.READ_BLOOD_GLUCOSE
+                };
+        mPackageInfo2.requestedPermissionsFlags =
+                new int[] {
+                    0,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED
+                };
+        when(HealthConnectManager.isHealthPermission(any(), any())).thenReturn(true);
+        when(mPackageInfoUtils.getPackagesHoldingHealthPermissions(any(), any()))
+                .thenReturn(List.of(mPackageInfo1, mPackageInfo2));
+
+        Map<Integer, Set<String>> expectedResult = new HashMap<>();
+        expectedResult.put(
+                HealthDataCategory.ACTIVITY,
+                Set.of(APP_PACKAGE_NAME, APP_PACKAGE_NAME_2, APP_PACKAGE_NAME_3));
+        expectedResult.put(HealthDataCategory.VITALS, Set.of(APP_PACKAGE_NAME, APP_PACKAGE_NAME_3));
+        assertThat(mHealthDataCategoryPriorityHelper.getAllInactiveApps(mContext))
+                .isEqualTo(expectedResult);
+    }
+
+    @Test
+    public void testAppHasWriteHealthPermissionsForCategory_ifWritePermission_returnsTrue() {
+        mPackageInfo1.packageName = APP_PACKAGE_NAME;
+        mPackageInfo1.requestedPermissions =
+                new String[] {
+                    HealthPermissions.WRITE_SLEEP,
+                    HealthPermissions.WRITE_STEPS,
+                    HealthPermissions.READ_HEART_RATE,
+                    HealthPermissions.WRITE_OVULATION_TEST
+                };
+        mPackageInfo1.requestedPermissionsFlags =
+                new int[] {
+                    0,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED
+                };
+
+        mPackageInfo2.packageName = APP_PACKAGE_NAME_2;
+        mPackageInfo2.requestedPermissions =
+                new String[] {
+                    HealthPermissions.WRITE_SLEEP,
+                    HealthPermissions.READ_SLEEP,
+                    HealthPermissions.WRITE_HEART_RATE,
+                    HealthPermissions.READ_BLOOD_GLUCOSE
+                };
+        mPackageInfo2.requestedPermissionsFlags =
+                new int[] {
+                    0,
+                    0,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED
+                };
+        when(HealthConnectManager.isHealthPermission(any(), any())).thenReturn(true);
+        when(mPackageInfoUtils.getPackagesHoldingHealthPermissions(any(), any()))
+                .thenReturn(List.of(mPackageInfo1, mPackageInfo2));
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.appHasWriteHealthPermissionsForCategory(
+                                APP_PACKAGE_NAME, HealthDataCategory.ACTIVITY, mContext))
+                .isTrue();
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.appHasWriteHealthPermissionsForCategory(
+                                APP_PACKAGE_NAME_2, HealthDataCategory.VITALS, mContext))
+                .isTrue();
+    }
+
+    @Test
+    public void testAppHasWriteHealthPermissionsForCategory_ifNoWritePermission_returnsFalse() {
+        mPackageInfo1.packageName = APP_PACKAGE_NAME;
+        mPackageInfo1.requestedPermissions =
+                new String[] {
+                    HealthPermissions.WRITE_STEPS,
+                    HealthPermissions.WRITE_SLEEP,
+                    HealthPermissions.READ_HEART_RATE,
+                    HealthPermissions.WRITE_OVULATION_TEST
+                };
+        mPackageInfo1.requestedPermissionsFlags =
+                new int[] {
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    0,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED
+                };
+
+        mPackageInfo2.packageName = APP_PACKAGE_NAME_2;
+        mPackageInfo2.requestedPermissions =
+                new String[] {
+                    HealthPermissions.WRITE_STEPS,
+                    HealthPermissions.READ_SLEEP,
+                    HealthPermissions.WRITE_HEART_RATE,
+                    HealthPermissions.READ_BLOOD_GLUCOSE
+                };
+        mPackageInfo2.requestedPermissionsFlags =
+                new int[] {
+                    0,
+                    0,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                    PackageInfo.REQUESTED_PERMISSION_GRANTED
+                };
+        when(HealthConnectManager.isHealthPermission(any(), any())).thenReturn(true);
+        when(mPackageInfoUtils.getPackagesHoldingHealthPermissions(any(), any()))
+                .thenReturn(List.of(mPackageInfo1, mPackageInfo2));
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.appHasWriteHealthPermissionsForCategory(
+                                APP_PACKAGE_NAME, HealthDataCategory.SLEEP, mContext))
+                .isFalse();
+        assertThat(
+                        mHealthDataCategoryPriorityHelper.appHasWriteHealthPermissionsForCategory(
+                                APP_PACKAGE_NAME_2, HealthDataCategory.BODY_MEASUREMENTS, mContext))
+                .isFalse();
+    }
+
+    private void verifyPriorityUpdate(List<Long> priorityOrder, int dataCategory) {
+        verify(mTransactionManager)
+                .insertOrReplace(argThat(new UpsertRequestMatcher(priorityOrder)));
+
+        assertThat(mHealthDataCategoryPriorityHelper.getAppIdPriorityOrder(dataCategory))
                 .isEqualTo(priorityOrder);
     }
 
-    private static final class RequestMatcher implements ArgumentMatcher<UpsertTableRequest> {
+    private static final class UpsertRequestMatcher implements ArgumentMatcher<UpsertTableRequest> {
 
         private final List<Long> mPriorityOrder;
         private UpsertTableRequest mRequest;
 
-        RequestMatcher(List<Long> priorityOrder) {
+        UpsertRequestMatcher(List<Long> priorityOrder) {
             mPriorityOrder = priorityOrder;
         }
 
@@ -221,4 +1605,65 @@
                     + "]";
         }
     }
+
+    private static final class DeleteRequestMatcher implements ArgumentMatcher<DeleteTableRequest> {
+
+        private final int mDataCategory;
+        private DeleteTableRequest mRequest;
+
+        DeleteRequestMatcher(int dataCategory) {
+            mDataCategory = dataCategory;
+        }
+
+        @Override
+        public boolean matches(DeleteTableRequest request) {
+            mRequest = request;
+            return request != null
+                    && request.getIdColumnName() != null
+                    && request.getIdColumnName().equals(HEALTH_DATA_CATEGORY_COLUMN_NAME)
+                    && request.getIds()
+                            .equals(
+                                    Collections.singletonList(
+                                            StorageUtils.getNormalisedString(
+                                                    String.valueOf(mDataCategory))));
+        }
+
+        @Override
+        public String toString() {
+            return "Expected Delete Request for category = ["
+                    + mDataCategory
+                    + "]"
+                    + ", Actual category = ["
+                    + mRequest.getIds()
+                    + "]";
+        }
+    }
+
+    private void setupPriorityList(Map<Integer, List<Long>> priorityList) {
+        List<Boolean> cursorNext = new ArrayList<>();
+        List<Integer> categories = new ArrayList<>();
+        List<String> priorities = new ArrayList<>();
+
+        for (Map.Entry<Integer, List<Long>> entry : priorityList.entrySet()) {
+            int dataCategory = entry.getKey();
+            List<Long> priorityListForCategory = entry.getValue();
+            String flattened = flattenLongList(priorityListForCategory);
+            cursorNext.add(true);
+            categories.add(dataCategory);
+            priorities.add(flattened);
+        }
+        cursorNext.add(false);
+        when(mCursor.moveToNext())
+                .thenReturn(
+                        cursorNext.get(0),
+                        cursorNext.subList(1, cursorNext.size()).toArray(new Boolean[] {}));
+        when(mCursor.getInt(eq(HEALTH_DATA_CATEGORY_COLUMN_INDEX)))
+                .thenReturn(
+                        categories.get(0),
+                        categories.subList(1, categories.size()).toArray(new Integer[] {}));
+        when(mCursor.getString(APP_ID_PRIORITY_ORDER_COLUMN_INDEX))
+                .thenReturn(
+                        priorities.get(0),
+                        priorities.subList(1, priorities.size()).toArray(new String[] {}));
+    }
 }
diff --git a/tests/unittests/src/com/android/server/healthconnect/storage/datatypehelpers/RecordHelperTest.java b/tests/unittests/src/com/android/server/healthconnect/storage/datatypehelpers/RecordHelperTest.java
index 2240e18..397d1b9 100644
--- a/tests/unittests/src/com/android/server/healthconnect/storage/datatypehelpers/RecordHelperTest.java
+++ b/tests/unittests/src/com/android/server/healthconnect/storage/datatypehelpers/RecordHelperTest.java
@@ -16,26 +16,44 @@
 
 package com.android.server.healthconnect.storage.datatypehelpers;
 
-import static com.android.server.healthconnect.TestUtils.TEST_USER;
+import static android.health.connect.Constants.DEFAULT_LONG;
+import static android.health.connect.Constants.MAXIMUM_ALLOWED_CURSOR_COUNT;
+
 import static com.android.server.healthconnect.storage.datatypehelpers.StepsRecordHelper.STEPS_TABLE_NAME;
+import static com.android.server.healthconnect.storage.datatypehelpers.TransactionTestUtils.createStepsRecord;
+import static com.android.server.healthconnect.storage.utils.WhereClauses.LogicalOperator.AND;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertThrows;
+
 import android.database.Cursor;
+import android.health.connect.ReadRecordsRequestUsingFilters;
+import android.health.connect.TimeInstantRangeFilter;
+import android.health.connect.aidl.ReadRecordsRequestParcel;
+import android.health.connect.datatypes.StepsRecord;
 import android.health.connect.internal.datatypes.RecordInternal;
 import android.health.connect.internal.datatypes.StepsRecordInternal;
+import android.util.Pair;
 
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.healthconnect.HealthConnectUserContext;
 import com.android.server.healthconnect.storage.TransactionManager;
 import com.android.server.healthconnect.storage.request.ReadTableRequest;
+import com.android.server.healthconnect.storage.utils.OrderByClause;
+import com.android.server.healthconnect.storage.utils.PageTokenUtil;
+import com.android.server.healthconnect.storage.utils.PageTokenWrapper;
+import com.android.server.healthconnect.storage.utils.WhereClauses;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.time.Instant;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.UUID;
 
@@ -52,17 +70,27 @@
     public void setup() throws Exception {
         HealthConnectUserContext context = testRule.getUserContext();
         mTransactionManager = TransactionManager.getInstance(context);
-        mTransactionTestUtils = new TransactionTestUtils(context, TEST_USER);
+        DatabaseHelper.clearAllData(mTransactionManager);
+        mTransactionTestUtils = new TransactionTestUtils(context, mTransactionManager);
         mTransactionTestUtils.insertApp(TEST_PACKAGE_NAME);
     }
 
+    @After
+    public void tearDown() {
+        DatabaseHelper.clearAllData(mTransactionManager);
+        TransactionManager.clearInstance();
+    }
+
     @Test
     public void getInternalRecords_insertThenRead_recordReturned() {
         RecordHelper<?> helper = new StepsRecordHelper();
-        String uid = mTransactionTestUtils.insertStepsRecord(4000, 5000, 100);
+        String uid =
+                mTransactionTestUtils
+                        .insertRecords(TEST_PACKAGE_NAME, createStepsRecord(4000, 5000, 100))
+                        .get(0);
         ReadTableRequest request = new ReadTableRequest(STEPS_TABLE_NAME);
         try (Cursor cursor = mTransactionManager.read(request)) {
-            List<RecordInternal<?>> records = helper.getInternalRecords(cursor, 1);
+            List<RecordInternal<?>> records = helper.getInternalRecords(cursor);
             assertThat(records).hasSize(1);
 
             StepsRecordInternal record = (StepsRecordInternal) records.get(0);
@@ -72,4 +100,213 @@
             assertThat(record.getCount()).isEqualTo(100);
         }
     }
+
+    @Test
+    public void getInternalRecords_requestSizeMoreThanRecordNumber_recordsReturned() {
+        RecordHelper<?> helper = new StepsRecordHelper();
+        String uid =
+                mTransactionTestUtils
+                        .insertRecords(TEST_PACKAGE_NAME, createStepsRecord(4000, 5000, 100))
+                        .get(0);
+        ReadTableRequest request = new ReadTableRequest(STEPS_TABLE_NAME);
+        try (Cursor cursor = mTransactionManager.read(request)) {
+            assertThat(cursor.getCount()).isEqualTo(1);
+            List<RecordInternal<?>> records = helper.getInternalRecords(cursor);
+            assertThat(records).hasSize(1);
+            assertThat(records.get(0).getUuid()).isEqualTo(UUID.fromString(uid));
+        }
+    }
+
+    @Test
+    public void getInternalRecords_cursorHasTooManyData_throws() {
+        RecordHelper<?> helper = new StepsRecordHelper();
+        int startTime = 9527;
+        List<RecordInternal<?>> records = new ArrayList<>(MAXIMUM_ALLOWED_CURSOR_COUNT + 1);
+        for (int i = 0; i <= MAXIMUM_ALLOWED_CURSOR_COUNT; i++) {
+            records.add(createStepsRecord(startTime + i, startTime + i + 1, 100));
+        }
+        mTransactionTestUtils.insertRecords(TEST_PACKAGE_NAME, records);
+
+        ReadTableRequest request = new ReadTableRequest(STEPS_TABLE_NAME);
+        try (Cursor cursor = mTransactionManager.read(request)) {
+            Throwable thrown =
+                    assertThrows(
+                            IllegalArgumentException.class,
+                            () -> helper.getInternalRecords(cursor));
+            assertThat(thrown.getMessage()).contains("Too many records in the cursor.");
+        }
+    }
+
+    @Test
+    public void getNextInternalRecordsPageAndToken_zeroOffsetDesc_correctResults() {
+        RecordHelper<?> helper = new StepsRecordHelper();
+        int pageSize = 1;
+        boolean isAscending = false;
+        mTransactionTestUtils.insertRecords(
+                TEST_PACKAGE_NAME,
+                createStepsRecord(
+                        "client.id1",
+                        /* startTimeMillis= */ 4000,
+                        /* endTimeMillis= */ 4500,
+                        /* stepsCount= */ 1000),
+                createStepsRecord(
+                        "client.id2",
+                        /* startTimeMillis= */ 6000,
+                        /* endTimeMillis= */ 7000,
+                        /* stepsCount= */ 500));
+
+        long expectedTimestamp = 4000L;
+        int expectedOffset = 0;
+        PageTokenWrapper expectedPageToken =
+                PageTokenWrapper.of(isAscending, expectedTimestamp, expectedOffset);
+
+        OrderByClause orderByStartTime =
+                new OrderByClause().addOrderByClause(helper.getStartTimeColumnName(), isAscending);
+        ReadTableRequest request1 =
+                new ReadTableRequest(STEPS_TABLE_NAME)
+                        .setOrderBy(orderByStartTime)
+                        .setLimit(pageSize + 1);
+        try (Cursor cursor = mTransactionManager.read(request1)) {
+            Pair<List<RecordInternal<?>>, Long> page1 =
+                    helper.getNextInternalRecordsPageAndToken(
+                            cursor, pageSize, PageTokenWrapper.ofAscending(isAscending));
+            assertThat(page1.first).hasSize(pageSize);
+            assertThat(page1.first.get(0).getClientRecordId()).isEqualTo("client.id2");
+            assertThat(page1.second).isEqualTo(PageTokenUtil.encode(expectedPageToken));
+        }
+
+        WhereClauses whereClause =
+                new WhereClauses(AND)
+                        .addWhereLessThanOrEqualClause(
+                                helper.getStartTimeColumnName(), expectedTimestamp);
+        ReadTableRequest request2 =
+                new ReadTableRequest(STEPS_TABLE_NAME)
+                        .setOrderBy(orderByStartTime)
+                        .setWhereClause(whereClause)
+                        .setLimit(pageSize + 1 + expectedOffset);
+        try (Cursor cursor = mTransactionManager.read(request2)) {
+            Pair<List<RecordInternal<?>>, Long> page2 =
+                    helper.getNextInternalRecordsPageAndToken(cursor, pageSize, expectedPageToken);
+            assertThat(page2.first).hasSize(pageSize);
+            assertThat(page2.first.get(0).getClientRecordId()).isEqualTo("client.id1");
+            assertThat(page2.second).isEqualTo(DEFAULT_LONG);
+        }
+    }
+
+    @Test
+    public void getNextInternalRecordsPageAndToken_sameStartTimeAsc_correctResults() {
+        RecordHelper<?> helper = new StepsRecordHelper();
+        int pageSize = 3;
+        boolean isAscending = true;
+        mTransactionTestUtils.insertRecords(
+                TEST_PACKAGE_NAME,
+                // in page 1
+                createStepsRecord(
+                        "id1",
+                        /* startTimeMillis= */ 3000,
+                        /* endTimeMillis= */ 45000,
+                        /* stepsCount= */ 1000),
+                createStepsRecord(
+                        "id2",
+                        /* startTimeMillis= */ 4000,
+                        /* endTimeMillis= */ 5000,
+                        /* stepsCount= */ 100),
+                createStepsRecord(
+                        "id3",
+                        /* startTimeMillis= */ 4000,
+                        /* endTimeMillis= */ 6000,
+                        /* stepsCount= */ 200),
+                // in page 2
+                createStepsRecord(
+                        "id4",
+                        /* startTimeMillis= */ 4000,
+                        /* endTimeMillis= */ 7000,
+                        /* stepsCount= */ 300),
+                createStepsRecord(
+                        "id5",
+                        /* startTimeMillis= */ 5000,
+                        /* endTimeMillis= */ 6000,
+                        /* stepsCount= */ 400),
+                createStepsRecord(
+                        "id6",
+                        /* startTimeMillis= */ 6000,
+                        /* endTimeMillis= */ 7000,
+                        /* stepsCount= */ 500));
+
+        long expectedTimestamp = 4000L;
+        int expectedOffset = 2;
+        PageTokenWrapper expectedPageToken =
+                PageTokenWrapper.of(isAscending, expectedTimestamp, expectedOffset);
+
+        TimeInstantRangeFilter filter =
+                new TimeInstantRangeFilter.Builder()
+                        .setStartTime(Instant.ofEpochMilli(3000))
+                        .setEndTime(Instant.ofEpochMilli(10000))
+                        .build();
+        ReadRecordsRequestUsingFilters<StepsRecord> readRequest1 =
+                new ReadRecordsRequestUsingFilters.Builder<>(StepsRecord.class)
+                        .setTimeRangeFilter(filter)
+                        .setPageSize(pageSize)
+                        .build();
+        ReadTableRequest request1 =
+                getReadTableRequest(helper, readRequest1.toReadRecordsRequestParcel());
+        try (Cursor cursor = mTransactionManager.read(request1)) {
+            Pair<List<RecordInternal<?>>, Long> page1 =
+                    helper.getNextInternalRecordsPageAndToken(
+                            cursor, pageSize, PageTokenWrapper.ofAscending(isAscending));
+            assertThat(page1.first).hasSize(3);
+            assertThat(page1.first.get(0).getClientRecordId()).isEqualTo("id1");
+            assertThat(page1.first.get(1).getClientRecordId()).isEqualTo("id2");
+            assertThat(page1.first.get(2).getClientRecordId()).isEqualTo("id3");
+            assertThat(page1.second).isEqualTo(PageTokenUtil.encode(expectedPageToken));
+        }
+
+        ReadRecordsRequestUsingFilters<StepsRecord> readRequest2 =
+                new ReadRecordsRequestUsingFilters.Builder<>(StepsRecord.class)
+                        .setTimeRangeFilter(filter)
+                        .setPageSize(pageSize)
+                        .setPageToken(PageTokenUtil.encode(expectedPageToken))
+                        .build();
+        ReadTableRequest request2 =
+                getReadTableRequest(helper, readRequest2.toReadRecordsRequestParcel());
+        try (Cursor cursor = mTransactionManager.read(request2)) {
+            Pair<List<RecordInternal<?>>, Long> page2 =
+                    helper.getNextInternalRecordsPageAndToken(cursor, pageSize, expectedPageToken);
+            assertThat(page2.first).hasSize(pageSize);
+            assertThat(page2.first.get(0).getClientRecordId()).isEqualTo("id4");
+            assertThat(page2.first.get(1).getClientRecordId()).isEqualTo("id5");
+            assertThat(page2.first.get(2).getClientRecordId()).isEqualTo("id6");
+            assertThat(page2.second).isEqualTo(DEFAULT_LONG);
+        }
+    }
+
+    @Test
+    public void getNextInternalRecordsPageAndToken_wrongOffsetPageToken_skipSameStartTimeRecords() {
+        RecordHelper<?> helper = new StepsRecordHelper();
+        mTransactionTestUtils.insertRecords(
+                TEST_PACKAGE_NAME,
+                createStepsRecord("id1", 4000, 5000, 100),
+                createStepsRecord("id2", 5000, 6000, 100));
+        PageTokenWrapper incorrectToken = PageTokenWrapper.of(true, 4000, 2);
+        ReadTableRequest request = new ReadTableRequest(STEPS_TABLE_NAME);
+        try (Cursor cursor = mTransactionManager.read(request)) {
+            Pair<List<RecordInternal<?>>, Long> result =
+                    helper.getNextInternalRecordsPageAndToken(
+                            cursor, /* requestSize= */ 2, incorrectToken);
+            // skip the first record, but preserve the second because start time is different
+            assertThat(result.first).hasSize(1);
+            assertThat(result.first.get(0).getClientRecordId()).isEqualTo("id2");
+            assertThat(result.second).isEqualTo(DEFAULT_LONG);
+        }
+    }
+
+    private static ReadTableRequest getReadTableRequest(
+            RecordHelper<?> helper, ReadRecordsRequestParcel request) {
+        return helper.getReadTableRequest(
+                request,
+                TEST_PACKAGE_NAME,
+                /* enforceSelfRead= */ false,
+                /* startDateAccess= */ 0,
+                /* extraPermsState= */ null);
+    }
 }
diff --git a/tests/unittests/src/com/android/server/healthconnect/storage/datatypehelpers/TransactionTestUtils.java b/tests/unittests/src/com/android/server/healthconnect/storage/datatypehelpers/TransactionTestUtils.java
index 6bacaa2..b1bf925 100644
--- a/tests/unittests/src/com/android/server/healthconnect/storage/datatypehelpers/TransactionTestUtils.java
+++ b/tests/unittests/src/com/android/server/healthconnect/storage/datatypehelpers/TransactionTestUtils.java
@@ -16,34 +16,48 @@
 
 package com.android.server.healthconnect.storage.datatypehelpers;
 
+import static android.health.connect.Constants.DEFAULT_LONG;
+import static android.health.connect.datatypes.ExerciseSessionType.EXERCISE_SESSION_TYPE_RUNNING;
+
 import static com.android.server.healthconnect.storage.datatypehelpers.AppInfoHelper.PACKAGE_COLUMN_NAME;
 import static com.android.server.healthconnect.storage.datatypehelpers.AppInfoHelper.UNIQUE_COLUMN_INFO;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static java.time.Duration.ofMinutes;
+
 import android.content.ContentValues;
 import android.content.Context;
+import android.health.connect.aidl.ReadRecordsRequestParcel;
+import android.health.connect.datatypes.BloodPressureRecord;
+import android.health.connect.datatypes.StepsRecord;
+import android.health.connect.internal.datatypes.BloodPressureRecordInternal;
+import android.health.connect.internal.datatypes.ExerciseRouteInternal;
+import android.health.connect.internal.datatypes.ExerciseSessionRecordInternal;
+import android.health.connect.internal.datatypes.RecordInternal;
 import android.health.connect.internal.datatypes.StepsRecordInternal;
-import android.os.UserHandle;
 
-import com.android.server.healthconnect.HealthConnectUserContext;
 import com.android.server.healthconnect.storage.TransactionManager;
+import com.android.server.healthconnect.storage.request.ReadTransactionRequest;
 import com.android.server.healthconnect.storage.request.UpsertTableRequest;
 import com.android.server.healthconnect.storage.request.UpsertTransactionRequest;
 
-import com.google.common.collect.ImmutableList;
-
+import java.time.Instant;
 import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.stream.IntStream;
 
 /** Util class provides shared functionality for db transaction testing. */
 public final class TransactionTestUtils {
+    private static final Map<String, Boolean> NO_EXTRA_PERMS = Map.of();
+    private static final String TEST_PACKAGE_NAME = "package.name";
     private final TransactionManager mTransactionManager;
     private final Context mContext;
 
-    public TransactionTestUtils(Context context, UserHandle userHandle) {
+    public TransactionTestUtils(Context context, TransactionManager transactionManager) {
         mContext = context;
-        mTransactionManager =
-                TransactionManager.getInstance(new HealthConnectUserContext(context, userHandle));
+        mTransactionManager = transactionManager;
     }
 
     public void insertApp(String packageName) {
@@ -52,25 +66,89 @@
         mTransactionManager.insert(
                 new UpsertTableRequest(
                         AppInfoHelper.TABLE_NAME, contentValues, UNIQUE_COLUMN_INFO));
-        assertThat(AppInfoHelper.getInstance().getAppInfoId(packageName)).isEqualTo(1);
+        AppInfoHelper.getInstance().clearCache();
+        assertThat(AppInfoHelper.getInstance().getAppInfoId(packageName))
+                .isNotEqualTo(DEFAULT_LONG);
     }
 
-    public String insertStepsRecord(long startTimeMillis, long endTimeMillis, int stepsCount) {
-        StepsRecordInternal recordInternal =
-                (StepsRecordInternal)
-                        new StepsRecordInternal()
-                                .setCount(stepsCount)
-                                .setStartTime(startTimeMillis)
-                                .setEndTime(endTimeMillis);
+    /** Inserts records attributed to the given package. */
+    public List<String> insertRecords(String packageName, RecordInternal<?>... records) {
+        return insertRecords(packageName, List.of(records));
+    }
 
-        List<String> uids =
-                mTransactionManager.insertAll(
-                        new UpsertTransactionRequest(
-                                "package.name",
-                                ImmutableList.of(recordInternal),
-                                mContext,
-                                true,
-                                false));
-        return uids.get(0);
+    /** Inserts records attributed to the given package. */
+    public List<String> insertRecords(String packageName, List<RecordInternal<?>> records) {
+        return mTransactionManager.insertAll(
+                new UpsertTransactionRequest(
+                        packageName,
+                        records,
+                        mContext,
+                        /* isInsertRequest= */ true,
+                        /* skipPackageNameAndLogs= */ false));
+    }
+
+    public static ReadTransactionRequest getReadTransactionRequest(
+            Map<Integer, List<UUID>> recordTypeToUuids) {
+        return new ReadTransactionRequest(
+                TEST_PACKAGE_NAME, recordTypeToUuids, /* startDateAccess= */ 0, NO_EXTRA_PERMS);
+    }
+
+    public static ReadTransactionRequest getReadTransactionRequest(
+            ReadRecordsRequestParcel request) {
+        return new ReadTransactionRequest(
+                TEST_PACKAGE_NAME,
+                request,
+                /* startDateAccessMillis= */ 0,
+                /* enforceSelfRead= */ false,
+                NO_EXTRA_PERMS);
+    }
+
+    public static RecordInternal<StepsRecord> createStepsRecord(
+            long startTimeMillis, long endTimeMillis, int stepsCount) {
+        return createStepsRecord(/* clientId= */ null, startTimeMillis, endTimeMillis, stepsCount);
+    }
+
+    public static RecordInternal<StepsRecord> createStepsRecord(
+            String clientId, long startTimeMillis, long endTimeMillis, int stepsCount) {
+        return new StepsRecordInternal()
+                .setCount(stepsCount)
+                .setStartTime(startTimeMillis)
+                .setEndTime(endTimeMillis)
+                .setClientRecordId(clientId);
+    }
+
+    public static RecordInternal<BloodPressureRecord> createBloodPressureRecord(
+            long timeMillis, double systolic, double diastolic) {
+        return new BloodPressureRecordInternal()
+                .setSystolic(systolic)
+                .setDiastolic(diastolic)
+                .setTime(timeMillis);
+    }
+
+    /** Creates an exercise sessions with a route. */
+    public static ExerciseSessionRecordInternal createExerciseSessionRecordWithRoute(
+            Instant startTime) {
+        return (ExerciseSessionRecordInternal)
+                new ExerciseSessionRecordInternal()
+                        .setExerciseType(EXERCISE_SESSION_TYPE_RUNNING)
+                        .setRoute(createExerciseRoute(startTime))
+                        .setStartTime(startTime.toEpochMilli())
+                        .setEndTime(startTime.plus(ofMinutes(10)).toEpochMilli());
+    }
+
+    private static ExerciseRouteInternal createExerciseRoute(Instant startTime) {
+        int numberOfLocations = 3;
+        double latitude = 52.13;
+        double longitude = 0.14;
+
+        return new ExerciseRouteInternal(
+                IntStream.range(0, numberOfLocations)
+                        .mapToObj(
+                                i ->
+                                        new ExerciseRouteInternal.LocationInternal()
+                                                .setTime(startTime.plusSeconds(i).toEpochMilli())
+                                                .setLatitude(latitude + 0.001 * i)
+                                                .setLongitude(longitude + 0.001 * i))
+                        .toList());
     }
 }
diff --git a/tests/unittests/src/com/android/server/healthconnect/storage/datatypehelpers/aggregation/SessionPriorityAggregationTest.java b/tests/unittests/src/com/android/server/healthconnect/storage/datatypehelpers/aggregation/SessionPriorityAggregationTest.java
index 631e16e..fb76b5d 100644
--- a/tests/unittests/src/com/android/server/healthconnect/storage/datatypehelpers/aggregation/SessionPriorityAggregationTest.java
+++ b/tests/unittests/src/com/android/server/healthconnect/storage/datatypehelpers/aggregation/SessionPriorityAggregationTest.java
@@ -25,25 +25,37 @@
 
 import android.database.Cursor;
 
+import com.android.modules.utils.testing.ExtendedMockitoRule;
+import com.android.server.healthconnect.HealthConnectDeviceConfigManager;
 import com.android.server.healthconnect.storage.request.AggregateParams;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
+import org.mockito.quality.Strictness;
 
 import java.util.Collections;
 import java.util.List;
 
 public class SessionPriorityAggregationTest {
-    @Mock Cursor mCursor;
-
     PriorityRecordsAggregator mOneGroupAggregator;
     PriorityRecordsAggregator mMultiGroupAggregator;
     AggregateParams.PriorityAggregationExtraParams mParams =
             new AggregateParams.PriorityAggregationExtraParams("start", "end");
 
+    @Rule
+    public final ExtendedMockitoRule mExtendedMockitoRule =
+            new ExtendedMockitoRule.Builder(this)
+                    .mockStatic(HealthConnectDeviceConfigManager.class)
+                    .setStrictness(Strictness.LENIENT)
+                    .build();
+
+    @Mock Cursor mCursor;
+    @Mock HealthConnectDeviceConfigManager mHealthConnectDeviceConfigManager;
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -60,6 +72,10 @@
                                 0,
                                 mParams,
                                 false));
+        when(HealthConnectDeviceConfigManager.getInitialisedInstance())
+                .thenReturn(mHealthConnectDeviceConfigManager);
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(false);
     }
 
     @Test
@@ -131,6 +147,47 @@
     }
 
     @Test
+    public void testOneSession_newAggregation_noPriority_returnsNothing() {
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(true);
+        doReturn(createSessionData(10, 20, Integer.MIN_VALUE))
+                .when(mOneGroupAggregator)
+                .readNewData(mCursor);
+        when(mCursor.moveToNext()).thenReturn(true, false);
+        mOneGroupAggregator.calculateAggregation(mCursor);
+        assertThat(mOneGroupAggregator.getResultForGroup(0)).isNull();
+    }
+
+    @Test
+    public void testTwoSessions_newAggregation_sessionWithoutPriority_isNotCounted() {
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(true);
+        doReturn(
+                        createSessionData(5, 18, 1),
+                        createSessionData(12, 25, Integer.MIN_VALUE, List.of(13L), List.of(16L)))
+                .when(mOneGroupAggregator)
+                .readNewData(mCursor);
+        when(mCursor.moveToNext()).thenReturn(true, true, false);
+        mOneGroupAggregator.calculateAggregation(mCursor);
+        assertThat(mOneGroupAggregator.getResultForGroup(0)).isEqualTo(8.0);
+    }
+
+    // TODO testTwoSessions_noneHasPriority_returnsNothing
+    @Test
+    public void testTwoSessions_newAggregation_sessionsWithoutPriority_returnsNull() {
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(true);
+        doReturn(
+                        createSessionData(5, 18, Integer.MIN_VALUE),
+                        createSessionData(12, 25, Integer.MIN_VALUE, List.of(13L), List.of(16L)))
+                .when(mOneGroupAggregator)
+                .readNewData(mCursor);
+        when(mCursor.moveToNext()).thenReturn(true, true, false);
+        mOneGroupAggregator.calculateAggregation(mCursor);
+        assertThat(mOneGroupAggregator.getResultForGroup(0)).isNull();
+    }
+
+    @Test
     public void testTwoSessions_noOverlaps() {
         doReturn(createSessionData(5, 12, 1), createSessionData(15, 25, 2))
                 .when(mOneGroupAggregator)
diff --git a/tests/unittests/src/com/android/server/healthconnect/storage/datatypehelpers/aggregation/ValuePriorityAggregationTest.java b/tests/unittests/src/com/android/server/healthconnect/storage/datatypehelpers/aggregation/ValuePriorityAggregationTest.java
index 612008e..6069d6d 100644
--- a/tests/unittests/src/com/android/server/healthconnect/storage/datatypehelpers/aggregation/ValuePriorityAggregationTest.java
+++ b/tests/unittests/src/com/android/server/healthconnect/storage/datatypehelpers/aggregation/ValuePriorityAggregationTest.java
@@ -25,25 +25,37 @@
 
 import android.database.Cursor;
 
+import com.android.modules.utils.testing.ExtendedMockitoRule;
+import com.android.server.healthconnect.HealthConnectDeviceConfigManager;
 import com.android.server.healthconnect.storage.request.AggregateParams;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
+import org.mockito.quality.Strictness;
 
 import java.util.Collections;
 import java.util.List;
 
 public class ValuePriorityAggregationTest {
     @Mock Cursor mCursor;
+    @Mock HealthConnectDeviceConfigManager mHealthConnectDeviceConfigManager;
 
     PriorityRecordsAggregator mOneGroupAggregator;
     PriorityRecordsAggregator mMultiGroupAggregator;
     AggregateParams.PriorityAggregationExtraParams mParams =
             new AggregateParams.PriorityAggregationExtraParams("start", "end");
 
+    @Rule
+    public final ExtendedMockitoRule mExtendedMockitoRule =
+            new ExtendedMockitoRule.Builder(this)
+                    .mockStatic(HealthConnectDeviceConfigManager.class)
+                    .setStrictness(Strictness.LENIENT)
+                    .build();
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -60,6 +72,10 @@
                                 0,
                                 mParams,
                                 false));
+        when(HealthConnectDeviceConfigManager.getInitialisedInstance())
+                .thenReturn(mHealthConnectDeviceConfigManager);
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(false);
     }
 
     @Test
@@ -364,4 +380,46 @@
         mOneGroupAggregator.calculateAggregation(mCursor);
         assertThat(mOneGroupAggregator.getResultForGroup(0)).isEqualTo(40);
     }
+
+    @Test
+    public void testOneStepRecord_newAggregation_noPriority_notAccountedForAggregation() {
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(true);
+        doReturn(createStepsData(10, 20, 10, Integer.MIN_VALUE, 1))
+                .when(mOneGroupAggregator)
+                .readNewData(mCursor);
+        when(mCursor.moveToNext()).thenReturn(true, false);
+        mOneGroupAggregator.calculateAggregation(mCursor);
+        assertThat(mOneGroupAggregator.getResultForGroup(0)).isNull();
+    }
+
+    @Test
+    public void
+            testTwoStepRecords_newAggregation_recordWithNoPriority_notAccountedForAggregation() {
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(true);
+        doReturn(
+                        createStepsData(15, 15, 10, Integer.MIN_VALUE, 10),
+                        createStepsData(15, 15, 20, 100, 20))
+                .when(mOneGroupAggregator)
+                .readNewData(mCursor);
+        when(mCursor.moveToNext()).thenReturn(true, true, false);
+        mOneGroupAggregator.calculateAggregation(mCursor);
+        assertThat(mOneGroupAggregator.getResultForGroup(0)).isEqualTo(20);
+    }
+
+    @Test
+    public void
+            testTwoStepRecords_newAggregation_noRecordsWithPriority_notAccountedForAggregation() {
+        when(mHealthConnectDeviceConfigManager.isAggregationSourceControlsEnabled())
+                .thenReturn(true);
+        doReturn(
+                        createStepsData(15, 15, 10, Integer.MIN_VALUE, 10),
+                        createStepsData(15, 15, 20, Integer.MIN_VALUE, 20))
+                .when(mOneGroupAggregator)
+                .readNewData(mCursor);
+        when(mCursor.moveToNext()).thenReturn(true, true, false);
+        mOneGroupAggregator.calculateAggregation(mCursor);
+        assertThat(mOneGroupAggregator.getResultForGroup(0)).isNull();
+    }
 }
diff --git a/tests/unittests/src/com/android/server/healthconnect/storage/request/ReadTransactionRequestTest.java b/tests/unittests/src/com/android/server/healthconnect/storage/request/ReadTransactionRequestTest.java
new file mode 100644
index 0000000..dc0053b
--- /dev/null
+++ b/tests/unittests/src/com/android/server/healthconnect/storage/request/ReadTransactionRequestTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.server.healthconnect.storage.request;
+
+import static com.android.server.healthconnect.storage.datatypehelpers.TransactionTestUtils.getReadTransactionRequest;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.health.connect.ReadRecordsRequestUsingFilters;
+import android.health.connect.ReadRecordsRequestUsingIds;
+import android.health.connect.datatypes.RecordTypeIdentifier;
+import android.health.connect.datatypes.StepsRecord;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.healthconnect.HealthConnectUserContext;
+import com.android.server.healthconnect.storage.TransactionManager;
+import com.android.server.healthconnect.storage.datatypehelpers.HealthConnectDatabaseTestRule;
+import com.android.server.healthconnect.storage.utils.PageTokenUtil;
+import com.android.server.healthconnect.storage.utils.PageTokenWrapper;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+@RunWith(AndroidJUnit4.class)
+public class ReadTransactionRequestTest {
+    @Rule public final HealthConnectDatabaseTestRule testRule = new HealthConnectDatabaseTestRule();
+
+    @Before
+    public void setup() {
+        HealthConnectUserContext context = testRule.getUserContext();
+        TransactionManager.getInstance(context);
+    }
+
+    @Test
+    public void createReadByFilterRequest_noPageToken_correctPaginationInfo() {
+        PageTokenWrapper expectedToken = PageTokenWrapper.ofAscending(false);
+        ReadRecordsRequestUsingFilters<StepsRecord> readRecordsRequest =
+                new ReadRecordsRequestUsingFilters.Builder<>(StepsRecord.class)
+                        .setAscending(false)
+                        .setPageSize(500)
+                        .build();
+
+        ReadTransactionRequest request =
+                getReadTransactionRequest(readRecordsRequest.toReadRecordsRequestParcel());
+
+        assertThat(request.getReadRequests()).hasSize(1);
+        assertThat(request.getPageToken()).isEqualTo(expectedToken);
+        assertThat(request.getPageSize()).isEqualTo(Optional.of(500));
+    }
+
+    @Test
+    public void createReadByFilterRequest_hasPageToken_correctPaginationInfo() {
+        PageTokenWrapper expectedToken = PageTokenWrapper.of(true, 9876, 2);
+        ReadRecordsRequestUsingFilters<StepsRecord> readRecordsRequest =
+                new ReadRecordsRequestUsingFilters.Builder<>(StepsRecord.class)
+                        .setPageToken(PageTokenUtil.encode(expectedToken))
+                        .setPageSize(500)
+                        .build();
+        ReadTransactionRequest request =
+                getReadTransactionRequest(readRecordsRequest.toReadRecordsRequestParcel());
+
+        assertThat(request.getReadRequests()).hasSize(1);
+        assertThat(request.getPageToken()).isEqualTo(expectedToken);
+        assertThat(request.getPageSize()).isEqualTo(Optional.of(500));
+    }
+
+    @Test
+    public void createReadByIdRequest_singleType_noPaginationInfo() {
+        ReadRecordsRequestUsingIds<StepsRecord> readRecordsRequest =
+                new ReadRecordsRequestUsingIds.Builder<>(StepsRecord.class)
+                        .addClientRecordId("id")
+                        .build();
+        ReadTransactionRequest request =
+                getReadTransactionRequest(readRecordsRequest.toReadRecordsRequestParcel());
+
+        assertThat(request.getReadRequests()).hasSize(1);
+        assertThat(request.getPageToken()).isNull();
+        assertThat(request.getPageSize()).isEqualTo(Optional.empty());
+    }
+
+    @Test
+    public void createReadByIdRequest_multipleType_noPaginationInfo() {
+        List<UUID> ramdonUuids = ImmutableList.of(UUID.randomUUID());
+        ReadTransactionRequest request =
+                getReadTransactionRequest(
+                        ImmutableMap.of(
+                                RecordTypeIdentifier.RECORD_TYPE_STEPS, ramdonUuids,
+                                RecordTypeIdentifier.RECORD_TYPE_BLOOD_PRESSURE, ramdonUuids));
+
+        assertThat(request.getReadRequests()).hasSize(2);
+        assertThat(request.getPageToken()).isNull();
+        assertThat(request.getPageSize()).isEqualTo(Optional.empty());
+    }
+}
diff --git a/tests/unittests/src/com/android/server/healthconnect/storage/utils/PageTokenUtilTest.java b/tests/unittests/src/com/android/server/healthconnect/storage/utils/PageTokenUtilTest.java
new file mode 100644
index 0000000..8ca7951
--- /dev/null
+++ b/tests/unittests/src/com/android/server/healthconnect/storage/utils/PageTokenUtilTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.server.healthconnect.storage.utils;
+
+import static android.health.connect.Constants.DEFAULT_LONG;
+
+import static com.android.server.healthconnect.storage.utils.PageTokenUtil.MAX_ALLOWED_TIME_MILLIS;
+import static com.android.server.healthconnect.storage.utils.PageTokenUtil.encode;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import static java.time.temporal.ChronoUnit.DAYS;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.Instant;
+
+@RunWith(AndroidJUnit4.class)
+public class PageTokenUtilTest {
+
+    @Test
+    public void encodeAndRetrieveIsAscending_expectCorrectResult() {
+        PageTokenWrapper wrapper =
+                PageTokenWrapper.of(/* isAscending= */ true, 1234, /* offset= */ 0);
+        long token = encode(wrapper);
+        assertThat(wrapper.isTimestampSet()).isTrue();
+        assertThat(decode(token, /* defaultIsAscending= */ false).isAscending()).isTrue();
+
+        wrapper = PageTokenWrapper.of(/* isAscending= */ false, 5678, /* offset= */ 0);
+        token = encode(wrapper);
+        assertThat(wrapper.isTimestampSet()).isTrue();
+        assertThat(decode(token, /* defaultIsAscending= */ true).isAscending()).isFalse();
+    }
+
+    @Test
+    public void encodeAndRetrieveTimestamp_expectCorrectResult() {
+        long nowTimeMillis = Instant.now().toEpochMilli();
+        PageTokenWrapper wrapper =
+                PageTokenWrapper.of(/* isAscending= */ false, nowTimeMillis, /* offset= */ 0);
+        long token = encode(wrapper);
+        assertThat(wrapper.isTimestampSet()).isTrue();
+        assertThat(decode(token).timeMillis()).isEqualTo(nowTimeMillis);
+
+        long futureTimeMillis = Instant.now().plus(36500, DAYS).toEpochMilli();
+        wrapper = PageTokenWrapper.of(/* isAscending= */ true, futureTimeMillis, /* offset= */ 0);
+        token = encode(wrapper);
+        assertThat(wrapper.isTimestampSet()).isTrue();
+        assertThat(decode(token).timeMillis()).isEqualTo(futureTimeMillis);
+
+        long pastTimeMillis = Instant.now().minus(3650, DAYS).toEpochMilli();
+        wrapper = PageTokenWrapper.of(/* isAscending= */ true, pastTimeMillis, /* offset= */ 0);
+        token = encode(wrapper);
+        assertThat(wrapper.isTimestampSet()).isTrue();
+        assertThat(decode(token).timeMillis()).isEqualTo(pastTimeMillis);
+
+        wrapper =
+                PageTokenWrapper.of(/* isAscending= */ true, /* timeMillis= */ 0, /* offset= */ 0);
+        token = encode(wrapper);
+        assertThat(wrapper.isTimestampSet()).isTrue();
+        assertThat(decode(token).timeMillis()).isEqualTo(0);
+
+        wrapper =
+                PageTokenWrapper.of(
+                        /* isAscending= */ true, MAX_ALLOWED_TIME_MILLIS, /* offset= */ 0);
+        token = encode(wrapper);
+        assertThat(wrapper.isTimestampSet()).isTrue();
+        assertThat(decode(token).timeMillis()).isEqualTo(MAX_ALLOWED_TIME_MILLIS);
+    }
+
+    @Test
+    public void encodeAndRetrieveOffset_expectCorrectResult() {
+        int maxOffset = (1 << 18) - 1;
+        int minOffset = 0;
+        long timestamp = Instant.now().toEpochMilli();
+
+        PageTokenWrapper wrapper =
+                PageTokenWrapper.of(/* isAscending= */ false, timestamp, maxOffset);
+        long token = encode(wrapper);
+        assertThat(wrapper.isTimestampSet()).isTrue();
+        assertThat(decode(token).offset()).isEqualTo(maxOffset);
+
+        wrapper = PageTokenWrapper.of(/* isAscending= */ true, timestamp, minOffset);
+        token = encode(wrapper);
+        assertThat(wrapper.isTimestampSet()).isTrue();
+        assertThat(decode(token).offset()).isEqualTo(minOffset);
+    }
+
+    @Test
+    public void decode_pageTokenNotSet_defaultIsAscendingUsed() {
+        PageTokenWrapper wrapper = decode(DEFAULT_LONG, /* defaultIsAscending= */ true);
+        assertThat(wrapper.isTimestampSet()).isFalse();
+        assertThat(wrapper.isAscending()).isTrue();
+
+        wrapper = decode(DEFAULT_LONG, /* defaultIsAscending= */ false);
+        assertThat(wrapper.isTimestampSet()).isFalse();
+        assertThat(wrapper.isAscending()).isFalse();
+    }
+
+    @Test
+    public void decode_invalidPageToken_throws() {
+        Throwable thrown =
+                assertThrows(
+                        IllegalArgumentException.class,
+                        () -> decode(-123, /* defaultIsAscending= */ true));
+        assertThat(thrown.getMessage()).isEqualTo("pageToken cannot be negative");
+    }
+
+    private static PageTokenWrapper decode(long pageToken) {
+        return decode(pageToken, /* defaultIsAscending= */ true);
+    }
+
+    private static PageTokenWrapper decode(long pageToken, boolean defaultIsAscending) {
+        return PageTokenUtil.decode(pageToken, defaultIsAscending);
+    }
+}
diff --git a/tests/unittests/src/com/android/server/healthconnect/storage/utils/PageTokenWrapperTest.java b/tests/unittests/src/com/android/server/healthconnect/storage/utils/PageTokenWrapperTest.java
new file mode 100644
index 0000000..9581c35
--- /dev/null
+++ b/tests/unittests/src/com/android/server/healthconnect/storage/utils/PageTokenWrapperTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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.server.healthconnect.storage.utils;
+
+import static com.android.server.healthconnect.storage.utils.PageTokenUtil.MAX_ALLOWED_OFFSET;
+import static com.android.server.healthconnect.storage.utils.PageTokenUtil.MAX_ALLOWED_TIME_MILLIS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class PageTokenWrapperTest {
+    @Test
+    public void of_createInstance() {
+        boolean isAscending = false;
+        long timeMillis = 123;
+        int offset = 456;
+        PageTokenWrapper wrapper = PageTokenWrapper.of(isAscending, timeMillis, offset);
+
+        assertThat(wrapper.isAscending()).isEqualTo(isAscending);
+        assertThat(wrapper.timeMillis()).isEqualTo(timeMillis);
+        assertThat(wrapper.offset()).isEqualTo(offset);
+        assertThat(wrapper.isTimestampSet()).isTrue();
+    }
+
+    @Test
+    public void of_isAscending_timestampNotSet() {
+        PageTokenWrapper wrapper = PageTokenWrapper.ofAscending(/* isAscending= */ true);
+        assertThat(wrapper.isAscending()).isTrue();
+        assertThat(wrapper.timeMillis()).isEqualTo(0);
+        assertThat(wrapper.offset()).isEqualTo(0);
+        assertThat(wrapper.isTimestampSet()).isFalse();
+    }
+
+    @Test
+    public void of_offsetTooLarge_setToMax() {
+        boolean isAscending = true;
+        PageTokenWrapper wrapper =
+                PageTokenWrapper.of(isAscending, /* timeMillis= */ 0, (int) MAX_ALLOWED_OFFSET + 1);
+        assertThat(wrapper.offset()).isEqualTo(MAX_ALLOWED_OFFSET);
+    }
+
+    @Test
+    public void of_invalidArgument_throws() {
+        boolean isAscending = true;
+        Throwable thrown;
+
+        thrown =
+                assertThrows(
+                        IllegalArgumentException.class,
+                        () ->
+                                PageTokenWrapper.of(
+                                        isAscending, /* timeMillis= */ -1, /* offset= */ 0));
+        assertThat(thrown.getMessage()).isEqualTo("timestamp can not be negative");
+
+        thrown =
+                assertThrows(
+                        IllegalArgumentException.class,
+                        () ->
+                                PageTokenWrapper.of(
+                                        isAscending, /* timeMillis= */ 0, /* offset= */ -1));
+        assertThat(thrown.getMessage()).isEqualTo("offset can not be negative");
+
+        thrown =
+                assertThrows(
+                        IllegalArgumentException.class,
+                        () ->
+                                PageTokenWrapper.of(
+                                        isAscending, MAX_ALLOWED_TIME_MILLIS + 1, /* offset= */ 0));
+        assertThat(thrown.getMessage()).isEqualTo("timestamp too large");
+    }
+
+    @Test
+    public void equals_sameValue_expectTrue() {
+        PageTokenWrapper wrapper1 =
+                PageTokenWrapper.of(
+                        /* isAscending= */ false, /* timeMillis= */ 1234, /* offset= */ 567);
+        PageTokenWrapper wrapper2 =
+                PageTokenWrapper.of(
+                        /* isAscending= */ false, /* timeMillis= */ 1234, /* offset= */ 567);
+
+        assertThat(wrapper1.equals(wrapper2)).isTrue();
+    }
+
+    @Test
+    public void equals_differentValue_expectFalse() {
+        PageTokenWrapper wrapper =
+                PageTokenWrapper.of(
+                        /* isAscending= */ false, /* timeMillis= */ 1234, /* offset= */ 567);
+        PageTokenWrapper differentIsAscending =
+                PageTokenWrapper.of(
+                        /* isAscending= */ true, /* timeMillis= */ 1234, /* offset= */ 567);
+        PageTokenWrapper differentTime =
+                PageTokenWrapper.of(
+                        /* isAscending= */ false, /* timeMillis= */ 123, /* offset= */ 567);
+        PageTokenWrapper differentOffset =
+                PageTokenWrapper.of(
+                        /* isAscending= */ false, /* timeMillis= */ 1234, /* offset= */ 5678);
+
+        assertThat(wrapper.equals(differentIsAscending)).isFalse();
+        assertThat(wrapper.equals(differentTime)).isFalse();
+        assertThat(wrapper.equals(differentOffset)).isFalse();
+    }
+}