From 858ef9e1829818b6c242d98596cec473b29d2b2b Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Fri, 15 Mar 2024 12:22:04 +0000 Subject: [PATCH] RMET-3190 H&F Plugin - Use exact alarms for background jobs (#109) * feat: declare SCHEDULE_EXACT_ALARM permission in manifest Context: We're using exact alarms to schedule background jobs to run, so we need to declare this permission in the AndroidManifest.xml file of the app. References: https://outsystemsrd.atlassian.net/browse/RMET-3190 * feat: request SCHEDULE_EXACT_ALARM permission when setting background job References: https://outsystemsrd.atlassian.net/browse/RMET-3190 * feat: handle SCHEDULE_EXACT_ALARM permission not being given when setting a background job Context: As for the time being the only way we have to set background jobs to run is using exact alarms, if the permission is not given, then we should return an error. References: https://outsystemsrd.atlassian.net/browse/RMET-3190 * feat: ask for SCHEDULE_EXACT_ALARM permission before other ones References: https://outsystemsrd.atlassian.net/browse/RMET-3190 * feat: get foreground notification info from strings.xml file References: https://outsystemsrd.atlassian.net/browse/RMET-3190 * feat: replace workManager with alarmManager Context: We're now using exact alarms for background jobs, so we need to use the AlarmManager. References: https://outsystemsrd.atlassian.net/browse/RMET-3190 * feat: pass context parameter to AdvancedQuery, as it is needed References: https://outsystemsrd.atlassian.net/browse/RMET-3190 * chore: update dependency to H&F Android library References: https://outsystemsrd.atlassian.net/browse/RMET-3190 * chore: update dependency to H&F Android lib References: https://outsystemsrd.atlassian.net/browse/RMET-3190 * chore: add explanatory comment References: https://outsystemsrd.atlassian.net/browse/RMET-3190 * chore: update changelog References: https://outsystemsrd.atlassian.net/browse/RMET-3190 --- CHANGELOG.md | 3 + hooks/androidCopyPreferencesPermissions.js | 1 + src/android/build.gradle | 4 +- .../plugins/healthfitness/OSHealthFitness.kt | 109 ++++++++++++++++-- 4 files changed, 105 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 116d57d5..ebddeaa6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ The changes documented here do not include those from the original repository. ## [Unreleased] +## 2024-03-14 +- Implemented the usage of exact alarms for background jobs (https://outsystemsrd.atlassian.net/browse/RMET-3190). + ## 2024-02-28 - Implemented `Open Health Connect App` (https://outsystemsrd.atlassian.net/browse/RMET-3158). diff --git a/hooks/androidCopyPreferencesPermissions.js b/hooks/androidCopyPreferencesPermissions.js index db84930f..2fecef2b 100644 --- a/hooks/androidCopyPreferencesPermissions.js +++ b/hooks/androidCopyPreferencesPermissions.js @@ -275,6 +275,7 @@ function addBackgroundJobPermissionsToManifest(configParser, projectRoot, parser addEntryToManifest(manifestXmlDoc, 'android.permission.FOREGROUND_SERVICE') addEntryToManifest(manifestXmlDoc, 'android.permission.FOREGROUND_SERVICE_HEALTH') addEntryToManifest(manifestXmlDoc, 'android.permission.HIGH_SAMPLING_RATE_SENSORS') + addEntryToManifest(manifestXmlDoc, 'android.permission.SCHEDULE_EXACT_ALARM') // serialize the updated XML document back to string const serializer = new XMLSerializer(); diff --git a/src/android/build.gradle b/src/android/build.gradle index d6d8b545..a8b5e498 100644 --- a/src/android/build.gradle +++ b/src/android/build.gradle @@ -24,8 +24,8 @@ dependencies{ implementation 'com.google.code.findbugs:jsr305:1.3.9' implementation("com.github.outsystems:oscore-android:1.2.0@aar") - implementation("com.github.outsystems:oscordova-android:1.2.0@aar") - implementation("com.github.outsystems:oshealthfitness-android:1.2.0.20@aar") + implementation("com.github.outsystems:oscordova-android:2.0.1@aar") + implementation("com.github.outsystems:oshealthfitness-android:1.2.0.22@aar") implementation("com.github.outsystems:osnotificationpermissions-android:0.0.4@aar") // activity diff --git a/src/android/com/outsystems/plugins/healthfitness/OSHealthFitness.kt b/src/android/com/outsystems/plugins/healthfitness/OSHealthFitness.kt index 80819e1d..149400c0 100755 --- a/src/android/com/outsystems/plugins/healthfitness/OSHealthFitness.kt +++ b/src/android/com/outsystems/plugins/healthfitness/OSHealthFitness.kt @@ -1,10 +1,13 @@ package com.outsystems.plugins.healthfitness import android.Manifest +import android.app.AlarmManager +import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.os.Build import android.os.Build.VERSION.SDK_INT +import android.provider.Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM import androidx.core.content.ContextCompat import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability @@ -25,12 +28,24 @@ class OSHealthFitness : CordovaImplementation() { var healthStore: HealthStoreInterface? = null val gson by lazy { Gson() } - lateinit var healthConnectViewModel: HealthConnectViewModel - lateinit var healthConnectRepository: HealthConnectRepository - lateinit var healthConnectDataManager: HealthConnectDataManager - lateinit var healthConnectHelper: HealthConnectHelper - lateinit var workManagerHelper: WorkManagerHelperInterface - lateinit var backgroundParameters: BackgroundJobParameters + private lateinit var healthConnectViewModel: HealthConnectViewModel + private lateinit var healthConnectRepository: HealthConnectRepository + private lateinit var healthConnectDataManager: HealthConnectDataManager + private lateinit var healthConnectHelper: HealthConnectHelper + private lateinit var alarmManagerHelper: AlarmManagerHelper + private lateinit var backgroundParameters: BackgroundJobParameters + + private lateinit var alarmManager: AlarmManager + + // we need this variable because onResume is being called when + // returning from the SCHEDULE_EXACT_ALARM permission screen + private var requestingExactAlarmPermission = false + + // variables to hold foreground notification title and description + // these values are defined in build time so we only need to read + // them once on the initialize method + private lateinit var foregroundNotificationTitle: String + private lateinit var foregroundNotificationDescription: String override fun initialize(cordova: CordovaInterface, webView: CordovaWebView) { super.initialize(cordova, webView) @@ -41,9 +56,27 @@ class OSHealthFitness : CordovaImplementation() { healthConnectDataManager = HealthConnectDataManager(database) healthConnectRepository = HealthConnectRepository(healthConnectDataManager) healthConnectHelper = HealthConnectHelper() - workManagerHelper = WorkManagerHelper() + alarmManagerHelper = AlarmManagerHelper() healthConnectViewModel = - HealthConnectViewModel(healthConnectRepository, healthConnectHelper, workManagerHelper) + HealthConnectViewModel(healthConnectRepository, healthConnectHelper, alarmManagerHelper) + alarmManager = getContext().getSystemService(Context.ALARM_SERVICE) as AlarmManager + + // get foreground notification title and description from resources (strings.xml) + foregroundNotificationTitle = getContext().resources.getString( + getActivity().resources.getIdentifier( + "background_notification_title", + "string", + getActivity().packageName + ) + ) + foregroundNotificationDescription = getContext().resources.getString( + getActivity().resources.getIdentifier( + "background_notification_description", + "string", + getActivity().packageName + ) + ) + } override fun execute( @@ -96,6 +129,14 @@ class OSHealthFitness : CordovaImplementation() { return true } + // onResume is called when returning from the SCHEDULE_EXACT_ALARM permission screen + override fun onResume(multitasking: Boolean) { + if (requestingExactAlarmPermission) { + requestingExactAlarmPermission = false + onScheduleExactAlarmPermissionResult() + } + } + private fun initAndRequestPermissions(args: JSONArray) { try { healthConnectViewModel.initAndRequestPermissions( @@ -178,6 +219,7 @@ class OSHealthFitness : CordovaImplementation() { val parameters = gson.fromJson(args.getString(0), HealthAdvancedQueryParameters::class.java) healthConnectViewModel.advancedQuery( parameters, + getContext(), { response -> val pluginResponseJson = gson.toJson(response) sendPluginResult(pluginResponseJson) @@ -227,10 +269,31 @@ class OSHealthFitness : CordovaImplementation() { } + /** + * Navigates to the permission screen for exact alarms or + * skips it and request the other necessary permissions. + * Also stores the background job parameters in a global variable to be used later. + */ private fun setBackgroundJob(args: JSONArray) { // save arguments for later use backgroundParameters = gson.fromJson(args.getString(0), BackgroundJobParameters::class.java) + //request permission for exact alarms if necessary + if (SDK_INT >= 31 && !alarmManager.canScheduleExactAlarms()) { + requestingExactAlarmPermission = true + // we only need to request this permission if exact alarms need to be used + // when there's another way to schedule background jobs to run, we can avoid this for some variables (e.g. steps) + // we intended to use the Activity Recognition API, but it currently has a bug already reported to Google + getContext().startActivity(Intent(ACTION_REQUEST_SCHEDULE_EXACT_ALARM)); + } else { // we can move on to other permissions if we don't need to request exact alarm permissions + requestBackgroundJobPermissions() + } + } + + /** + * Requests the POST_NOTIFICATIONS and ACTIVITY_RECOGNITION permissions. + */ + private fun requestBackgroundJobPermissions() { val permissions = mutableListOf().apply { if (SDK_INT >= 33) { add(Manifest.permission.POST_NOTIFICATIONS) @@ -243,9 +306,34 @@ class OSHealthFitness : CordovaImplementation() { PermissionHelper.requestPermissions(this, BACKGROUND_JOB_PERMISSIONS_REQUEST_CODE, permissions) } + /** + * Handles user response to exact alarm permission request. + * + */ + private fun onScheduleExactAlarmPermissionResult() { + val permissionDenied = SDK_INT >= 31 && !alarmManager.canScheduleExactAlarms() + if (permissionDenied) { + // send plugin result with error + sendPluginResult( + null, + Pair( + HealthFitnessError.BACKGROUND_JOB_EXACT_ALARM_PERMISSION_DENIED_ERROR.code.toString(), + HealthFitnessError.BACKGROUND_JOB_EXACT_ALARM_PERMISSION_DENIED_ERROR.message + ) + ) + return + } + requestBackgroundJobPermissions() + } + + /** + * Sets a background job by calling the setBackgroundJob method of the ViewModel + */ private fun setBackgroundJobWithParameters(parameters: BackgroundJobParameters) { healthConnectViewModel.setBackgroundJob( parameters, + foregroundNotificationTitle, + foregroundNotificationDescription, getContext(), { sendPluginResult("success", null) @@ -260,6 +348,7 @@ class OSHealthFitness : CordovaImplementation() { val jobId = args.getString(0) healthConnectViewModel.deleteBackgroundJob( jobId, + getContext(), { sendPluginResult("success", null) }, @@ -323,7 +412,7 @@ class OSHealthFitness : CordovaImplementation() { } ) } - + private fun openHealthConnect() { healthConnectViewModel.openHealthConnect( getContext(), @@ -336,7 +425,7 @@ class OSHealthFitness : CordovaImplementation() { ) } - override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent) { + override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) { super.onActivityResult(requestCode, resultCode, intent) healthConnectViewModel.handleActivityResult(requestCode, resultCode, intent, {