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.