diff options
9 files changed, 284 insertions, 4 deletions
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/utils/Utils.java b/PermissionController/src/com/android/permissioncontroller/permission/utils/Utils.java index 38495f3b3..327142896 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/utils/Utils.java +++ b/PermissionController/src/com/android/permissioncontroller/permission/utils/Utils.java @@ -39,6 +39,7 @@ import static android.content.Intent.EXTRA_REASON; import static android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT; import static android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT; import static android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT; +import static android.content.pm.PackageManager.FLAG_PERMISSION_REVOKE_WHEN_REQUESTED; import static android.content.pm.PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED; import static android.content.pm.PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED; import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY; @@ -78,6 +79,7 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.hardware.SensorPrivacyManager; import android.health.connect.HealthConnectManager; +import android.health.connect.HealthPermissions; import android.os.Binder; import android.os.Build; import android.os.Parcelable; @@ -1107,17 +1109,31 @@ public final class Utils { return false; } + // Always show Fitness&Wellness chip on Wear. + if (Flags.replaceBodySensorPermissionEnabled() + && pm.hasSystemFeature(PackageManager.FEATURE_WATCH)) { + return true; + } + // Check in permission is already granted as we should not hide it in the UX at that point. List<String> grantedPermissions = packageInfo.getGrantedPermissions(); for (PermissionInfo permission : permissions) { boolean isCurrentlyGranted = grantedPermissions.contains(permission.name); if (isCurrentlyGranted) { - Log.d(LOG_TAG, "At least one Health permission group permission is granted, " + Log.d( + LOG_TAG, + "At least one Health permission group permission is granted, " + "show permission group entry"); return true; } } + // When none health permission is granted, exempt health permission view usage intent filter + // if all the requested health permissions are from permission splits. + if (isRequestFromSplitHealthPermission(packageInfo)) { + return true; + } + Intent viewUsageIntent = new Intent(Intent.ACTION_VIEW_PERMISSION_USAGE); viewUsageIntent.addCategory(HealthConnectManager.CATEGORY_HEALTH_PERMISSIONS); viewUsageIntent.setPackage(packageInfo.getPackageName()); @@ -1132,6 +1148,71 @@ public final class Utils { } /** + * Returns true if the request is being made as the result of a split health permission from + * BODY_SENSORS call. + */ + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + private static boolean isRequestFromSplitHealthPermission(LightPackageInfo packageInfo) { + // Sdk check to make sure HealthConnectManager.isHealthPermission() is supported. + if (!SdkLevel.isAtLeastU() || !Flags.replaceBodySensorPermissionEnabled()) { + return false; + } + + PermissionControllerApplication app = PermissionControllerApplication.get(); + PackageManager pm = app.getPackageManager(); + String packageName = packageInfo.getPackageName(); + UserHandle user = UserHandle.getUserHandleForUid(packageInfo.getUid()); + Context context = Utils.getUserContext(app, user); + + List<String> requestedHealthPermissions = new ArrayList<>(); + for (String permission : packageInfo.getRequestedPermissions()) { + if (HealthConnectManager.isHealthPermission(context, permission)) { + requestedHealthPermissions.add(permission); + } + } + + // Split permission only applies to READ_HEART_RATE. + if (!requestedHealthPermissions.contains(HealthPermissions.READ_HEART_RATE)) { + return false; + } + + // If there are other health permissions (other than READ_HEALTH_DATA_IN_BACKGROUND) + // don't consider this a pure split-permission request. + if (requestedHealthPermissions.size() > 2) { + return false; + } + + boolean isBackgroundPermissionRequested = + requestedHealthPermissions.contains( + HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND); + // If there are two health permissions declared, make sure the other is + // READ_HEALTH_DATA_IN_BACKGROUND. + if (requestedHealthPermissions.size() == 2 && !isBackgroundPermissionRequested) { + return false; + } + + // If READ_HEALTH_DATA_IN_BACKGROUND is requested, check permission flag to see if is from + // split permission. + if (isBackgroundPermissionRequested) { + int readHealthDataInBackgroundFlag = + pm.getPermissionFlags( + HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND, packageName, user); + if (!isFromSplitPermission(readHealthDataInBackgroundFlag)) { + return false; + } + } + + // Check READ_HEART_RATE permission flag to see if is from split permission. + int readHeartRateFlag = + pm.getPermissionFlags(HealthPermissions.READ_HEART_RATE, packageName, user); + return isFromSplitPermission(readHeartRateFlag); + } + + private static boolean isFromSplitPermission(int permissionFlag) { + return (permissionFlag & FLAG_PERMISSION_REVOKE_WHEN_REQUESTED) != 0; + } + + /** * Get a device protected storage based shared preferences. Avoid storing sensitive data in it. * * @param context the context to get the shared preferences diff --git a/PermissionController/tests/permissionui/Android.bp b/PermissionController/tests/permissionui/Android.bp index e0e8fed10..8cc91bd99 100644 --- a/PermissionController/tests/permissionui/Android.bp +++ b/PermissionController/tests/permissionui/Android.bp @@ -72,6 +72,8 @@ android_test { ":PermissionUiUseAdditionalPermissionApp", ":PermissionUiUseTwoAdditionalPermissionsApp", ":PermissionUiReadCalendarPermissionApp", + ":PermissionUiUseLegacyBodySensorsPermissionApp", + ":PermissionUiUseReadHeartRatePermissionApp", ], per_testcase_directory: true, } diff --git a/PermissionController/tests/permissionui/AndroidTest.xml b/PermissionController/tests/permissionui/AndroidTest.xml index a4aa03abe..9cadbd12f 100644 --- a/PermissionController/tests/permissionui/AndroidTest.xml +++ b/PermissionController/tests/permissionui/AndroidTest.xml @@ -57,6 +57,10 @@ value="/data/local/tmp/pc-permissionui/PermissionUiUseTwoAdditionalPermissionsApp.apk" /> <option name="push-file" key="PermissionUiReadCalendarPermissionApp.apk" value="/data/local/tmp/pc-permissionui/PermissionUiReadCalendarPermissionApp.apk" /> + <option name="push-file" key="PermissionUiUseLegacyBodySensorsPermissionApp.apk" + value="/data/local/tmp/pc-permissionui/PermissionUiUseLegacyBodySensorsPermissionApp.apk" /> + <option name="push-file" key="PermissionUiUseReadHeartRatePermissionApp.apk" + value="/data/local/tmp/pc-permissionui/PermissionUiUseReadHeartRatePermissionApp.apk" /> </target_preparer> <!-- Uninstall test-apps --> diff --git a/PermissionController/tests/permissionui/PermissionUiUseLegacyBodySensorsPermissionApp/Android.bp b/PermissionController/tests/permissionui/PermissionUiUseLegacyBodySensorsPermissionApp/Android.bp new file mode 100644 index 000000000..eb0e2d0fc --- /dev/null +++ b/PermissionController/tests/permissionui/PermissionUiUseLegacyBodySensorsPermissionApp/Android.bp @@ -0,0 +1,34 @@ +// +// Copyright (C) 2025 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "packages_modules_Permission_PermissionController_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: [ + "packages_modules_Permission_PermissionController_license", + ], +} + +android_test_helper_app { + name: "PermissionUiUseLegacyBodySensorsPermissionApp", + + srcs: ["src/**/*.kt"], + + sdk_version: "34", +} diff --git a/PermissionController/tests/permissionui/PermissionUiUseLegacyBodySensorsPermissionApp/AndroidManifest.xml b/PermissionController/tests/permissionui/PermissionUiUseLegacyBodySensorsPermissionApp/AndroidManifest.xml new file mode 100644 index 000000000..fac299b3e --- /dev/null +++ b/PermissionController/tests/permissionui/PermissionUiUseLegacyBodySensorsPermissionApp/AndroidManifest.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 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="com.android.permissioncontroller.tests.appthatrequestpermission"> + + <uses-permission android:name="android.permission.BODY_SENSORS" /> + <uses-permission android:name="android.permission.BODY_SENSORS_BACKGROUND" /> + + <application /> +</manifest> diff --git a/PermissionController/tests/permissionui/PermissionUiUseReadHeartRatePermissionApp/Android.bp b/PermissionController/tests/permissionui/PermissionUiUseReadHeartRatePermissionApp/Android.bp new file mode 100644 index 000000000..ccc9c9636 --- /dev/null +++ b/PermissionController/tests/permissionui/PermissionUiUseReadHeartRatePermissionApp/Android.bp @@ -0,0 +1,34 @@ +// +// Copyright (C) 2025 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "packages_modules_Permission_PermissionController_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: [ + "packages_modules_Permission_PermissionController_license", + ], +} + +android_test_helper_app { + name: "PermissionUiUseReadHeartRatePermissionApp", + + srcs: ["src/**/*.kt"], + + sdk_version: "34", +} diff --git a/PermissionController/tests/permissionui/PermissionUiUseReadHeartRatePermissionApp/AndroidManifest.xml b/PermissionController/tests/permissionui/PermissionUiUseReadHeartRatePermissionApp/AndroidManifest.xml new file mode 100644 index 000000000..278af1987 --- /dev/null +++ b/PermissionController/tests/permissionui/PermissionUiUseReadHeartRatePermissionApp/AndroidManifest.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 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="com.android.permissioncontroller.tests.appthatrequestpermission"> + + <uses-permission android:name="android.permission.health.READ_HEART_RATE" /> + <uses-permission android:name="android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND" /> + + <application /> +</manifest> diff --git a/PermissionController/tests/permissionui/src/com/android/permissioncontroller/permissionui/ui/HealthConnectAppPermissionFragmentTest.kt b/PermissionController/tests/permissionui/src/com/android/permissioncontroller/permissionui/ui/HealthConnectAppPermissionFragmentTest.kt index b2d47a7d7..d4d4be6ec 100644 --- a/PermissionController/tests/permissionui/src/com/android/permissioncontroller/permissionui/ui/HealthConnectAppPermissionFragmentTest.kt +++ b/PermissionController/tests/permissionui/src/com/android/permissioncontroller/permissionui/ui/HealthConnectAppPermissionFragmentTest.kt @@ -16,10 +16,16 @@ package com.android.permissioncontroller.permissionui.ui +import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.os.Build +import android.permission.flags.Flags.FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED +import android.platform.test.annotations.RequiresFlagsEnabled +import android.platform.test.flag.junit.DeviceFlagsValueProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SdkSuppress +import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.By import com.android.compatibility.common.util.SystemUtil.eventually import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity @@ -28,7 +34,9 @@ import com.android.compatibility.common.util.UiAutomatorUtils2.waitUntilObjectGo import com.android.permissioncontroller.permissionui.wakeUpScreen import org.junit.After import org.junit.Assume.assumeFalse +import org.junit.Assume.assumeTrue import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -41,10 +49,17 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake") class HealthConnectAppPermissionFragmentTest : BasePermissionUiTest() { + + @Rule @JvmField val mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + + private lateinit var context: Context + @Before fun assumeNotTelevision() = assumeFalse(isTelevision) @Before - fun wakeScreenUp() { + fun setUp() { + context = InstrumentationRegistry.getInstrumentation().context + wakeUpScreen() } @@ -52,8 +67,10 @@ class HealthConnectAppPermissionFragmentTest : BasePermissionUiTest() { fun uninstallTestApp() { uninstallTestApps() } + @Test - fun usedHealthConnectPermissionsAreListed() { + fun usedHealthConnectPermissionsAreListed_handHeldDevices() { + assumeFalse(context.packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)) installTestAppThatUsesHealthConnectPermission() startManageAppPermissionsActivity() @@ -62,7 +79,8 @@ class HealthConnectAppPermissionFragmentTest : BasePermissionUiTest() { } @Test - fun invalidUngrantedUsedHealthConnectPermissionsAreNotListed() { + fun invalidUngrantedUsedHealthConnectPermissionsAreNotListed_handHeldDevices() { + assumeFalse(context.packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)) installInvalidTestAppThatUsesHealthConnectPermission() startManageAppPermissionsActivity() @@ -70,6 +88,54 @@ class HealthConnectAppPermissionFragmentTest : BasePermissionUiTest() { waitUntilObjectGone(By.text(HEALTH_CONNECT_LABEL), TIMEOUT_SHORT) } + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.BAKLAVA, codeName = "Baklava") + @RequiresFlagsEnabled(FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED) + @Test + fun startManageAppPermissionsActivity_wearDevices_requestLegacyBodySensorsUngranted_fitnessAndWellnessShowsUp() { + assumeTrue(context.packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)) + installTestAppThatUsesLegacyBodySensorsPermissions() + + startManageAppPermissionsActivity() + + eventually { waitFindObject(By.text(FITNESS_AND_WELLNESS_LABEL)) } + } + + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.BAKLAVA, codeName = "Baklava") + @RequiresFlagsEnabled(FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED) + @Test + fun startManageAppPermissionsActivity_wearDevices_requestReadHeartRateUngranted_fitnessAndWellnessShowsUp() { + assumeTrue(context.packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)) + installTestAppThatUsesReadHeartRatePermissions() + + startManageAppPermissionsActivity() + + eventually { waitFindObject(By.text(FITNESS_AND_WELLNESS_LABEL)) } + } + + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.BAKLAVA, codeName = "Baklava") + @RequiresFlagsEnabled(FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED) + @Test + fun startManageAppPermissionsActivity_handHeldDevices_requestLegacyBodySensorsUngranted_healthConnectShowsUp() { + assumeFalse(context.packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)) + installTestAppThatUsesLegacyBodySensorsPermissions() + + startManageAppPermissionsActivity() + + eventually { waitFindObject(By.text(HEALTH_CONNECT_LABEL)) } + } + + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.BAKLAVA, codeName = "Baklava") + @RequiresFlagsEnabled(FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED) + @Test + fun startManageAppPermissionsActivity_handHeldDevices_requestReadHeartRateUngranted_healthConnectNotShowsUp() { + assumeFalse(context.packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)) + installTestAppThatUsesReadHeartRatePermissions() + + startManageAppPermissionsActivity() + + waitUntilObjectGone(By.text(HEALTH_CONNECT_LABEL), TIMEOUT_SHORT) + } + private fun startManageAppPermissionsActivity() { runWithShellPermissionIdentity { instrumentationContext.startActivity( @@ -83,6 +149,7 @@ class HealthConnectAppPermissionFragmentTest : BasePermissionUiTest() { } companion object { + private const val FITNESS_AND_WELLNESS_LABEL = "Fitness and wellness" // Health connect label uses a non breaking space private const val HEALTH_CONNECT_LABEL = "Health\u00A0Connect" private const val HEALTH_CONNECT_PERMISSION_READ_FLOORS_CLIMBED = diff --git a/PermissionController/tests/permissionui/src/com/android/permissioncontroller/permissionui/ui/TestAppUtils.kt b/PermissionController/tests/permissionui/src/com/android/permissioncontroller/permissionui/ui/TestAppUtils.kt index 7227ea3d8..8eef11e73 100644 --- a/PermissionController/tests/permissionui/src/com/android/permissioncontroller/permissionui/ui/TestAppUtils.kt +++ b/PermissionController/tests/permissionui/src/com/android/permissioncontroller/permissionui/ui/TestAppUtils.kt @@ -36,6 +36,10 @@ private const val TWO_ADDITIONAL_PERM_USER_APK = "$APK_DIRECTORY/PermissionUiUseTwoAdditionalPermissionsApp.apk" private const val ADDITIONAL_PERM_DEFINER_APK = "$APK_DIRECTORY/PermissionUiDefineAdditionalPermissionApp.apk" +private const val LEGACY_BODY_SENSORS_APK = + "$APK_DIRECTORY/PermissionUiUseLegacyBodySensorsPermissionApp.apk" +private const val READ_HEART_RATE_APK = + "$APK_DIRECTORY/PermissionUiUseReadHeartRatePermissionApp.apk" // All 4 of the AppThatUses_X_Permission(s) applications share the same package name. private const val PERM_DEFINER_PACKAGE = @@ -69,6 +73,10 @@ fun installTestAppThatUsesTwoAdditionalPermissions() = install(TWO_ADDITIONAL_PE fun installTestAppThatDefinesAdditionalPermissions() = install(ADDITIONAL_PERM_DEFINER_APK) +fun installTestAppThatUsesLegacyBodySensorsPermissions() = install(LEGACY_BODY_SENSORS_APK) + +fun installTestAppThatUsesReadHeartRatePermissions() = install(READ_HEART_RATE_APK) + fun uninstallTestApps() { uninstallApp(PERM_USER_PACKAGE) uninstallApp(PERM_DEFINER_PACKAGE) |