Snap for 11418769 from 0398c2048a8efbd029efa0bd802d5ea87f24fc93 to mainline-os-statsd-release

Change-Id: I2da02dcb1cb55d8923461dd3c3995a903d44b93a
diff --git a/apk/res/layout/widget_banner_preference.xml b/apk/res/layout/widget_banner_preference.xml
index f6a80b6..8b2e0bd 100644
--- a/apk/res/layout/widget_banner_preference.xml
+++ b/apk/res/layout/widget_banner_preference.xml
@@ -64,17 +64,40 @@
         android:layout_height="wrap_content"
         android:textAppearance="?attr/textAppearanceSummary"/>
 
-    <Button
-        android:id="@+id/banner_button"
-        android:paddingTop="@dimen/spacing_small"
-        android:layout_width="wrap_content"
+    <LinearLayout
+        android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_gravity="end"
-        style="@style/Widget.HealthConnect.Button.Borderless"
-        android:textColor="?android:attr/textColorPrimary"
-        android:visibility="gone"
-        />
+        android:orientation="horizontal">
 
+        <Space
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:layout_weight="1"/>
 
+        <Button
+            android:id="@+id/banner_secondary_button"
+            android:paddingVertical="@dimen/spacing_small"
+            android:paddingHorizontal="@dimen/spacing_normal"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="end"
+            style="@style/Widget.HealthConnect.Button.Borderless"
+            android:textColor="?android:attr/textColorPrimary"
+            android:visibility="gone"
+            />
+        <Button
+            android:id="@+id/banner_primary_button"
+            android:paddingVertical="@dimen/spacing_small"
+            android:paddingHorizontal="@dimen/spacing_normal"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="end"
+            style="@style/Widget.HealthConnect.Button.Borderless"
+            android:textColor="?android:attr/textColorPrimary"
+            android:visibility="gone"
+            />
+
+    </LinearLayout>
 
 </LinearLayout>
\ No newline at end of file
diff --git a/apk/res/values/strings.xml b/apk/res/values/strings.xml
index ac5369c..3f9d21d 100644
--- a/apk/res/values/strings.xml
+++ b/apk/res/values/strings.xml
@@ -365,6 +365,7 @@
     <string name="connected_apps_text" description="Description text shown on screen for managing which apps have access to health and fitness data. Note: Health Connect is the brand [CHAR_LIMIT=NONE]">Control which apps can access data stored in Health&#160;Connect. Tap an app to review the data it can read or write.</string>
     <string name="connected_apps_section_title" description="Title of section containing all the connected apps [CHAR_LIMIT=40]">Allowed access</string>
     <string name="not_connected_apps_section_title" description="Title of section containing all the not-connected apps [CHAR_LIMIT=40]">Not allowed access</string>
+    <string name="needs_updating_apps_section_title" description="Title of section containing all the apps that need updating in order to work with Health Connect [CHAR_LIMIT=40]">Needs updating</string>
     <string name="settings_and_help_header" description="Title shown for the section of settings and help [CHAR_LIMIT=70]">Settings &amp; help</string>
     <string name="disconnect_all_apps" description="Button text for removing access to health and fitness data for all apps [CHAR_LIMIT=40]">Remove access for all apps</string>
     <string name="manage_permissions_read_header" description="Header for list of permissions an app is allowed read access to [CHAR_LIMIT=40]">Allowed to read</string>
@@ -1062,9 +1063,10 @@
 
     <!-- region App update needed banner -->
     <string name="app_update_needed_banner_title" description="Title of banner prompting the user to update an app which is not currently working on this Android version.">App update needed</string>
-    <string name="app_update_needed_banner_description_single" description="Description of banner prompting the user to update an app which is not currently working on this Android version."><xliff:g example="Run Tracker" id="app_name">%1$s</xliff:g> needs to be up-to-date in order to keep working with Health&#160;Connect</string>
-    <string name="app_update_needed_banner_description_multiple" description="Description of banner prompting the user to update an app which is not currently working on this Android version."> Some apps need to be up-to-date in order to keep working with Health&#160;Connect</string>
+    <string name="app_update_needed_banner_description_single" description="Description of banner prompting the user to update an app which is not currently working on this Android version."><xliff:g example="Run Tracker" id="app_name">%1$s</xliff:g> needs to be updated to continue syncing with Health&#160;Connect</string>
+    <string name="app_update_needed_banner_description_multiple" description="Description of banner prompting the user to update an app which is not currently working on this Android version."> Some apps need to be updated to continue syncing with Health&#160;Connect</string>
     <string name="app_update_needed_banner_button" description="Button of banner prompting the user to update an app which is not currently working on this Android version.">Check for updates</string>
+    <string name="app_update_needed_banner_learn_more_button" description="Button of banner prompting the user to update an app which is not currently working on this Android version.">Learn more</string>
     <!--  endregion -->
 
     <!-- region Migration pending permissions dialog -->
diff --git a/apk/res/xml/connected_apps_screen.xml b/apk/res/xml/connected_apps_screen.xml
index b375804..202b15e 100644
--- a/apk/res/xml/connected_apps_screen.xml
+++ b/apk/res/xml/connected_apps_screen.xml
@@ -34,14 +34,19 @@
         android:key="not_allowed_apps"
         android:title="@string/not_connected_apps_section_title"
         app:isPreferenceVisible="false"/>
-    <com.android.healthconnect.controller.shared.LongSummaryPreferenceCategory
+    <PreferenceCategory
         android:order="4"
+        android:key="need_update_apps"
+        android:title="@string/needs_updating_apps_section_title"
+        app:isPreferenceVisible="false"/>
+    <com.android.healthconnect.controller.shared.LongSummaryPreferenceCategory
+        android:order="5"
         android:key="inactive_apps"
         android:title="@string/inactive_apps_section_title"
         android:summary="@string/inactive_apps_section_message"
         app:isPreferenceVisible="false"/>
     <PreferenceCategory
-        android:order="5"
+        android:order="6"
         android:key="settings_and_help"
         android:title="@string/settings_and_help_header"
         app:isPreferenceVisible="false">
