Filter onboarding candidate apps by non-medical permissions, including requested and granted permissions.

Test: atest
Bug: 417974138
Flag: com.android.healthfitness.flags.onboarding
Change-Id: I3a15ec918f5a79d9d3ebea3aeb14d2d8ae740b8c
diff --git a/framework/java/android/health/connect/internal/datatypes/utils/HealthConnectMappings.java b/framework/java/android/health/connect/internal/datatypes/utils/HealthConnectMappings.java
index 83bbe0b..64329c2 100644
--- a/framework/java/android/health/connect/internal/datatypes/utils/HealthConnectMappings.java
+++ b/framework/java/android/health/connect/internal/datatypes/utils/HealthConnectMappings.java
@@ -178,6 +178,15 @@
         return mWritePermissionToDataCategoryMap.containsKey(permissionName);
     }
 
+    /**
+     * @return true if {@code permissionName} is a fitness-permission
+     * @hide
+     */
+    public boolean isFitnessPermission(@NonNull String permissionName) {
+        return mPermissionCategoryToReadPermissionMap.containsValue(permissionName)
+                || mPermissionCategoryToWritePermissionMap.containsValue(permissionName);
+    }
+
     /** @hide */
     public String getHealthReadPermission(@HealthPermissionCategory.Type int permissionCategory) {
         if (!Flags.healthConnectMappings()) {
diff --git a/framework/tests/java/android/health/connect/internal/datatypes/utils/HealthConnectMappingsTest.java b/framework/tests/java/android/health/connect/internal/datatypes/utils/HealthConnectMappingsTest.java
index 371aa6a..be335d6 100644
--- a/framework/tests/java/android/health/connect/internal/datatypes/utils/HealthConnectMappingsTest.java
+++ b/framework/tests/java/android/health/connect/internal/datatypes/utils/HealthConnectMappingsTest.java
@@ -432,7 +432,6 @@
         Flags.FLAG_CLOUD_BACKUP_AND_RESTORE_DB,
         Flags.FLAG_EXERCISE_SEGMENT_IMPROVEMENTS_DB
     })
-
     @Test
     public void nicotineIntakeFlagEnabled_containsNicotineIntake() {
         HealthConnectMappings healthConnectMappings = new HealthConnectMappings();
@@ -460,4 +459,40 @@
         assertThat(healthConnectMappings.getAllRecordTypeIdentifiers())
                 .doesNotContain(RECORD_TYPE_NICOTINE_INTAKE);
     }
+
+    @Test
+    public void isFitnessPermission_returnsTrueForAllFitnessPermissions() {
+        HealthConnectMappings healthConnectMappings = new HealthConnectMappings();
+        for (DataTypeDescriptor descriptor : getAllDataTypeDescriptors()) {
+            assertThat(healthConnectMappings.isFitnessPermission(descriptor.getReadPermission()))
+                    .isTrue();
+            assertThat(healthConnectMappings.isFitnessPermission(descriptor.getWritePermission()))
+                    .isTrue();
+        }
+    }
+
+    @Test
+    public void isFitnessPermission_returnsFalseForAdditionalPermissions() {
+        HealthConnectMappings healthConnectMappings = new HealthConnectMappings();
+        assertThat(
+                        healthConnectMappings.isFitnessPermission(
+                                HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND))
+                .isFalse();
+        assertThat(
+                        healthConnectMappings.isFitnessPermission(
+                                HealthPermissions.READ_HEALTH_DATA_HISTORY))
+                .isFalse();
+        assertThat(
+                        healthConnectMappings.isFitnessPermission(
+                                HealthPermissions.READ_EXERCISE_ROUTES))
+                .isFalse();
+    }
+
+    @Test
+    public void isFitnessPermission_returnsFalseForMedicalPermissions() {
+        HealthConnectMappings healthConnectMappings = new HealthConnectMappings();
+        for (String permission : HealthPermissions.getAllMedicalPermissions()) {
+            assertThat(healthConnectMappings.isFitnessPermission(permission)).isFalse();
+        }
+    }
 }
