diff --git a/CHANGELOG.md b/CHANGELOG.md index 5396eb40..c678b66d 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, {