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 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 & 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 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 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 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 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);
+ }
}