diff --git a/service/java/com/android/server/healthconnect/onboarding/OnboardingStateManager.java b/service/java/com/android/server/healthconnect/onboarding/OnboardingStateManager.java
index f30b7e0..da1cb11 100644
--- a/service/java/com/android/server/healthconnect/onboarding/OnboardingStateManager.java
+++ b/service/java/com/android/server/healthconnect/onboarding/OnboardingStateManager.java
@@ -190,13 +190,11 @@
     }
 
     private boolean isConnected(PackageInfo app) {
-        return mHealthConnectPermissionHelper.hasGrantedHealthPermissions(
-                app.packageName, mUserHandle);
+        return mHealthConnectPermissionHelper.hasGrantedFitnessPermission(app);
     }
 
     private boolean hasFitnessPerm(PackageInfo app) {
-        // TODO(b/417974138) implement this
-        return true;
+        return mHealthConnectPermissionHelper.isRequestingFitnessPermission(app);
     }
 
     private boolean hasBeenUsed(PackageInfo app) {
diff --git a/service/java/com/android/server/healthconnect/permission/HealthConnectPermissionHelper.java b/service/java/com/android/server/healthconnect/permission/HealthConnectPermissionHelper.java
index 23c037a..cee3a57 100644
--- a/service/java/com/android/server/healthconnect/permission/HealthConnectPermissionHelper.java
+++ b/service/java/com/android/server/healthconnect/permission/HealthConnectPermissionHelper.java
@@ -478,6 +478,42 @@
         return isFromSplitPermission(permissionFlag, targetSdkVersion);
     }
 