diff --git a/apk/src/com/android/healthconnect/controller/home/HomeFragment.kt b/apk/src/com/android/healthconnect/controller/home/HomeFragment.kt
index 3500d39..3a38dd5 100644
--- a/apk/src/com/android/healthconnect/controller/home/HomeFragment.kt
+++ b/apk/src/com/android/healthconnect/controller/home/HomeFragment.kt
@@ -218,12 +218,12 @@
 
     private fun getMigrationBanner(): BannerPreference {
         return BannerPreference(requireContext()).also {
-            it.setButton(resources.getString(R.string.resume_migration_banner_button))
+            it.setPrimaryButton(resources.getString(R.string.resume_migration_banner_button))
             it.title = resources.getString(R.string.resume_migration_banner_title)
             it.key = MIGRATION_BANNER_PREFERENCE_KEY
             it.summary = migrationBannerSummary
             it.setIcon(R.drawable.ic_settings_alert)
-            it.setButtonOnClickListener {
+            it.setPrimaryButtonOnClickListener {
                 findNavController().navigate(R.id.action_homeFragment_to_migrationActivity)
             }
             it.order = 1
diff --git a/apk/src/com/android/healthconnect/controller/permissions/connectedapps/ConnectedAppsFragment.kt b/apk/src/com/android/healthconnect/controller/permissions/connectedapps/ConnectedAppsFragment.kt
index 6c59a29..73251b3 100644
--- a/apk/src/com/android/healthconnect/controller/permissions/connectedapps/ConnectedAppsFragment.kt
+++ b/apk/src/com/android/healthconnect/controller/permissions/connectedapps/ConnectedAppsFragment.kt
@@ -15,11 +15,13 @@
  */
 package com.android.healthconnect.controller.permissions.connectedapps
 
+import android.content.Context
 import android.content.Intent
 import android.content.Intent.EXTRA_PACKAGE_NAME
 import android.os.Bundle
 import android.view.MenuItem
 import android.view.View
+import android.view.View.GONE
 import android.widget.Toast
 import androidx.annotation.StringRes
 import androidx.appcompat.app.AlertDialog
@@ -35,6 +37,7 @@
 import com.android.healthconnect.controller.deletion.DeletionConstants.FRAGMENT_TAG_DELETION
 import com.android.healthconnect.controller.deletion.DeletionFragment
 import com.android.healthconnect.controller.deletion.DeletionType
+import com.android.healthconnect.controller.migration.AppUpdateRequiredFragment
 import com.android.healthconnect.controller.permissions.connectedapps.ConnectedAppsViewModel.DisconnectAllState
 import com.android.healthconnect.controller.permissions.shared.Constants.EXTRA_APP_NAME
 import com.android.healthconnect.controller.permissions.shared.HelpAndFeedbackFragment.Companion.APP_INTEGRATION_REQUEST_BUCKET_ID
@@ -43,13 +46,16 @@
 import com.android.healthconnect.controller.shared.app.ConnectedAppStatus.ALLOWED
 import com.android.healthconnect.controller.shared.app.ConnectedAppStatus.DENIED
 import com.android.healthconnect.controller.shared.app.ConnectedAppStatus.INACTIVE
+import com.android.healthconnect.controller.shared.app.ConnectedAppStatus.NEEDS_UPDATE
 import com.android.healthconnect.controller.shared.dialog.AlertDialogBuilder
 import com.android.healthconnect.controller.shared.inactiveapp.InactiveAppPreference
 import com.android.healthconnect.controller.shared.preference.BannerPreference
 import com.android.healthconnect.controller.shared.preference.HealthPreference
 import com.android.healthconnect.controller.shared.preference.HealthPreferenceFragment
+import com.android.healthconnect.controller.utils.AppStoreUtils
 import com.android.healthconnect.controller.utils.AttributeResolver
 import com.android.healthconnect.controller.utils.DeviceInfoUtils
+import com.android.healthconnect.controller.utils.NavigationUtils
 import com.android.healthconnect.controller.utils.dismissLoadingDialog
 import com.android.healthconnect.controller.utils.logging.AppPermissionsElement
 import com.android.healthconnect.controller.utils.logging.DisconnectAllAppsDialogElement
@@ -72,8 +78,11 @@
         const val ALLOWED_APPS_CATEGORY = "allowed_apps"
         private const val NOT_ALLOWED_APPS = "not_allowed_apps"
         private const val INACTIVE_APPS = "inactive_apps"
+        private const val NEED_UPDATE_APPS = "need_update_apps"
         private const val THINGS_TO_TRY = "things_to_try_app_permissions_screen"
         private const val SETTINGS_AND_HELP = "settings_and_help"
+        private const val APP_UPDATE_NEEDED_BANNER_SEEN = "app_update_banner_seen"
+        private const val BANNER_PREFERENCE_KEY = "banner_preference"
     }
 
     init {
@@ -81,8 +90,10 @@
     }
 
     @Inject lateinit var logger: HealthConnectLogger
-
+    @Inject lateinit var appStoreUtils: AppStoreUtils
     @Inject lateinit var deviceInfoUtils: DeviceInfoUtils
+    @Inject lateinit var navigationUtils: NavigationUtils
+
     private val viewModel: ConnectedAppsViewModel by viewModels()
     private lateinit var searchMenuItem: MenuItem
     private lateinit var removeAllAppsDialog: AlertDialog
@@ -99,6 +110,10 @@
         preferenceScreen.findPreference(NOT_ALLOWED_APPS)
     }
 
+    private val mNeedUpdateAppsCategory: PreferenceGroup? by lazy {
+        preferenceScreen.findPreference(NEED_UPDATE_APPS)
+    }
+
     private val mInactiveAppsCategory: PreferenceGroup? by lazy {
         preferenceScreen.findPreference(INACTIVE_APPS)
     }
@@ -198,6 +213,7 @@
                 val connectedAppsGroup = connectedApps.groupBy { it.status }
                 val allowedApps = connectedAppsGroup[ALLOWED].orEmpty()
                 val notAllowedApps = connectedAppsGroup[DENIED].orEmpty()
+                val needUpdateApps = connectedAppsGroup[NEEDS_UPDATE].orEmpty()
                 val activeApps: MutableList<ConnectedAppMetadata> = allowedApps.toMutableList()
                 activeApps.addAll(notAllowedApps)
                 createRemoveAllAppsAccessDialog(activeApps)
@@ -219,6 +235,7 @@
                 updateAllowedApps(allowedApps)
                 updateDeniedApps(notAllowedApps)
                 updateInactiveApps(connectedAppsGroup[INACTIVE].orEmpty())
+                updateNeedUpdateApps(needUpdateApps)
 
                 viewModel.alertDialogActive.observe(viewLifecycleOwner) { state ->
                     if (state) {
@@ -257,6 +274,40 @@
         }
     }
 
+    private fun updateNeedUpdateApps(appsList: List<ConnectedAppMetadata>) {
+        if (appsList.isEmpty()) {
+            mNeedUpdateAppsCategory?.isVisible = false
+        } else {
+            mNeedUpdateAppsCategory?.isVisible = true
+            appsList.forEach { app ->
+                val packageName =
+                    getString(
+                        resources.getIdentifier(
+                            AppUpdateRequiredFragment.HC_PACKAGE_NAME_CONFIG_NAME, null, null))
+
+                val intent = appStoreUtils.getAppStoreLink(packageName)
+                if (intent == null) {
+                    mNeedUpdateAppsCategory?.addPreference(
+                        getAppPreference(app).also { it.isSelectable = false })
+                } else {
+                    mNeedUpdateAppsCategory?.addPreference(
+                        getAppPreference(app) { navigationUtils.startActivity(this, intent) })
+                }
+            }
+
+            val sharedPreference =
+                requireActivity()
+                    .getSharedPreferences("USER_ACTIVITY_TRACKER", Context.MODE_PRIVATE)
+            val bannerSeen = sharedPreference.getBoolean(APP_UPDATE_NEEDED_BANNER_SEEN, false)
+
+            if (!bannerSeen) {
+                val banner = getAppUpdateNeededBanner(appsList)
+                preferenceScreen.removePreferenceRecursively(BANNER_PREFERENCE_KEY)
+                preferenceScreen.addPreference(banner)
+            }
+        }
+    }
+
     private fun updateAllowedApps(appsList: List<ConnectedAppMetadata>) {
         if (appsList.isEmpty()) {
             mAllowedAppsCategory?.addPreference(getNoAppsPreference(R.string.no_apps_allowed))
@@ -376,18 +427,49 @@
 
     // TODO (b/275602235) Use this banner to indicate one or more apps need updating to work with
     // Android U
-    private fun getAppUpdateNeededBanner(): BannerPreference {
+    private fun getAppUpdateNeededBanner(appsList: List<ConnectedAppMetadata>): BannerPreference {
         return BannerPreference(requireContext()).also { banner ->
-            banner.setButton(resources.getString(R.string.app_update_needed_banner_button))
+            banner.setPrimaryButton(resources.getString(R.string.app_update_needed_banner_button))
+            banner.setSecondaryButton(
+                resources.getString(R.string.app_update_needed_banner_learn_more_button))
             banner.title = resources.getString(R.string.app_update_needed_banner_title)
-            banner.summary =
-                resources.getString(R.string.app_update_needed_banner_description_multiple)
+
+            if (appsList.size > 1) {
+                banner.summary =
+                    resources.getString(R.string.app_update_needed_banner_description_multiple)
+            } else {
+                banner.summary =
+                    resources.getString(
+                        R.string.app_update_needed_banner_description_single,
+                        appsList[0].appMetadata.appName)
+            }
+
+            banner.key = BANNER_PREFERENCE_KEY
             banner.setIcon(R.drawable.ic_apps_outage)
-            banner.setButtonOnClickListener {
-                // TODO (b/275602235) navigate to play store
+            banner.order = 1
+            if (deviceInfoUtils.isPlayStoreAvailable(requireContext())) {
+                banner.setPrimaryButtonOnClickListener {
+                    navigationUtils.navigate(this, R.id.action_connected_apps_to_updated_apps)
+                    true
+                }
+            } else {
+                banner.setPrimaryButtonVisibility(GONE)
+            }
+
+            banner.setSecondaryButtonOnClickListener {
+                deviceInfoUtils.openHCGetStartedLink(requireActivity())
             }
             banner.setIsDismissable(true)
-            banner.setDismissAction { preferenceScreen.removePreference(banner) }
+            banner.setDismissAction {
+                val sharedPreference =
+                    requireActivity()
+                        .getSharedPreferences("USER_ACTIVITY_TRACKER", Context.MODE_PRIVATE)
+                sharedPreference.edit().apply {
+                    putBoolean(APP_UPDATE_NEEDED_BANNER_SEEN, true)
+                    apply()
+                }
+                preferenceScreen.removePreference(banner)
+            }
         }
     }
 
@@ -410,6 +492,7 @@
     private fun setAppAndSettingsCategoriesVisibility(isVisible: Boolean) {
         mInactiveAppsCategory?.isVisible = isVisible
         mAllowedAppsCategory?.isVisible = isVisible
+        mNeedUpdateAppsCategory?.isVisible = isVisible
         mNotAllowedAppsCategory?.isVisible = isVisible
         mSettingsAndHelpCategory?.isVisible = isVisible
     }
@@ -418,6 +501,7 @@
         mThingsToTryCategory?.removeAll()
         mAllowedAppsCategory?.removeAll()
         mNotAllowedAppsCategory?.removeAll()
+        mNeedUpdateAppsCategory?.removeAll()
         mInactiveAppsCategory?.removeAll()
         mSettingsAndHelpCategory?.removeAll()
     }
diff --git a/apk/src/com/android/healthconnect/controller/permissions/connectedapps/LoadHealthPermissionApps.kt b/apk/src/com/android/healthconnect/controller/permissions/connectedapps/LoadHealthPermissionApps.kt
index aaf6a5a..32d6a97 100644
--- a/apk/src/com/android/healthconnect/controller/permissions/connectedapps/LoadHealthPermissionApps.kt
+++ b/apk/src/com/android/healthconnect/controller/permissions/connectedapps/LoadHealthPermissionApps.kt
@@ -15,14 +15,14 @@
  */
 package com.android.healthconnect.controller.permissions.connectedapps
 
-import com.android.healthconnect.controller.permissions.api.GetGrantedHealthPermissionsUseCase
-import com.android.healthconnect.controller.permissions.shared.QueryRecentAccessLogsUseCase
+import com.android.healthconnect.controller.permissions.api.IGetGrantedHealthPermissionsUseCase
+import com.android.healthconnect.controller.permissions.shared.IQueryRecentAccessLogsUseCase
 import com.android.healthconnect.controller.service.IoDispatcher
 import com.android.healthconnect.controller.shared.HealthPermissionReader
 import com.android.healthconnect.controller.shared.app.AppInfoReader
 import com.android.healthconnect.controller.shared.app.ConnectedAppMetadata
 import com.android.healthconnect.controller.shared.app.ConnectedAppStatus
-import com.android.healthconnect.controller.shared.app.GetContributorAppInfoUseCase
+import com.android.healthconnect.controller.shared.app.IGetContributorAppInfoUseCase
 import javax.inject.Inject
 import javax.inject.Singleton
 import kotlinx.coroutines.CoroutineDispatcher
@@ -33,9 +33,9 @@
 @Inject
 constructor(
     private val healthPermissionReader: HealthPermissionReader,
-    private val loadGrantedHealthPermissionsUseCase: GetGrantedHealthPermissionsUseCase,
-    private val getContributorAppInfoUseCase: GetContributorAppInfoUseCase,
-    private val queryRecentAccessLogsUseCase: QueryRecentAccessLogsUseCase,
+    private val loadGrantedHealthPermissionsUseCase: IGetGrantedHealthPermissionsUseCase,
+    private val getContributorAppInfoUseCase: IGetContributorAppInfoUseCase,
+    private val queryRecentAccessLogsUseCase: IQueryRecentAccessLogsUseCase,
     private val appInfoReader: AppInfoReader,
     @IoDispatcher private val dispatcher: CoroutineDispatcher,
 ) : ILoadHealthPermissionApps {
@@ -47,6 +47,8 @@
             val appsWithData = getContributorAppInfoUseCase.invoke()
             val connectedApps = mutableListOf<ConnectedAppMetadata>()
             val recentAccess = queryRecentAccessLogsUseCase.invoke()
+            val appsWithOldHealthPermissions =
+                healthPermissionReader.getAppsWithOldHealthPermissions()
 
             connectedApps.addAll(
                 appsWithHealthPermissions.map { packageName ->
@@ -66,7 +68,19 @@
                     .filter { !appsWithHealthPermissions.contains(it.packageName) }
                     .map { ConnectedAppMetadata(it, ConnectedAppStatus.INACTIVE) }
 
+            val appsThatNeedUpdating =
+                appsWithOldHealthPermissions
+                    .map { packageName ->
+                        val metadata = appInfoReader.getAppMetadata(packageName)
+                        ConnectedAppMetadata(
+                            metadata,
+                            ConnectedAppStatus.NEEDS_UPDATE,
+                            recentAccess[metadata.packageName])
+                    }
+                    .filter { !appsWithHealthPermissions.contains(it.appMetadata.packageName) }
+
             connectedApps.addAll(inactiveApps)
+            connectedApps.addAll(appsThatNeedUpdating)
             connectedApps
         }
 }
diff --git a/apk/src/com/android/healthconnect/controller/permissions/shared/QueryRecentAccessLogsUseCase.kt b/apk/src/com/android/healthconnect/controller/permissions/shared/QueryRecentAccessLogsUseCase.kt
index 5a757ef..ee88d53 100644
--- a/apk/src/com/android/healthconnect/controller/permissions/shared/QueryRecentAccessLogsUseCase.kt
+++ b/apk/src/com/android/healthconnect/controller/permissions/shared/QueryRecentAccessLogsUseCase.kt
@@ -33,17 +33,17 @@
  */
 package com.android.healthconnect.controller.permissions.shared
 
-import android.health.connect.accesslog.AccessLog
 import android.health.connect.HealthConnectManager
+import android.health.connect.accesslog.AccessLog
 import android.util.Log
 import androidx.core.os.asOutcomeReceiver
 import com.android.healthconnect.controller.service.IoDispatcher
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.suspendCancellableCoroutine
-import kotlinx.coroutines.withContext
 import java.time.Instant
 import javax.inject.Inject
 import javax.inject.Singleton
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
 
 /** Query recent access logs for health connect connected apps. */
 @Singleton
@@ -52,13 +52,13 @@
 constructor(
     private val manager: HealthConnectManager,
     @IoDispatcher private val dispatcher: CoroutineDispatcher
-) {
+) : IQueryRecentAccessLogsUseCase {
 
     companion object {
         private const val TAG = "QueryRecentAccessLogsUseCase"
     }
 
-    suspend fun invoke(): Map<String, Instant> =
+    override suspend fun invoke(): Map<String, Instant> =
         withContext(dispatcher) {
             try {
                 val accessLogs =
@@ -72,3 +72,7 @@
             }
         }
 }
+
+interface IQueryRecentAccessLogsUseCase {
+    suspend fun invoke(): Map<String, Instant>
+}
diff --git a/apk/src/com/android/healthconnect/controller/service/UseCaseModule.kt b/apk/src/com/android/healthconnect/controller/service/UseCaseModule.kt
index 5d18819..37abf43 100644
--- a/apk/src/com/android/healthconnect/controller/service/UseCaseModule.kt
+++ b/apk/src/com/android/healthconnect/controller/service/UseCaseModule.kt
@@ -15,6 +15,7 @@
  */
 package com.android.healthconnect.controller.service
 
+import android.content.Context
 import android.health.connect.HealthConnectManager
 import com.android.healthconnect.controller.data.access.ILoadAccessUseCase
 import com.android.healthconnect.controller.data.access.ILoadPermissionTypeContributorAppsUseCase
@@ -49,6 +50,7 @@
 import com.android.healthconnect.controller.permissions.api.IGetGrantedHealthPermissionsUseCase
 import com.android.healthconnect.controller.permissions.connectedapps.ILoadHealthPermissionApps
 import com.android.healthconnect.controller.permissions.connectedapps.LoadHealthPermissionApps
+import com.android.healthconnect.controller.permissions.shared.IQueryRecentAccessLogsUseCase
 import com.android.healthconnect.controller.permissions.shared.QueryRecentAccessLogsUseCase
 import com.android.healthconnect.controller.permissiontypes.api.ILoadPriorityListUseCase
 import com.android.healthconnect.controller.permissiontypes.api.LoadPriorityListUseCase
@@ -57,10 +59,12 @@
 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.shared.app.IGetContributorAppInfoUseCase
 import com.android.healthconnect.controller.utils.TimeSource
 import dagger.Module
 import dagger.Provides
 import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
 import dagger.hilt.components.SingletonComponent
 import kotlinx.coroutines.CoroutineDispatcher
 
@@ -248,4 +252,21 @@
     ): IGetGrantedHealthPermissionsUseCase {
         return GetGrantedHealthPermissionsUseCase(healthPermissionManager)
     }
+
+    @Provides
+    fun providesQueryRecentAccessLogsUseCase(
+        healthConnectManager: HealthConnectManager,
+        @IoDispatcher dispatcher: CoroutineDispatcher
+    ): IQueryRecentAccessLogsUseCase {
+        return QueryRecentAccessLogsUseCase(healthConnectManager, dispatcher)
+    }
+
+    @Provides
+    fun providesGetContributorAppInfoUseCase(
+        healthConnectManager: HealthConnectManager,
+        @ApplicationContext context: Context,
+        @IoDispatcher dispatcher: CoroutineDispatcher
+    ): IGetContributorAppInfoUseCase {
+        return GetContributorAppInfoUseCase(healthConnectManager, context, dispatcher)
+    }
 }
diff --git a/apk/src/com/android/healthconnect/controller/shared/HealthPermissionReader.kt b/apk/src/com/android/healthconnect/controller/shared/HealthPermissionReader.kt
index 1b46721..5614a43 100644
--- a/apk/src/com/android/healthconnect/controller/shared/HealthPermissionReader.kt
+++ b/apk/src/com/android/healthconnect/controller/shared/HealthPermissionReader.kt
@@ -63,7 +63,7 @@
             )
     }
 
-    suspend fun getAppsWithHealthPermissions(): List<String> {
+    fun getAppsWithHealthPermissions(): List<String> {
         return try {
             val appsWithDeclaredIntent =
                 context.packageManager
@@ -78,7 +78,31 @@
         }
     }
 
-    suspend fun getDeclaredPermissions(packageName: String): List<HealthPermission> {
+    /**
+     * Identifies apps that have the old permissions declared - they need to update before
+     * continuing to sync with Health Connect.
+     */
+    fun getAppsWithOldHealthPermissions(): List<String> {
+        return try {
+            val oldPermissionsRationale = "androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE"
+            val oldPermissionsMetaDataKey = "health_permissions"
+            val intent = Intent(oldPermissionsRationale)
+            val resolveInfoList =
+                context.packageManager
+                    .queryIntentActivities(intent, PackageManager.GET_META_DATA)
+                    .filter { resolveInfo -> resolveInfo.activityInfo != null }
+                    .filter { resolveInfo -> resolveInfo.activityInfo.metaData != null }
+                    .filter { resolveInfo ->
+                        resolveInfo.activityInfo.metaData.getInt(oldPermissionsMetaDataKey) != -1
+                    }
+
+            resolveInfoList.map { it.activityInfo.packageName }.distinct()
+        } catch (e: NameNotFoundException) {
+            emptyList()
+        }
+    }
+
+    fun getDeclaredPermissions(packageName: String): List<HealthPermission> {
         return try {
             val appInfo =
                 context.packageManager.getPackageInfo(
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 d1c2303..1d3e699 100644
--- a/apk/src/com/android/healthconnect/controller/shared/app/ConnectedAppMetadata.kt
+++ b/apk/src/com/android/healthconnect/controller/shared/app/ConnectedAppMetadata.kt
@@ -29,5 +29,6 @@
 enum class ConnectedAppStatus {
     ALLOWED,
     DENIED,
-    INACTIVE
+    INACTIVE,
+    NEEDS_UPDATE
 }
diff --git a/apk/src/com/android/healthconnect/controller/shared/app/GetContributorAppInfoUseCase.kt b/apk/src/com/android/healthconnect/controller/shared/app/GetContributorAppInfoUseCase.kt
index 9dfbdd2..eea18e0 100644
--- a/apk/src/com/android/healthconnect/controller/shared/app/GetContributorAppInfoUseCase.kt
+++ b/apk/src/com/android/healthconnect/controller/shared/app/GetContributorAppInfoUseCase.kt
@@ -57,12 +57,12 @@
     private val healthConnectManager: HealthConnectManager,
     @ApplicationContext private val context: Context,
     @IoDispatcher private val dispatcher: CoroutineDispatcher
-) {
+) : IGetContributorAppInfoUseCase {
     companion object {
         private const val TAG = "GetContributorAppInfo"
     }
 
-    suspend fun invoke(): Map<String, AppMetadata> =
+    override suspend fun invoke(): Map<String, AppMetadata> =
         withContext(dispatcher) {
             try {
                 val appInfoList =
@@ -81,7 +81,8 @@
     private fun toAppMetadata(appInfo: AppInfo): AppMetadata {
         return AppMetadata(
             packageName = appInfo.packageName,
-            appName = appInfo.name
+            appName =
+                appInfo.name
                     ?: appInfo.packageName, // default to package name if appInfo name is null
             icon = getIcon(appInfo.icon))
     }
@@ -90,3 +91,7 @@
         return bitmap?.let { BitmapDrawable(context.resources, it) }
     }
 }
+
+interface IGetContributorAppInfoUseCase {
+    suspend operator fun invoke(): Map<String, AppMetadata>
+}
diff --git a/apk/src/com/android/healthconnect/controller/shared/preference/BannerPreference.kt b/apk/src/com/android/healthconnect/controller/shared/preference/BannerPreference.kt
index 93b110e..02513a5 100644
--- a/apk/src/com/android/healthconnect/controller/shared/preference/BannerPreference.kt
+++ b/apk/src/com/android/healthconnect/controller/shared/preference/BannerPreference.kt
@@ -29,12 +29,17 @@
     private lateinit var bannerIcon: ImageView
     private lateinit var bannerTitle: TextView
     private lateinit var bannerMessage: TextView
-    private lateinit var bannerButton: Button
+    private lateinit var bannerPrimaryButton: Button
+    private lateinit var bannerSecondaryButton: Button
 
     private var dismissButton: ImageView? = null
 
-    private var buttonText: String? = null
-    private var buttonAction: OnClickListener? = null
+    private var buttonPrimaryText: String? = null
+    private var buttonPrimaryAction: OnClickListener? = null
+    private var buttonSecondaryText: String? = null
+    private var buttonSecondaryAction: OnClickListener? = null
+    private var buttonPrimaryVisibility = View.VISIBLE
+    private var buttonSecondaryVisibility = View.VISIBLE
     private var isDismissable = false
     private var dismissAction: OnClickListener? = null
 
@@ -43,12 +48,28 @@
         isSelectable = false
     }
 
-    fun setButton(buttonText: String) {
-        this.buttonText = buttonText
+    fun setPrimaryButton(buttonText: String) {
+        this.buttonPrimaryText = buttonText
     }
 
-    fun setButtonOnClickListener(onClickListener: OnClickListener?) {
-        this.buttonAction = onClickListener
+    fun setPrimaryButtonOnClickListener(onClickListener: OnClickListener?) {
+        this.buttonPrimaryAction = onClickListener
+    }
+
+    fun setPrimaryButtonVisibility(visibility: Int) {
+        this.buttonPrimaryVisibility = visibility
+    }
+
+    fun setSecondaryButton(buttonText: String) {
+        this.buttonSecondaryText = buttonText
+    }
+
+    fun setSecondaryButtonOnClickListener(onClickListener: OnClickListener?) {
+        this.buttonSecondaryAction = onClickListener
+    }
+
+    fun setSecondaryButtonVisibility(visibility: Int) {
+        this.buttonSecondaryVisibility = visibility
     }
 
     fun setIsDismissable(isDismissable: Boolean) {
@@ -64,19 +85,29 @@
         bannerIcon = holder.findViewById(R.id.banner_icon) as ImageView
         bannerTitle = holder.findViewById(R.id.banner_title) as TextView
         bannerMessage = holder.findViewById(R.id.banner_message) as TextView
-        bannerButton = holder.findViewById(R.id.banner_button) as Button
+        bannerPrimaryButton = holder.findViewById(R.id.banner_primary_button) as Button
+        bannerSecondaryButton = holder.findViewById(R.id.banner_secondary_button) as Button
 
         bannerTitle.text = title
         bannerMessage.text = summary
         bannerIcon.background = icon
 
         // set button text and visibility
-        buttonText?.let {
-            bannerButton.text = it
-            bannerButton.visibility = View.VISIBLE
-            bannerButton.setOnClickListener(buttonAction)
+        buttonPrimaryText?.let {
+            bannerPrimaryButton.text = it
+            bannerPrimaryButton.visibility = View.VISIBLE
+            bannerPrimaryButton.setOnClickListener(buttonPrimaryAction)
         }
 
+        buttonSecondaryText?.let {
+            bannerSecondaryButton.text = it
+            bannerSecondaryButton.visibility = View.VISIBLE
+            bannerSecondaryButton.setOnClickListener(buttonSecondaryAction)
+        }
+
+        bannerPrimaryButton.visibility = buttonPrimaryVisibility
+        bannerSecondaryButton.visibility = buttonSecondaryVisibility
+
         if (isDismissable) {
             dismissButton = holder.findViewById(R.id.dismiss_button) as ImageView
             dismissButton?.visibility = View.VISIBLE
diff --git a/apk/tests/Android.bp b/apk/tests/Android.bp
index d42a49d..c9b8c04 100644
--- a/apk/tests/Android.bp
+++ b/apk/tests/Android.bp
@@ -102,5 +102,6 @@
         ":HealthConnectUITestApp",
         ":HealthConnectUITestApp2",
         ":UnsupportedTestApp",
+        ":OldPermissionsTestApp",
     ],
 }
diff --git a/apk/tests/AndroidTest.xml b/apk/tests/AndroidTest.xml
index 613f053..fbea81a 100644
--- a/apk/tests/AndroidTest.xml
+++ b/apk/tests/AndroidTest.xml
@@ -30,6 +30,7 @@
     <option name="test-file-name" value="HealthConnectUITestApp.apk"/>
     <option name="test-file-name" value="HealthConnectUITestApp2.apk"/>
     <option name="test-file-name" value="UnsupportedTestApp.apk"/>
+    <option name="test-file-name" value="OldPermissionsTestApp.apk"/>
   </target_preparer>
   <test class="com.android.tradefed.testtype.AndroidJUnitTest">
     <option name="runner" value="com.android.healthconnect.controller.tests.HiltTestRunner"/>
diff --git a/apk/tests/OldPermissionsTestApp/Android.bp b/apk/tests/OldPermissionsTestApp/Android.bp
new file mode 100644
index 0000000..bec63b8
--- /dev/null
+++ b/apk/tests/OldPermissionsTestApp/Android.bp
@@ -0,0 +1,48 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+filegroup {
+    name: "healthconnect-ui-testapp4-srcs",
+    srcs: [
+        "src/**/*.java",
+    ],
+    visibility: [
+        "//packages/modules/HealthFitness:__subpackages__"
+    ],
+}
+
+android_test_helper_app {
+    name: "OldPermissionsTestApp",
+
+    libs: [
+        "android.test.base",
+        "android.test.runner",
+    ],
+    resource_dirs: ["res"],
+
+    srcs: [":healthconnect-ui-testapp4-srcs"],
+
+    test_suites: [
+        "device-tests",
+        "general-tests",
+    ],
+
+    target_sdk_version: "34",
+    min_sdk_version: "34",
+
+}
diff --git a/apk/tests/OldPermissionsTestApp/AndroidManifest.xml b/apk/tests/OldPermissionsTestApp/AndroidManifest.xml
new file mode 100644
index 0000000..69849d6
--- /dev/null
+++ b/apk/tests/OldPermissionsTestApp/AndroidManifest.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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.controller.test.app4">
+
+    <application
+        android:label="Old permissions HC app">
+        <uses-library android:name="android.test.runner"/>
+
+        <activity android:name=".MainActivity"
+                  android:label="MainActivity"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
+            </intent-filter>
+
+            <intent-filter>
+                <action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
+                    <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+
+            <meta-data
+                android:name="health_permissions"
+                android:resource="@array/health_permissions"/>
+
+        </activity>
+
+    </application>
+</manifest>
diff --git a/apk/tests/OldPermissionsTestApp/res/values/health_permissions.xml b/apk/tests/OldPermissionsTestApp/res/values/health_permissions.xml
new file mode 100644
index 0000000..f8716e7
--- /dev/null
+++ b/apk/tests/OldPermissionsTestApp/res/values/health_permissions.xml
@@ -0,0 +1,14 @@
+<resources>
+  <array name="health_permissions">
+    <item>androidx.health.permission.ExerciseSession.READ</item>
+    <item>androidx.health.permission.ExerciseSession.WRITE</item>
+    <item>androidx.health.permission.Steps.READ</item>
+    <item>androidx.health.permission.Steps.WRITE</item>
+    <item>androidx.health.permission.HeartRate.READ</item>
+    <item>androidx.health.permission.HeartRate.WRITE</item>
+    <item>androidx.health.permission.Weight.READ</item>
+    <item>androidx.health.permission.Weight.WRITE</item>
+    <item>androidx.health.permission.TotalCaloriesBurned.READ</item>
+    <item>androidx.health.permission.TotalCaloriesBurned.WRITE</item>
+  </array>
+</resources>
diff --git a/apk/tests/OldPermissionsTestApp/src/android/healthconnect/controller/test/app3/MainActivity.java b/apk/tests/OldPermissionsTestApp/src/android/healthconnect/controller/test/app3/MainActivity.java
new file mode 100644
index 0000000..1c675d1
--- /dev/null
+++ b/apk/tests/OldPermissionsTestApp/src/android/healthconnect/controller/test/app3/MainActivity.java
@@ -0,0 +1,26 @@
+/*
+ * 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.controller.test.app4;
+
+import android.app.Activity;
+import android.health.connect.HealthConnectManager;
+
+/**
+ * This app is used as an external package to test system api {@link HealthConnectManager}
+ * permission-related APIs.
+ */
+public class MainActivity extends Activity {}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/permissions/connectedapps/ConnectedAppsFragmentTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/permissions/connectedapps/ConnectedAppsFragmentTest.kt
index c411e18..277a9f0 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/permissions/connectedapps/ConnectedAppsFragmentTest.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/permissions/connectedapps/ConnectedAppsFragmentTest.kt
@@ -20,6 +20,7 @@
 import androidx.lifecycle.MutableLiveData
 import androidx.recyclerview.widget.RecyclerView
 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
@@ -39,8 +40,11 @@
 import com.android.healthconnect.controller.shared.app.ConnectedAppStatus.ALLOWED
 import com.android.healthconnect.controller.shared.app.ConnectedAppStatus.DENIED
 import com.android.healthconnect.controller.shared.app.ConnectedAppStatus.INACTIVE
+import com.android.healthconnect.controller.shared.app.ConnectedAppStatus.NEEDS_UPDATE
+import com.android.healthconnect.controller.tests.utils.OLD_TEST_APP
 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.di.FakeDeviceInfoUtils
@@ -48,15 +52,23 @@
 import com.android.healthconnect.controller.tests.utils.whenever
 import com.android.healthconnect.controller.utils.DeviceInfoUtils
 import com.android.healthconnect.controller.utils.DeviceInfoUtilsModule
+import com.android.healthconnect.controller.utils.NavigationUtils
+import com.google.common.truth.Truth.assertThat
 import dagger.hilt.android.testing.BindValue
 import dagger.hilt.android.testing.HiltAndroidRule
 import dagger.hilt.android.testing.HiltAndroidTest
 import dagger.hilt.android.testing.UninstallModules
 import javax.inject.Inject
+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
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.verify
 
 @UninstallModules(DeviceInfoUtilsModule::class)
 @HiltAndroidTest
@@ -70,14 +82,21 @@
     val viewModel: ConnectedAppsViewModel = Mockito.mock(ConnectedAppsViewModel::class.java)
 
     @BindValue val deviceInfoUtils: DeviceInfoUtils = FakeDeviceInfoUtils()
+    @BindValue val navigationUtils: NavigationUtils = Mockito.mock(NavigationUtils::class.java)
 
     @Before
     fun setup() {
+        MockitoAnnotations.initMocks(this)
         hiltRule.inject()
         whenever(viewModel.disconnectAllState).then { MutableLiveData(NotStarted) }
         whenever(viewModel.alertDialogActive).then { MutableLiveData(false) }
     }
 
+    @After
+    fun tearDown() {
+        (deviceInfoUtils as FakeDeviceInfoUtils).reset()
+    }
+
     @Test
     fun test_allowedApps() {
         val connectApp = listOf(ConnectedAppMetadata(TEST_APP, status = ALLOWED))
@@ -267,4 +286,76 @@
 
         onView(withText("Help & feedback")).check(doesNotExist())
     }
+
+    @Test
+    fun appNeedsUpdatingElements_shownWhenOldAppWithDataInstalled() {
+        val connectApp =
+            listOf(
+                ConnectedAppMetadata(TEST_APP, status = ALLOWED),
+                ConnectedAppMetadata(TEST_APP_2, status = DENIED),
+                ConnectedAppMetadata(OLD_TEST_APP, status = NEEDS_UPDATE))
+        whenever(viewModel.connectedApps).then { MutableLiveData(connectApp) }
+        (deviceInfoUtils as FakeDeviceInfoUtils).setPlayStoreAvailability(true)
+
+        launchFragment<ConnectedAppsFragment>(Bundle())
+
+        onView(withText("App update needed")).check(matches(isDisplayed()))
+        onView(
+                withText(
+                    "Old permissions test app needs to be updated to " +
+                        "continue syncing with Health Connect"))
+            .check(matches(isDisplayed()))
+        onView(withText("Learn more")).check(matches(isDisplayed()))
+        onView(withText("Check for updates")).check(matches(isDisplayed()))
+        onView(withText("Needs updating")).perform(scrollTo()).check(matches(isDisplayed()))
+        onView(withText("Old permissions test app"))
+            .perform(scrollTo())
+            .check(matches(isDisplayed()))
+    }
+
+    @Test
+    fun appNeedsUpdateBanner_multipleOldApps_playStoreNotAvailable_showsCorrectly() {
+        val connectApp =
+            listOf(
+                ConnectedAppMetadata(TEST_APP, status = ALLOWED),
+                ConnectedAppMetadata(TEST_APP_2, status = DENIED),
+                ConnectedAppMetadata(OLD_TEST_APP, status = NEEDS_UPDATE),
+                ConnectedAppMetadata(TEST_APP_3, status = NEEDS_UPDATE))
+        whenever(viewModel.connectedApps).then { MutableLiveData(connectApp) }
+        (deviceInfoUtils as FakeDeviceInfoUtils).setPlayStoreAvailability(false)
+
+        launchFragment<ConnectedAppsFragment>(Bundle())
+        onView(withText("App update needed")).check(matches(isDisplayed()))
+        onView(withText("Some apps need to be updated to continue syncing with Health Connect"))
+            .check(matches(isDisplayed()))
+        onView(withText("Learn more")).check(matches(isDisplayed()))
+        onView(withText("Check for updates")).check(matches(not(isDisplayed())))
+
+        onView(withText("Learn more")).perform(click())
+        assertThat(deviceInfoUtils.helpCenterInvoked).isTrue()
+    }
+
+    @Test
+    fun appNeedsUpdateBanner_navigatesToPlayStoreWhenAvailable() {
+        Mockito.doNothing().whenever(navigationUtils).navigate(any(), any())
+
+        val connectApp =
+            listOf(
+                ConnectedAppMetadata(TEST_APP, status = ALLOWED),
+                ConnectedAppMetadata(TEST_APP_2, status = DENIED),
+                ConnectedAppMetadata(OLD_TEST_APP, status = NEEDS_UPDATE),
+                ConnectedAppMetadata(TEST_APP_3, status = NEEDS_UPDATE))
+        whenever(viewModel.connectedApps).then { MutableLiveData(connectApp) }
+        (deviceInfoUtils as FakeDeviceInfoUtils).setPlayStoreAvailability(true)
+
+        launchFragment<ConnectedAppsFragment>(Bundle())
+        onView(withText("App update needed")).check(matches(isDisplayed()))
+        onView(withText("Some apps need to be updated to continue syncing with Health Connect"))
+            .check(matches(isDisplayed()))
+        onView(withText("Learn more")).check(matches(isDisplayed()))
+        onView(withText("Check for updates")).check(matches(isDisplayed()))
+
+        onView(withText("Check for updates")).perform(click())
+        verify(navigationUtils).navigate(any(), eq(R.id.action_connected_apps_to_updated_apps))
+    }
 }
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/permissions/connectedapps/LoadHealthPermissionAppsTest.kt b/apk/tests/src/com/android/healthconnect/controller/tests/permissions/connectedapps/LoadHealthPermissionAppsTest.kt
new file mode 100644
index 0000000..90f1186
--- /dev/null
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/permissions/connectedapps/LoadHealthPermissionAppsTest.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2024 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.connectedapps
+
+import android.content.Context
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.healthconnect.controller.permissions.connectedapps.LoadHealthPermissionApps
+import com.android.healthconnect.controller.shared.HealthPermissionReader
+import com.android.healthconnect.controller.shared.app.AppInfoReader
+import com.android.healthconnect.controller.shared.app.ConnectedAppMetadata
+import com.android.healthconnect.controller.shared.app.ConnectedAppStatus
+import com.android.healthconnect.controller.tests.utils.OLD_PERMISSIONS_TEST_APP_PACKAGE_NAME
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME
+import com.android.healthconnect.controller.tests.utils.TEST_APP_PACKAGE_NAME_2
+import com.android.healthconnect.controller.tests.utils.di.FakeGetContributorAppInfoUseCase
+import com.android.healthconnect.controller.tests.utils.di.FakeGetGrantedHealthPermissionsUseCase
+import com.android.healthconnect.controller.tests.utils.di.FakeQueryRecentAccessLogsUseCase
+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.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito
+
+@ExperimentalCoroutinesApi
+@HiltAndroidTest
+class LoadHealthPermissionAppsTest {
+
+    @get:Rule val hiltRule = HiltAndroidRule(this)
+    private lateinit var context: Context
+
+    private val healthPermissionReader: HealthPermissionReader =
+        Mockito.mock(HealthPermissionReader::class.java)
+    private val loadGrantedHealthPermissionsUseCase = FakeGetGrantedHealthPermissionsUseCase()
+    private val getContributorAppInfoUseCase = FakeGetContributorAppInfoUseCase()
+    private val queryRecentAccessLogsUseCase = FakeQueryRecentAccessLogsUseCase()
+    @Inject lateinit var appInfoReader: AppInfoReader
+
+    private lateinit var loadHealthPermissionApps: LoadHealthPermissionApps
+
+    @Before
+    fun setup() {
+        hiltRule.inject()
+        context = InstrumentationRegistry.getInstrumentation().context
+        loadHealthPermissionApps =
+            LoadHealthPermissionApps(
+                healthPermissionReader,
+                loadGrantedHealthPermissionsUseCase,
+                getContributorAppInfoUseCase,
+                queryRecentAccessLogsUseCase,
+                appInfoReader,
+                Dispatchers.Main)
+    }
+
+    @After
+    fun tearDown() {
+        loadGrantedHealthPermissionsUseCase.reset()
+        getContributorAppInfoUseCase.reset()
+        queryRecentAccessLogsUseCase.reset()
+    }
+
+    @Test
+    fun appsWithHealthPermissions_correctlyCategorisedAsAllowedOrDenied() = runTest {
+        // appsWithHealthPermissions
+        whenever(healthPermissionReader.getAppsWithHealthPermissions())
+            .thenReturn(listOf(TEST_APP_PACKAGE_NAME, TEST_APP_PACKAGE_NAME_2))
+        loadGrantedHealthPermissionsUseCase.updateData(
+            TEST_APP_PACKAGE_NAME, listOf("PERM_1", "PERM_2"))
+        loadGrantedHealthPermissionsUseCase.updateData(TEST_APP_PACKAGE_NAME_2, listOf())
+
+        // appsWithData
+        getContributorAppInfoUseCase.setAppInfo(emptyMap())
+
+        // recentAccess
+        queryRecentAccessLogsUseCase.recentAccessMap(emptyMap())
+
+        // old permission apps
+        whenever(healthPermissionReader.getAppsWithOldHealthPermissions()).thenReturn(listOf())
+
+        val connectedAppsList = loadHealthPermissionApps.invoke()
+        val testAppMetadata = appInfoReader.getAppMetadata(TEST_APP_PACKAGE_NAME)
+        val testApp2Metadata = appInfoReader.getAppMetadata(TEST_APP_PACKAGE_NAME_2)
+        assertThat(connectedAppsList)
+            .containsExactlyElementsIn(
+                listOf(
+                    ConnectedAppMetadata(
+                        testAppMetadata, status = ConnectedAppStatus.ALLOWED, null),
+                    ConnectedAppMetadata(
+                        testApp2Metadata, status = ConnectedAppStatus.DENIED, null)))
+    }
+
+    @Test
+    fun appsWithData_butNoHealthPermissions_correctlyCategorisedAsInactive() = runTest {
+        // appsWithHealthPermissions
+        whenever(healthPermissionReader.getAppsWithHealthPermissions())
+            .thenReturn(listOf(TEST_APP_PACKAGE_NAME))
+        loadGrantedHealthPermissionsUseCase.updateData(
+            TEST_APP_PACKAGE_NAME, listOf("PERM_1", "PERM_2"))
+        val testAppMetadata = appInfoReader.getAppMetadata(TEST_APP_PACKAGE_NAME)
+        val testApp2Metadata = appInfoReader.getAppMetadata(TEST_APP_PACKAGE_NAME_2)
+
+        // appsWithData
+        getContributorAppInfoUseCase.setAppInfo(mapOf(TEST_APP_PACKAGE_NAME_2 to testApp2Metadata))
+
+        // recentAccess
+        queryRecentAccessLogsUseCase.recentAccessMap(emptyMap())
+
+        // old permission apps
+        whenever(healthPermissionReader.getAppsWithOldHealthPermissions()).thenReturn(listOf())
+
+        val connectedAppsList = loadHealthPermissionApps.invoke()
+        assertThat(connectedAppsList)
+            .containsExactlyElementsIn(
+                listOf(
+                    ConnectedAppMetadata(
+                        testAppMetadata, status = ConnectedAppStatus.ALLOWED, null),
+                    ConnectedAppMetadata(
+                        testApp2Metadata, status = ConnectedAppStatus.INACTIVE, null)))
+    }
+
+    @Test
+    fun appsWithOldHealthPermissions_withoutNewPermissions_correctlyCategorisedAsNeedsUpdate() =
+        runTest {
+            // appsWithHealthPermissions
+            whenever(healthPermissionReader.getAppsWithHealthPermissions())
+                .thenReturn(listOf(TEST_APP_PACKAGE_NAME))
+            loadGrantedHealthPermissionsUseCase.updateData(TEST_APP_PACKAGE_NAME, listOf())
+            val testAppMetadata = appInfoReader.getAppMetadata(TEST_APP_PACKAGE_NAME)
+            val oldTestAppMetadata =
+                appInfoReader.getAppMetadata(OLD_PERMISSIONS_TEST_APP_PACKAGE_NAME)
+
+            // appsWithData
+            getContributorAppInfoUseCase.setAppInfo(emptyMap())
+
+            // recentAccess
+            queryRecentAccessLogsUseCase.recentAccessMap(emptyMap())
+
+            // old permission apps
+            whenever(healthPermissionReader.getAppsWithOldHealthPermissions())
+                .thenReturn(listOf(OLD_PERMISSIONS_TEST_APP_PACKAGE_NAME))
+
+            val connectedAppsList = loadHealthPermissionApps.invoke()
+            assertThat(connectedAppsList)
+                .containsExactlyElementsIn(
+                    listOf(
+                        ConnectedAppMetadata(
+                            testAppMetadata, status = ConnectedAppStatus.DENIED, null),
+                        ConnectedAppMetadata(
+                            oldTestAppMetadata, status = ConnectedAppStatus.NEEDS_UPDATE, null)))
+        }
+
+    @Test
+    fun appsWithOldHealthPermissions_andNewPermissions_correctlyCategorisedAsAllowed() = runTest {
+        // appsWithHealthPermissions
+        whenever(healthPermissionReader.getAppsWithHealthPermissions())
+            .thenReturn(listOf(TEST_APP_PACKAGE_NAME, OLD_PERMISSIONS_TEST_APP_PACKAGE_NAME))
+        loadGrantedHealthPermissionsUseCase.updateData(TEST_APP_PACKAGE_NAME, listOf("PERM_1"))
+        loadGrantedHealthPermissionsUseCase.updateData(
+            OLD_PERMISSIONS_TEST_APP_PACKAGE_NAME, listOf("PERM_1"))
+        val testAppMetadata = appInfoReader.getAppMetadata(TEST_APP_PACKAGE_NAME)
+        val oldTestAppMetadata = appInfoReader.getAppMetadata(OLD_PERMISSIONS_TEST_APP_PACKAGE_NAME)
+
+        // appsWithData
+        getContributorAppInfoUseCase.setAppInfo(emptyMap())
+
+        // recentAccess
+        queryRecentAccessLogsUseCase.recentAccessMap(emptyMap())
+
+        // old permission apps
+        whenever(healthPermissionReader.getAppsWithOldHealthPermissions())
+            .thenReturn(listOf(OLD_PERMISSIONS_TEST_APP_PACKAGE_NAME))
+
+        val connectedAppsList = loadHealthPermissionApps.invoke()
+        assertThat(connectedAppsList)
+            .containsExactlyElementsIn(
+                listOf(
+                    ConnectedAppMetadata(
+                        testAppMetadata, status = ConnectedAppStatus.ALLOWED, null),
+                    ConnectedAppMetadata(
+                        oldTestAppMetadata, status = ConnectedAppStatus.ALLOWED, null)))
+    }
+}
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 4926a85..1897823 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
@@ -5,6 +5,7 @@
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.healthconnect.controller.permissions.data.HealthPermission
 import com.android.healthconnect.controller.shared.HealthPermissionReader
+import com.android.healthconnect.controller.tests.utils.OLD_PERMISSIONS_TEST_APP_PACKAGE_NAME
 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
@@ -112,13 +113,24 @@
         assertThat(apps).isEqualTo(apps.distinct())
     }
 
-
     @Test
     fun getAppsWithHealthPermissions_doesNotReturnUnsupportedApps() = runTest {
         assertThat(permissionReader.getAppsWithHealthPermissions())
             .doesNotContain(UNSUPPORTED_TEST_APP_PACKAGE_NAME)
     }
 
+    @Test
+    fun getAppsWithOldHealthPermissions_returnsOldSupportedApps() = runTest {
+        assertThat(permissionReader.getAppsWithOldHealthPermissions())
+            .containsExactly(OLD_PERMISSIONS_TEST_APP_PACKAGE_NAME)
+    }
+
+    @Test
+    fun getAppsWithOldHealthPermissions_returnsDistinctApps() = runTest {
+        val apps = permissionReader.getAppsWithOldHealthPermissions()
+        assertThat(apps).isEqualTo(apps.distinct())
+    }
+
     private fun String.toHealthPermission(): HealthPermission {
         return HealthPermission.fromPermissionString(this)
     }
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 7ee2fa6..e735218 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
@@ -143,9 +143,11 @@
 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 OLD_PERMISSIONS_TEST_APP_PACKAGE_NAME = "android.healthconnect.controller.test.app4"
 const val TEST_APP_NAME = "Health Connect test app"
 const val TEST_APP_NAME_2 = "Health Connect test app 2"
 const val TEST_APP_NAME_3 = "Health Connect test app 3"
+const val OLD_APP_NAME = "Old permissions test app"
 
 val TEST_APP =
     AppMetadata(packageName = TEST_APP_PACKAGE_NAME, appName = TEST_APP_NAME, icon = null)
@@ -153,5 +155,7 @@
     AppMetadata(packageName = TEST_APP_PACKAGE_NAME_2, appName = TEST_APP_NAME_2, icon = null)
 val TEST_APP_3 =
     AppMetadata(packageName = TEST_APP_PACKAGE_NAME_3, appName = TEST_APP_NAME_3, icon = null)
-
+val OLD_TEST_APP =
+    AppMetadata(
+        packageName = OLD_PERMISSIONS_TEST_APP_PACKAGE_NAME, appName = OLD_APP_NAME, icon = null)
 // endregion
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 d088790..e595b51 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
@@ -38,11 +38,13 @@
 import com.android.healthconnect.controller.permissions.api.IGetGrantedHealthPermissionsUseCase
 import com.android.healthconnect.controller.permissions.connectedapps.ILoadHealthPermissionApps
 import com.android.healthconnect.controller.permissions.data.HealthPermissionType
+import com.android.healthconnect.controller.permissions.shared.IQueryRecentAccessLogsUseCase
 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.app.IGetContributorAppInfoUseCase
 import com.android.healthconnect.controller.shared.usecase.UseCaseResults
 import java.time.Instant
 import java.time.LocalDate
@@ -390,3 +392,36 @@
         forceFail = false
     }
 }
+
+class FakeGetContributorAppInfoUseCase : IGetContributorAppInfoUseCase {
+
+    private var appInfoMap: Map<String, AppMetadata> = emptyMap()
+
+    fun setAppInfo(appInfoMap: Map<String, AppMetadata>) {
+        this.appInfoMap = appInfoMap
+    }
+
+    override suspend fun invoke(): Map<String, AppMetadata> {
+        return appInfoMap
+    }
+
+    fun reset() {
+        this.appInfoMap = emptyMap()
+    }
+}
+
+class FakeQueryRecentAccessLogsUseCase : IQueryRecentAccessLogsUseCase {
+    private var recentAccessMap: Map<String, Instant> = emptyMap()
+
+    fun recentAccessMap(recentAccessMap: Map<String, Instant>) {
+        this.recentAccessMap = recentAccessMap
+    }
+
+    override suspend fun invoke(): Map<String, Instant> {
+        return recentAccessMap
+    }
+
+    fun reset() {
+        this.recentAccessMap = emptyMap()
+    }
+}
diff --git a/apk/tests/src/com/android/healthconnect/controller/tests/utils/di/FakeDeviceInfoUtils.kt b/apk/tests/src/com/android/healthconnect/controller/tests/utils/di/FakeDeviceInfoUtils.kt
index 14abf96..5979373 100644
--- a/apk/tests/src/com/android/healthconnect/controller/tests/utils/di/FakeDeviceInfoUtils.kt
+++ b/apk/tests/src/com/android/healthconnect/controller/tests/utils/di/FakeDeviceInfoUtils.kt
@@ -30,6 +30,15 @@
 
     private var isHealthConnectAvailable = true
 
+    var helpCenterInvoked = false
+
+    fun reset() {
+        sendFeedbackAvailable = false
+        playStoreAvailable = false
+        isHealthConnectAvailable = true
+        helpCenterInvoked = false
+    }
+
     fun setSendFeedbackAvailability(available: Boolean) {
         sendFeedbackAvailable = available
     }
@@ -54,7 +63,9 @@
         return playStoreAvailable
     }
 
-    override fun openHCGetStartedLink(activity: FragmentActivity) {}
+    override fun openHCGetStartedLink(activity: FragmentActivity) {
+        helpCenterInvoked = true
+    }
 
     override fun openSendFeedbackActivity(activity: FragmentActivity) {}
 
diff --git a/framework/java/android/health/connect/HealthConnectManager.java b/framework/java/android/health/connect/HealthConnectManager.java
index da236d2..0498353 100644
--- a/framework/java/android/health/connect/HealthConnectManager.java
+++ b/framework/java/android/health/connect/HealthConnectManager.java
@@ -338,6 +338,8 @@
      * hold the permission, a {@link java.lang.SecurityException} is thrown. If the package or
      * permission is invalid, a {@link java.lang.IllegalArgumentException} is thrown.
      *
+     * <p><b>Note:</b> This API sets {@code PackageManager.FLAG_PERMISSION_USER_SET}.
+     *
      * @hide
      */
     @RequiresPermission(MANAGE_HEALTH_PERMISSIONS)
@@ -357,6 +359,10 @@
      * java.lang.SecurityException} is thrown. If the package or permission is invalid, a {@link
      * java.lang.IllegalArgumentException} is thrown.
      *
+     * <p><b>Note:</b> This API sets {@code PackageManager.FLAG_PERMISSION_USER_SET} or {@code
+     * PackageManager.FLAG_PERMISSION_USER_FIXED} based on the number of revocations of a particular
+     * permission for a package.
+     *
      * @hide
      */
     @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression
diff --git a/tests/integrationtests/src/android/healthconnect/tests/permissions/GrantTimeIntegrationTest.java b/tests/integrationtests/src/android/healthconnect/tests/permissions/GrantTimeIntegrationTest.java
index f4a3485..a4e1a76 100644
--- a/tests/integrationtests/src/android/healthconnect/tests/permissions/GrantTimeIntegrationTest.java
+++ b/tests/integrationtests/src/android/healthconnect/tests/permissions/GrantTimeIntegrationTest.java
@@ -145,6 +145,19 @@
     }
 
     @Test
+    public void testGrantHealthPermission_notAllPermissionsRevoked_returnsTheSameTime()
+            throws Exception {
+        grantHealthPermission(DEFAULT_APP_PACKAGE, DEFAULT_PERM);
+        Instant grantTime = getHealthDataHistoricalAccessStartDate(DEFAULT_APP_PACKAGE);
+        grantHealthPermission(DEFAULT_APP_PACKAGE, DEFAULT_PERM_2);
+        Instant grantTime2 = getHealthDataHistoricalAccessStartDate(DEFAULT_APP_PACKAGE);
+        assertThat(grantTime).isEqualTo(grantTime2);
+        revokePermissionWithDelay(DEFAULT_APP_PACKAGE, DEFAULT_PERM);
+        Instant grantTime3 = getHealthDataHistoricalAccessStartDate(DEFAULT_APP_PACKAGE);
+        assertThat(grantTime3).isEqualTo(grantTime);
+    }
+
+    @Test
     public void testGrantHealthPermission_permissionGrantedAndRevoked_resetGrantTime()
             throws Exception {
         grantHealthPermission(DEFAULT_APP_PACKAGE, DEFAULT_PERM);
diff --git a/tests/integrationtests/src/android/healthconnect/tests/permissions/HealthConnectWithManagePermissionsTest.java b/tests/integrationtests/src/android/healthconnect/tests/permissions/HealthConnectWithManagePermissionsTest.java
index 9cf61e2..2a36bc2 100644
--- a/tests/integrationtests/src/android/healthconnect/tests/permissions/HealthConnectWithManagePermissionsTest.java
+++ b/tests/integrationtests/src/android/healthconnect/tests/permissions/HealthConnectWithManagePermissionsTest.java
@@ -93,6 +93,8 @@
 
         revokePermissionViaPackageManager(DEFAULT_APP_PACKAGE, DEFAULT_PERM);
         revokePermissionViaPackageManager(DEFAULT_APP_PACKAGE, DEFAULT_PERM_2);
+        resetPermissionFlags(DEFAULT_APP_PACKAGE, DEFAULT_PERM);
+        resetPermissionFlags(DEFAULT_APP_PACKAGE, DEFAULT_PERM_2);
         assertPermNotGrantedForApp(DEFAULT_APP_PACKAGE, DEFAULT_PERM);
         assertPermNotGrantedForApp(DEFAULT_APP_PACKAGE, DEFAULT_PERM_2);
         deleteAllStagedRemoteData();
@@ -110,6 +112,39 @@
         assertPermGrantedForApp(DEFAULT_APP_PACKAGE, DEFAULT_PERM);
     }
 
+    @Test
+    public void testGrantHealthPermission_appHasPermissionDeclared_flagUserSetEnabled()
+            throws Exception {
+        grantHealthPermission(DEFAULT_APP_PACKAGE, DEFAULT_PERM);
+        Map<String, Integer> permissionsFlags =
+                getHealthPermissionsFlags(DEFAULT_APP_PACKAGE, List.of(DEFAULT_PERM));
+
+        assertPermGrantedForApp(DEFAULT_APP_PACKAGE, DEFAULT_PERM);
+        assertFlagsSet(permissionsFlags.get(DEFAULT_PERM), PackageManager.FLAG_PERMISSION_USER_SET);
+        assertFlagsNotSet(
+                permissionsFlags.get(DEFAULT_PERM), PackageManager.FLAG_PERMISSION_USER_FIXED);
+    }
+
+    @Test
+    public void testGrantHealthPermission_revokeTwiceThenGrant_flagUserSetEnabled()
+            throws Exception {
+        revokeHealthPermission(DEFAULT_APP_PACKAGE, DEFAULT_PERM, /* reason= */ null);
+        revokeHealthPermission(DEFAULT_APP_PACKAGE, DEFAULT_PERM, /* reason= */ null);
+        Map<String, Integer> permissionsFlags =
+                getHealthPermissionsFlags(DEFAULT_APP_PACKAGE, List.of(DEFAULT_PERM));
+
+        assertPermNotGrantedForApp(DEFAULT_APP_PACKAGE, DEFAULT_PERM);
+        assertFlagsSet(
+                permissionsFlags.get(DEFAULT_PERM), PackageManager.FLAG_PERMISSION_USER_FIXED);
+
+        grantHealthPermission(DEFAULT_APP_PACKAGE, DEFAULT_PERM);
+        permissionsFlags = getHealthPermissionsFlags(DEFAULT_APP_PACKAGE, List.of(DEFAULT_PERM));
+
+        assertFlagsSet(permissionsFlags.get(DEFAULT_PERM), PackageManager.FLAG_PERMISSION_USER_SET);
+        assertFlagsNotSet(
+                permissionsFlags.get(DEFAULT_PERM), PackageManager.FLAG_PERMISSION_USER_FIXED);
+    }
+
     @Test(expected = SecurityException.class)
     public void testGrantHealthPermission_usageIntentNotSupported_throwsIllegalArgumentException()
             throws Exception {
@@ -187,7 +222,44 @@
     }
 
     @Test
-    public void testRevokeHealthPermission_appHasPermissionNotGranted_success() throws Exception {
+    public void testRevokeHealthPermission_firstRevoke_flagUserSetEnabled() throws Exception {
+        revokeHealthPermission(DEFAULT_APP_PACKAGE, DEFAULT_PERM, /* reason= */ null);
+        Map<String, Integer> permissionsFlags =
+                getHealthPermissionsFlags(DEFAULT_APP_PACKAGE, List.of(DEFAULT_PERM));
+
+        assertPermNotGrantedForApp(DEFAULT_APP_PACKAGE, DEFAULT_PERM);
+        assertFlagsSet(permissionsFlags.get(DEFAULT_PERM), PackageManager.FLAG_PERMISSION_USER_SET);
+        assertFlagsNotSet(
+                permissionsFlags.get(DEFAULT_PERM), PackageManager.FLAG_PERMISSION_USER_FIXED);
+    }
+
+    @Test
+    public void testRevokeHealthPermission_grantThenRevoke_flagUserSetEnabled() throws Exception {
+        grantHealthPermission(DEFAULT_APP_PACKAGE, DEFAULT_PERM);
+        revokeHealthPermission(DEFAULT_APP_PACKAGE, DEFAULT_PERM, /* reason= */ null);
+        Map<String, Integer> permissionsFlags =
+                getHealthPermissionsFlags(DEFAULT_APP_PACKAGE, List.of(DEFAULT_PERM));
+
+        assertPermNotGrantedForApp(DEFAULT_APP_PACKAGE, DEFAULT_PERM);
+        assertFlagsSet(permissionsFlags.get(DEFAULT_PERM), PackageManager.FLAG_PERMISSION_USER_SET);
+        assertFlagsNotSet(
+                permissionsFlags.get(DEFAULT_PERM), PackageManager.FLAG_PERMISSION_USER_FIXED);
+    }
+
+    @Test
+    public void testRevokeHealthPermission_secondRevoke_flagUserFixedEnabled() throws Exception {
+        revokeHealthPermission(DEFAULT_APP_PACKAGE, DEFAULT_PERM, /* reason= */ null);
+        revokeHealthPermission(DEFAULT_APP_PACKAGE, DEFAULT_PERM, /* reason= */ null);
+        Map<String, Integer> permissionsFlags =
+                getHealthPermissionsFlags(DEFAULT_APP_PACKAGE, List.of(DEFAULT_PERM));
+
+        assertPermNotGrantedForApp(DEFAULT_APP_PACKAGE, DEFAULT_PERM);
+        assertFlagsSet(
+                permissionsFlags.get(DEFAULT_PERM), PackageManager.FLAG_PERMISSION_USER_FIXED);
+    }
+
+    @Test
+    public void testRevokeHealthPermission_success() throws Exception {
         revokeHealthPermission(DEFAULT_APP_PACKAGE, DEFAULT_PERM, /* reason= */ null);
 
         assertPermNotGrantedForApp(DEFAULT_APP_PACKAGE, DEFAULT_PERM);
@@ -545,6 +617,10 @@
                 Manifest.permission.GRANT_RUNTIME_PERMISSIONS);
     }
 
+    private void resetPermissionFlags(String packageName, String permName) {
+        updatePermissionsFlagsViaPackageManager(packageName, permName, /* flags= */ 0);
+    }
+
     private void updatePermissionsFlagsViaPackageManager(
             String packageName, String permName, int flags) {
         int mask =
@@ -663,4 +739,8 @@
     private static void assertFlagsSet(int actualFlags, int expectedFlags) {
         assertThat((actualFlags & expectedFlags)).isEqualTo(expectedFlags);
     }
+
+    private static void assertFlagsNotSet(int actualFlags, int expectedFlagsNotSet) {
+        assertThat((actualFlags & expectedFlagsNotSet)).isEqualTo(0);
+    }
 }