+    /**
+     * Returns true if an app declares at least one fitness permission in its manifest. A fitness
+     * permission is a permission that is not medical and not additional.
+     */
+    public boolean isRequestingFitnessPermission(PackageInfo packageInfo) {
+        if (packageInfo == null || packageInfo.requestedPermissions == null) {
+            return false;
+        }
+
+        for (int i = 0; i < packageInfo.requestedPermissions.length; i++) {
+            String currentPermission = packageInfo.requestedPermissions[i];
+            if (mHealthConnectMappings.isFitnessPermission(currentPermission)) {
+                // A health permission that is not medical or additional is a fitness permission
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns true if an app was granted at least one fitness permission. A fitness permission is a
+     * permission that is not medical and not additional.
+     */
+    public boolean hasGrantedFitnessPermission(PackageInfo packageInfo) {
+        List<String> grantedHealthPermissions =
+                PackageInfoUtils.getGrantedHealthPermissions(mContext, packageInfo);
+
+        for (String permission : grantedHealthPermissions) {
+            if (mHealthConnectMappings.isFitnessPermission(permission)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
     /** Returns if the app is targeting SDK 35 and requesting the given permission. */
     private boolean isAppRequestingPermissionWithOutdatedTargetSdk(
             String packageName, UserHandle userHandle, String permission, int buildVersion) {
diff --git a/tests/unittests/src/com/android/server/healthconnect/onboarding/OnboardingStateManagerTest.java b/tests/unittests/src/com/android/server/healthconnect/onboarding/OnboardingStateManagerTest.java
index 6463123..4652bf6 100644
--- a/tests/unittests/src/com/android/server/healthconnect/onboarding/OnboardingStateManagerTest.java
+++ b/tests/unittests/src/com/android/server/healthconnect/onboarding/OnboardingStateManagerTest.java
@@ -29,6 +29,7 @@
 
 import static org.junit.Assert.assertEquals;
 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.never;
@@ -41,10 +42,12 @@
 import android.content.Context;
 import android.content.pm.PackageInfo;
 import android.health.connect.HealthConnectOnboardingState;
+import android.health.connect.HealthPermissions;
 import android.health.connect.accesslog.AccessLog;
 import android.health.connect.internal.datatypes.AppInfoInternal;
 import android.os.UserHandle;
 
+import androidx.test.InstrumentationRegistry;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import com.android.server.healthconnect.common.accesslog.AccessLogsHelper;
@@ -79,7 +82,7 @@
 
     @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
 
-    @Mock private Context mContext;
+    private Context mContext;
     @Mock private PreferenceHelper mPreferenceHelper;
     @Mock private HealthConnectPermissionHelper mHealthConnectPermissionHelper;
     @Mock private MockListener mMockListener;
@@ -105,10 +108,11 @@
 
     @Before
     public void setUp() {
-        setAppConnected(APP_PKG_1, /* isConnected= */ false);
-        setAppConnected(APP_PKG_2, /* isConnected= */ false);
-        setAppConnected(APP_PKG_3, /* isConnected= */ false);
-        setAppConnected(APP_PKG_4, /* isConnected= */ false);
+        mContext = InstrumentationRegistry.getTargetContext();
+        setAppConnectedFitnessPermission(APP_PKG_1, /* isConnected= */ false);
+        setAppConnectedFitnessPermission(APP_PKG_2, /* isConnected= */ false);
+        setAppConnectedFitnessPermission(APP_PKG_3, /* isConnected= */ false);
+        setAppConnectedFitnessPermission(APP_PKG_4, /* isConnected= */ false);
 
         mFakeAppInfoMap = new HashMap<>(4);
         mFakeAppInfoMap.put(APP_PKG_1, createAppInfo(APP_PKG_1, false));
@@ -174,10 +178,10 @@
 
     @Test
     public void updateAndGetOnboardingState_allAppsConnected_returnsHide() {
-        setAppConnected(APP_PKG_1, /* isConnected= */ true);
-        setAppConnected(APP_PKG_2, /* isConnected= */ true);
-        setAppConnected(APP_PKG_3, /* isConnected= */ true);
-        setAppConnected(APP_PKG_4, /* isConnected= */ true);
+        setAppConnectedFitnessPermission(APP_PKG_1, /* isConnected= */ true);
+        setAppConnectedFitnessPermission(APP_PKG_2, /* isConnected= */ true);
+        setAppConnectedFitnessPermission(APP_PKG_3, /* isConnected= */ true);
+        setAppConnectedFitnessPermission(APP_PKG_4, /* isConnected= */ true);
 
         setCompatibleApps(
                 ImmutableList.of(
@@ -194,12 +198,32 @@
         verifyStateChange(ONBOARDING_BANNER_STATE_HIDE);
     }
 
-    // TODO(b/417974138): Add test case for fitness permission
+    @Test
+    public void updateAndGetOnboardingState_onlyAppsWithoutFitnessPermissions_returnsHide() {
+        setAppConnectedFitnessPermission(APP_PKG_1, /* isConnected= */ false);
+        setAppConnectedFitnessPermission(APP_PKG_2, /* isConnected= */ false);
+        setAppConnectedFitnessPermission(APP_PKG_3, /* isConnected= */ false);
+        setAppRequestsFitnessPermission(APP_PKG_1, false);
+        setAppRequestsFitnessPermission(APP_PKG_2, false);
+        setAppRequestsFitnessPermission(APP_PKG_3, false);
+
+        setCompatibleApps(
+                ImmutableList.of(
+                        createPackageInfo(APP_PKG_1, EIGHT_DAYS_AGO),
+                        createPackageInfo(APP_PKG_2, SEVEN_DAYS_AGO),
+                        createPackageInfo(APP_PKG_3, SIX_DAYS_AGO)));
+        setOnboardingStateInPreference(ONBOARDING_BANNER_STATE_ZERO_APPS_CONNECTED);
+        clearInvocations(mPreferenceHelper, mMockListener);
+
+        assertThat(mOnboardingStateManager.updateAndGetOnboardingState())
+                .isEqualTo(ONBOARDING_BANNER_STATE_HIDE);
+        verifyStateChange(ONBOARDING_BANNER_STATE_HIDE);
+    }
 
     @Test
     public void updateAndGetOnboardingState_twoConnectedApps_returnsHide() {
-        setAppConnected(APP_PKG_1, /* isConnected= */ true);
-        setAppConnected(APP_PKG_2, /* isConnected= */ true);
+        setAppConnectedFitnessPermission(APP_PKG_1, /* isConnected= */ true);
+        setAppConnectedFitnessPermission(APP_PKG_2, /* isConnected= */ true);
 
         setCompatibleApps(
                 ImmutableList.of(
@@ -218,6 +242,9 @@
 
     @Test
     public void updateAndGetOnboardingState_zeroConnected_noCandidate_returnsHide() {
+        setAppRequestsFitnessPermission(APP_PKG_1, true);
+        setAppRequestsFitnessPermission(APP_PKG_2, true);
+        setAppRequestsFitnessPermission(APP_PKG_3, true);
         setCompatibleApps(
                 ImmutableList.of(
                         createPackageInfo(APP_PKG_1, SIX_DAYS_AGO), // too new
@@ -236,6 +263,10 @@
 
     @Test
     public void updateAndGetOnboardingState_zeroConnected_oneCandidate_returnsHide() {
+        setAppRequestsFitnessPermission(APP_PKG_1, true);
+        setAppRequestsFitnessPermission(APP_PKG_2, true);
+        setAppRequestsFitnessPermission(APP_PKG_3, true);
+        setAppRequestsFitnessPermission(APP_PKG_4, true);
         setCompatibleApps(
                 ImmutableList.of(
                         createPackageInfo(APP_PKG_1, EIGHT_DAYS_AGO), // candidate
@@ -255,6 +286,8 @@
 
     @Test
     public void updateAndGetOnboardingState_zeroConnected_twoCandidates_returnsZeroConnected() {
+        setAppRequestsFitnessPermission(APP_PKG_1, true);
+        setAppRequestsFitnessPermission(APP_PKG_2, true);
         setCompatibleApps(
                 ImmutableList.of(
                         createPackageInfo(APP_PKG_1, EIGHT_DAYS_AGO),
@@ -267,7 +300,10 @@
 
     @Test
     public void updateAndGetOnboardingState_oneConnected_noCandidate_returnsHide() {
-        setAppConnected(APP_PKG_1, /* isConnected= */ true);
+        setAppConnectedFitnessPermission(APP_PKG_1, /* isConnected= */ true);
+        setAppRequestsFitnessPermission(APP_PKG_2, true);
+        setAppRequestsFitnessPermission(APP_PKG_3, true);
+        setAppRequestsFitnessPermission(APP_PKG_4, true);
 
         setCompatibleApps(
                 ImmutableList.of(
@@ -289,7 +325,8 @@
 
     @Test
     public void updateAndGetOnboardingState_oneConnected_oneCandidate_returnsOneConnected() {
-        setAppConnected(APP_PKG_1, /* isConnected= */ true);
+        setAppConnectedFitnessPermission(APP_PKG_1, /* isConnected= */ true);
+        setAppRequestsFitnessPermission(APP_PKG_2, true);
 
         setCompatibleApps(
                 ImmutableList.of(
@@ -303,8 +340,8 @@
 
     @Test
     public void updateAndGetOnboardingState_zeroAppToOneAppConnected_updatesAndNotifies() {
-        setAppConnected(APP_PKG_2, /* isConnected= */ true);
-
+        setAppConnectedFitnessPermission(APP_PKG_2, /* isConnected= */ true);
+        setAppRequestsFitnessPermission(APP_PKG_1, true);
         setCompatibleApps(
                 ImmutableList.of(
                         createPackageInfo(APP_PKG_1, EIGHT_DAYS_AGO), // candidate
@@ -320,8 +357,8 @@
 
     @Test
     public void updateAndGetOnboardingState_stateDoesNotChange_notNotified() {
-        setAppConnected(APP_PKG_1, /* isConnected= */ true);
-        setAppConnected(APP_PKG_2, /* isConnected= */ true);
+        setAppConnectedFitnessPermission(APP_PKG_1, /* isConnected= */ true);
+        setAppConnectedFitnessPermission(APP_PKG_2, /* isConnected= */ true);
 
         setCompatibleApps(
                 ImmutableList.of(
@@ -364,10 +401,25 @@
     }
 
     /** Sets the connected state for the given package name. */
-    private void setAppConnected(String packageName, boolean isConnected) {
-        when(mHealthConnectPermissionHelper.hasGrantedHealthPermissions(
-                        eq(packageName), eq(mUserHandle)))
+    private void setAppConnectedFitnessPermission(String packageName, boolean isConnected) {
+        when(mHealthConnectPermissionHelper.hasGrantedFitnessPermission(
+                        argThat(
+                                argument ->
+                                        argument != null
+                                                && packageName.equals(argument.packageName))))
                 .thenReturn(isConnected);
+        if (isConnected) {
+            setAppRequestsFitnessPermission(packageName, true);
+        }
+    }
+
+    private void setAppRequestsFitnessPermission(String packageName, boolean requests) {
+        when(mHealthConnectPermissionHelper.isRequestingFitnessPermission(
+                        argThat(
+                                argument ->
+                                        argument != null
+                                                && packageName.equals(argument.packageName))))
+                .thenReturn(requests);
     }
 
     private void setAppUsedWithData(String packageName) {
@@ -392,9 +444,22 @@
 
     /** Helper method to create a PackageInfo object. */
     private PackageInfo createPackageInfo(String packageName, long firstInstallTime) {
+        String[] defaultPermissions = {
+            HealthPermissions.READ_ACTIVE_CALORIES_BURNED,
+            HealthPermissions.READ_STEPS,
+            HealthPermissions.WRITE_BLOOD_PRESSURE,
+            HealthPermissions.READ_HEALTH_DATA_HISTORY,
+            HealthPermissions.WRITE_MEDICAL_DATA
+        };
+        return createPackageInfo(packageName, firstInstallTime, defaultPermissions);
+    }
+
+    private PackageInfo createPackageInfo(
+            String packageName, long firstInstallTime, String[] requestedPermissions) {
         PackageInfo pkgInfo = new PackageInfo();
         pkgInfo.packageName = packageName;
         pkgInfo.firstInstallTime = firstInstallTime;
+        pkgInfo.requestedPermissions = requestedPermissions;
         return pkgInfo;
     }
 
diff --git a/tests/unittests/src/com/android/server/healthconnect/permission/HealthConnectPermissionHelperTest.java b/tests/unittests/src/com/android/server/healthconnect/permission/HealthConnectPermissionHelperTest.java
index 0f07e5e..16d2a7b 100644
--- a/tests/unittests/src/com/android/server/healthconnect/permission/HealthConnectPermissionHelperTest.java
+++ b/tests/unittests/src/com/android/server/healthconnect/permission/HealthConnectPermissionHelperTest.java
@@ -1777,6 +1777,110 @@
                         eq(CURRENT_USER));
     }
 
+    @Test
+    public void isRequestingFitnessPermission_whenOneFitnessRequested_returnsTrue()
+            throws PackageManager.NameNotFoundException {
+        PackageInfo mockPackageInfo = new PackageInfo();
+        mockPackageInfo.requestedPermissions =
+                new String[] {
+                    HealthPermissions.READ_HEART_RATE,
+                    HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND,
+                    HealthPermissions.READ_HEALTH_DATA_HISTORY,
+                    HealthPermissions.WRITE_STEPS,
+                    HealthPermissions.WRITE_MEDICAL_DATA
+                };
+        when(mPackageManager.getPackageInfo(eq(HC_PACKAGE_NAME), any()))
+                .thenReturn(mockPackageInfo);
+
+        assertTrue(mPermissionHelper.isRequestingFitnessPermission(mockPackageInfo));
+    }
+
+    @Test
+    public void isRequestingFitnessPermission_whenNoPermissionsRequested_returnsFalse()
+            throws PackageManager.NameNotFoundException {
+        PackageInfo mockPackageInfo = new PackageInfo();
+        // For now add a few of the HealthPermissions just for the test.
+        mockPackageInfo.requestedPermissions = new String[] {};
+        when(mPackageManager.getPackageInfo(eq(HC_PACKAGE_NAME), any()))
+                .thenReturn(mockPackageInfo);
+
+        assertFalse(mPermissionHelper.isRequestingFitnessPermission(mockPackageInfo));
+    }
+
+    @Test
+    public void isRequestingFitnessPermission_whenOnlyMedicalAndAdditionalRequested_returnsFalse()
+            throws PackageManager.NameNotFoundException {
+        PackageInfo mockPackageInfo = new PackageInfo();
+        mockPackageInfo.requestedPermissions =
+                new String[] {
+                    HealthPermissions.READ_MEDICAL_DATA_PREGNANCY,
+                    HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND,
+                    HealthPermissions.READ_HEALTH_DATA_HISTORY,
+                    HealthPermissions.WRITE_MEDICAL_DATA
+                };
+        when(mPackageManager.getPackageInfo(eq(HC_PACKAGE_NAME), any()))
+                .thenReturn(mockPackageInfo);
+
+        assertFalse(mPermissionHelper.isRequestingFitnessPermission(mockPackageInfo));
+    }
+
+    @Test
+    public void hasGrantedFitnessPermission_whenOneFitnessGranted_returnsTrue()
+            throws PackageManager.NameNotFoundException {
+        PackageInfo mockPackageInfo =
+                getMockPackageInfo(
+                        Build.VERSION_CODES.BAKLAVA,
+                        new String[] {
+                            HealthPermissions.READ_HEART_RATE,
+                            HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND,
+                        },
+                        new int[] {
+                            PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                            PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                        });
+        when(mPackageManager.getPackageInfo(eq(TEST_PACKAGE_NAME), any()))
+                .thenReturn(mockPackageInfo);
+        assertTrue(mPermissionHelper.hasGrantedFitnessPermission(mockPackageInfo));
+    }
+
+    @Test
+    public void hasGrantedFitnessPermission_whenNoPermissionsGranted_returnsFalse()
+            throws PackageManager.NameNotFoundException {
+        PackageInfo mockPackageInfo =
+                getMockPackageInfo(
+                        Build.VERSION_CODES.BAKLAVA,
+                        new String[] {
+                            HealthPermissions.READ_HEART_RATE,
+                            HealthPermissions.WRITE_MEDICAL_DATA,
+                            HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND,
+                        },
+                        new int[] {0, 0, 0});
+        when(mPackageManager.getPackageInfo(eq(TEST_PACKAGE_NAME), any()))
+                .thenReturn(mockPackageInfo);
+        assertFalse(mPermissionHelper.hasGrantedFitnessPermission(mockPackageInfo));
+    }
+
+    @Test
+    public void hasGrantedFitnessPermission_whenOnlyMedicalAndAdditionalGranted_returnsFalse()
+            throws PackageManager.NameNotFoundException {
+        PackageInfo mockPackageInfo =
+                getMockPackageInfo(
+                        Build.VERSION_CODES.BAKLAVA,
+                        new String[] {
+                            HealthPermissions.READ_HEART_RATE,
+                            HealthPermissions.WRITE_MEDICAL_DATA,
+                            HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND,
+                        },
+                        new int[] {
+                            0,
+                            PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                            PackageInfo.REQUESTED_PERMISSION_GRANTED
+                        });
+        when(mPackageManager.getPackageInfo(eq(TEST_PACKAGE_NAME), any()))
+                .thenReturn(mockPackageInfo);
+        assertFalse(mPermissionHelper.hasGrantedFitnessPermission(mockPackageInfo));
+    }
+
     private void setUpHealthPermissions() throws PackageManager.NameNotFoundException {
         PackageInfo mockPackageInfo = new PackageInfo();
         // For now add a few of the HealthPermissions just for the test.