Skip to content

Commit

Permalink
RMET- 3142 H&F Plugin - Hook for permissions (#106)
Browse files Browse the repository at this point in the history
* feat: first implementation of androidCopyPreferencesPermissions hook

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* fix: fix path to hook file

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* fix: add dependency to xmldom

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* feat: implement first version of hook processing preferences

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* refactor: remove unnecessary code

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* fix: fix comparator in condition

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* test: add log for troubleshooting

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* test: add log for troubleshooting

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* test: add log for troubleshooting

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* fix: use "" instead of null in comparison

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* refactor: remove logs

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* refactor: remove logs and comments

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* feat: add permissions code for Android <= 13

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* fix: fix variable name

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* feat: add background permissions to AndroidManifest.xml file

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* fix: properly pass DOMParser to functions

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* fix: fix condition in if for background job permissions

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* feat: remove unnecessary permissions

Context: The new version of the H&F plugin, which uses Health Connect, no longer needs these permissions by default.

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* test: add logs for troubleshooting

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* fix: fix if condition

Context: We only want to not include the permissions if DisableBackgroundJobs is exactly equal to "true"

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* feat: copy notification content to strings.xml

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* fix: fix query selector

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* fix: properly look for string tags

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* test: add logs for troubleshooting

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* misc: add log for troubleshooting

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* misc: add logs for troubleshooting

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* feat: use different way of setting texts in strings.xml

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* refactor: remove logs and comments

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* fix: replace const with var

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* feat: add PermissionsRationaleActivity to AndroidManifest.xml

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* feat: also use default value for notificationDescription

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* fix: add missing permissions for background jobs

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* chore: update changelog

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* refactor: use correct english term

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* fix: include necessary dependencies

Context: Because dependencies from our Health and Fitness Android library are not being transitive, we need to include them in the plugin's build.gradle file so that they're included in the app's build gradle.

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* fix: include dependencies for Jetpack Compose in build.gradle

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* refactor: fix typo

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* feat: add helper method to avoid code replication

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* fix: pass XML Documents to helper function

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* fix: pass necessary parameter to helper function

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* refactor: use helper methods to avoid code repetition

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* refactor: use helper functions and maps to avoid code repetition and improve overall quality

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* feat: populate objects with correct values

References: https://outsystemsrd.atlassian.net/browse/RMET-3142

* refactor: remove unused requires

References: https://outsystemsrd.atlassian.net/browse/RMET-3142
  • Loading branch information
alexgerardojacinto committed Apr 9, 2024
1 parent 5211a78 commit 8f24133
Show file tree
Hide file tree
Showing 5 changed files with 379 additions and 7 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ The changes documented here do not include those from the original repository.
## 2024-02-28
- Implemented `Open Health Connect App` (https://outsystemsrd.atlassian.net/browse/RMET-3158).

## 2024-02-27
- Implemented hook for permissions (https://outsystemsrd.atlassian.net/browse/RMET-3142).

## 2024-02-26
- Implemented `Show app's privacy policy dialog` (https://outsystemsrd.atlassian.net/browse/RMET-3145).

Expand Down
339 changes: 339 additions & 0 deletions hooks/androidCopyPreferencesPermissions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,339 @@
const fs = require('fs');
const path = require('path');
const { ConfigParser } = require('cordova-common');
const { DOMParser, XMLSerializer } = require('xmldom');

const READ = "Read"
const WRITE = "Write"
const READWRITE = "ReadWrite"

let permissions = {
HeartRate: {
variableName: "HeartRate",
readPermission: "android.permission.health.READ_HEART_RATE",
writePermission: "android.permission.health.WRITE_HEART_RATE",
configValue: undefined,
// we'll use these to know if we should write group permissions or not
wasSet: false
},
Steps: {
variableName: "Steps",
readPermission: "android.permission.health.READ_STEPS",
writePermission: "android.permission.health.WRITE_STEPS",
configValue: undefined,
wasSet: false
},
Weight: {
variableName: "Weight",
readPermission: "android.permission.health.READ_WEIGHT",
writePermission: "android.permission.health.WRITE_WEIGHT",
configValue: undefined,
wasSet: false
},
Height: {
variableName: "Height",
readPermission: "android.permission.health.READ_HEIGHT",
writePermission: "android.permission.health.WRITE_HEIGHT",
configValue: undefined,
wasSet: false
},
CaloriesBurned: {
variableName: "CaloriesBurned",
readPermission: "android.permission.health.READ_TOTAL_CALORIES_BURNED",
writePermission: "android.permission.health.WRITE_TOTAL_CALORIES_BURNED",
configValue: undefined,
wasSet: false
},
Sleep: {
variableName: "Sleep",
readPermission: "android.permission.health.READ_SLEEP",
writePermission: "android.permission.health.WRITE_SLEEP",
configValue: undefined,
wasSet: false
},
BloodPressure: {
variableName: "BloodPressure",
readPermission: "android.permission.health.READ_BLOOD_PRESSURE",
writePermission: "android.permission.health.WRITE_BLOOD_PRESSURE",
configValue: undefined,
wasSet: false
},
BloodGlucose: {
variableName: "BloodGlucose",
readPermission: "android.permission.health.READ_BLOOD_GLUCOSE",
writePermission: "android.permission.health.WRITE_BLOOD_GLUCOSE",
configValue: undefined,
wasSet: false
},
BodyFatPercentage: {
variableName: "BodyFatPercentage",
readPermission: "android.permission.health.READ_BODY_FAT",
writePermission: "android.permission.health.WRITE_BODY_FAT",
configValue: undefined,
wasSet: false
},
BasalMetabolicRate: {
variableName: "BasalMetabolicRate",
readPermission: "android.permission.health.READ_BASAL_METABOLIC_RATE",
writePermission: "android.permission.health.WRITE_BASAL_METABOLIC_RATE",
configValue: undefined,
wasSet: false
},
WalkingSpeed: {
variableName: "WalkingSpeed",
readPermission: "android.permission.health.READ_SPEED",
writePermission: "android.permission.health.WRITE_SPEED",
configValue: undefined,
wasSet: false
},
Distance: {
variableName: "Distance",
readPermission: "android.permission.health.READ_DISTANCE",
writePermission: "android.permission.health.WRITE_DISTANCE",
configValue: undefined,
wasSet: false
}
}

let groupPermissions = {
AllVariables: {
variableName: "AllVariables",
configValue: undefined,
wasSet: false,
groupVariables: []
},
FitnessVariables: {
variableName: "FitnessVariables",
configValue: undefined,
// we'll use these to know if we should set individual permissions or not
// e.g. when checking HeartRate, if all healthVariables were already set, we don't need to add it again
wasSet: false,
groupVariables: ["Steps", "CaloriesBurned", "WalkingSpeed", "Distance"]
},
HealthVariables: {
variableName: "HealthVariables",
configValue: undefined,
wasSet: false,
groupVariables: ["HeartRate", "Sleep", "BloodPressure", "BloodGlucose"]
},
ProfileVariables: {
variableName: "ProfileVariables",
configValue: undefined,
wasSet: false,
groupVariables: ["Weight", "Height", "BodyFatPercentage", "BasalMetabolicRate"]
}
}

module.exports = async function (context) {
const projectRoot = context.opts.cordova.project ? context.opts.cordova.project.root : context.opts.projectRoot;
const configXML = path.join(projectRoot, 'config.xml');
const configParser = new ConfigParser(configXML);
const parser = new DOMParser();

// add health connect permissions to AndroidManifest.xml and health_permissions.xml files
addHealthConnectPermissionsToXmlFiles(configParser, projectRoot, parser);

// add background job permissions to AndroidManifest.xml
addBackgroundJobPermissionsToManifest(configParser, projectRoot, parser);

// copy notification title and content for notificaiton for Foreground Service
copyNotificationContent(configParser, projectRoot, parser);
};

function addHealthConnectPermissionsToXmlFiles(configParser, projectRoot, parser) {

for(const key in permissions){
permissions[key].configValue = configParser.getPlatformPreference(permissions[key].variableName, 'android');
}

for(const key in groupPermissions){
groupPermissions[key].configValue = configParser.getPlatformPreference(groupPermissions[key].variableName, 'android');
}

// Android >= 14 dependencies should be included directly in the AndroidManifest.xml file
// Read the AndroidManifest.xml file
const manifestFilePath = path.join(projectRoot, 'platforms/android/app/src/main/AndroidManifest.xml');
const manifestXmlString = fs.readFileSync(manifestFilePath, 'utf-8');

// Parse the XML string
const manifestXmlDoc = parser.parseFromString(manifestXmlString, 'text/xml');

// Android <= 13 dependencies should be included in a separate XML file
// Create the health_permissions.xml file
const permissionsXmlDoc = parser.parseFromString('<?xml version="1.0" encoding="utf-8"?><resources><array name="health_permissions"></array></resources>', 'text/xml');

// Get the <array> element
const arrayElement = permissionsXmlDoc.getElementsByTagName('array')[0];

// process each individual variable
for(const key in permissions){
let p = permissions[key]
if (p.configValue == READWRITE || p.configValue == READ) {
p.wasSet = true;
processPermission(manifestXmlDoc, permissionsXmlDoc, arrayElement, p.readPermission)
}
if (p.configValue == READWRITE || p.configValue == WRITE) {
p.wasSet = true;
processPermission(manifestXmlDoc, permissionsXmlDoc, arrayElement, p.writePermission)
}
}

// process group variables
for(const key in groupPermissions){
let p = groupPermissions[key]
if (p.configValue == READWRITE || p.configValue == READ) {
p.wasSet = true;
p.groupVariables.forEach( v => {
if (!permissions[v].wasSet) {
processPermission(manifestXmlDoc, permissionsXmlDoc, arrayElement, permissions[v].readPermission)
}
})
}
if (p.configValue == READWRITE || p.configValue == WRITE) {
p.wasSet = true;
p.groupVariables.forEach( v => {
if (!permissions[v].wasSet) {
processPermission(manifestXmlDoc, permissionsXmlDoc, arrayElement, permissions[v].writePermission)
}
})
}
}

let permissionValues = Object.values(permissions)
let groupPermissionValues = Object.values(groupPermissions)

// process AllVariables
if (groupPermissions.AllVariables.configValue == READWRITE || groupPermissions.AllVariables.configValue == READ) {
processAllVariables(manifestXmlDoc, permissionsXmlDoc, arrayElement, READ, groupPermissionValues)

}

if ((groupPermissions.AllVariables.configValue == READWRITE || groupPermissions.AllVariables.configValue == WRITE)) {
processAllVariables(manifestXmlDoc, permissionsXmlDoc, arrayElement, WRITE, groupPermissionValues)
}

let numberOfPermissions = permissionValues.filter(p => p.configValue != "").length + groupPermissionValues.filter(p => p.configValue != "").length

// if there is no AllVariables nor anything else, then by default we add all the permissions
if (numberOfPermissions == 0) {
permissionValues.forEach( p => {
processPermission(manifestXmlDoc, permissionsXmlDoc, arrayElement, p.readPermission)
processPermission(manifestXmlDoc, permissionsXmlDoc, arrayElement, p.writePermission)
})
}

// Serialize the updated XML document back to string
const serializer = new XMLSerializer();

// Android >= 14
const updatedManifestXmlString = serializer.serializeToString(manifestXmlDoc);

// Write the updated XML string back to the same file
fs.writeFileSync(manifestFilePath, updatedManifestXmlString, 'utf-8');

// Android <= 13
const updatedPermissionsXmlString = serializer.serializeToString(permissionsXmlDoc);
const permissionsXmlFilePath = path.join(projectRoot, 'platforms/android/app/src/main/res/values/health_permissions.xml');

// Write the updated XML string back to the same file
fs.writeFileSync(permissionsXmlFilePath, updatedPermissionsXmlString, 'utf-8');

}

function processAllVariables(manifestXmlDoc, permissionsXmlDoc, arrayElement, permissionOperation, groupPermissionsValues) {
groupPermissionsValues.forEach(p => {
p.groupVariables.forEach( v => {
if (!p.wasSet && !permissions[v].wasSet) {
processPermission(manifestXmlDoc, permissionsXmlDoc, arrayElement, permissionOperation == READ ? permissions[v].readPermission : permissions[v].writePermission)
}
})
})
}

function processPermission(manifestXmlDoc, permissionsXmlDoc, arrayElement, permissionOperation) {
addEntryToManifest(manifestXmlDoc, permissionOperation)
addEntryToPermissionsXML(permissionsXmlDoc, arrayElement, permissionOperation)
}

function addBackgroundJobPermissionsToManifest(configParser, projectRoot, parser) {

const disableBackgroundJobs = configParser.getPlatformPreference('DisableBackgroundJobs', 'android');

// we want to include the permissions by default
// if disableBackgroundJobs == true then we don't want to include the permissions in the manfiest
if (disableBackgroundJobs !== "true") {

const manifestFilePath = path.join(projectRoot, 'platforms/android/app/src/main/AndroidManifest.xml');
const manifestXmlString = fs.readFileSync(manifestFilePath, 'utf-8');

// Parse the XML string
const manifestXmlDoc = parser.parseFromString(manifestXmlString, 'text/xml');

// add permissions to XML document
addEntryToManifest(manifestXmlDoc, 'android.permission.POST_NOTIFICATIONS')
addEntryToManifest(manifestXmlDoc, 'android.permission.ACTIVITY_RECOGNITION')
addEntryToManifest(manifestXmlDoc, 'android.permission.FOREGROUND_SERVICE')
addEntryToManifest(manifestXmlDoc, 'android.permission.FOREGROUND_SERVICE_HEALTH')
addEntryToManifest(manifestXmlDoc, 'android.permission.HIGH_SAMPLING_RATE_SENSORS')

// serialize the updated XML document back to string
const serializer = new XMLSerializer();
const updatedManifestXmlString = serializer.serializeToString(manifestXmlDoc);

// write the updated XML string back to the same file
fs.writeFileSync(manifestFilePath, updatedManifestXmlString, 'utf-8');
}

}

function addEntryToManifest(manifestXmlDoc, permission) {
const newPermission = manifestXmlDoc.createElement('uses-permission');
newPermission.setAttribute('android:name', permission);
manifestXmlDoc.documentElement.appendChild(newPermission);
}

function addEntryToPermissionsXML(permissionsXmlDoc, arrayElement, permission) {
const newItem = permissionsXmlDoc.createElement('item');
const textNode = permissionsXmlDoc.createTextNode(permission);
newItem.appendChild(textNode);
arrayElement.appendChild(newItem);
}

function copyNotificationContent(configParser, projectRoot, parser) {

// get values from config.xml
var notificationTitle = configParser.getPlatformPreference('BackgroundNotificationTitle', 'android');
var notificationDescription = configParser.getPlatformPreference('BackgroundNotificationDescription', 'android');

if (notificationTitle == "") {
notificationTitle = "Measuring your health and fitness data."
}

if (notificationDescription == "") {
notificationDescription = "The app is running in the background."
}

// insert values in strings.xml
const stringsXmlPath = path.join(projectRoot, 'platforms/android/app/src/main/res/values/strings.xml');
const stringsXmlString = fs.readFileSync(stringsXmlPath, 'utf-8');
const stringsXmlDoc = parser.parseFromString(stringsXmlString, 'text/xml')
const stringElements = stringsXmlDoc.getElementsByTagName('string');

// set text for each <string> element
for (let i = 0; i < stringElements.length; i++) {
const name = stringElements[i].getAttribute('name');
if (name == "background_notification_title") {
stringElements[i].textContent = notificationTitle;
}
else if (name == "background_notification_description") {
stringElements[i].textContent = notificationDescription;
}
}

// serialize the updated XML document back to string
const serializer = new XMLSerializer();
const updatedXmlString = serializer.serializeToString(stringsXmlDoc);

// write the updated XML string back to the same file
fs.writeFileSync(stringsXmlPath, updatedXmlString, 'utf-8');
}
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,8 @@
"ios"
]
},
"engines": []
"engines": [],
"dependencies": {
"xmldom": "^0.6.0"
}
}
22 changes: 16 additions & 6 deletions plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
</js-module>
<platform name="android">

<hook type="after_prepare" src="hooks/androidCopyPreferencesPermissions.js" />
<hook type="after_prepare" src="hooks/androidCopyPrivacyUrlEnv.js"/>

<config-file parent="/*" target="res/xml/config.xml">
Expand All @@ -22,14 +23,23 @@

<config-file parent="/*" target="res/values/strings.xml">
<string name="privacy_policy_url">PRIVACY_POLICY_URL</string>
<string name="background_notification_title"></string>
<string name="background_notification_description"></string>
</config-file>

<config-file parent="/*" target="AndroidManifest.xml">
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
<uses-permission android:name="android.permission.BODY_SENSORS" />
<uses-permission android:name="android.permission.BODY_SENSORS_BACKGROUND" />
</config-file>
<config-file target="AndroidManifest.xml" parent="/manifest/application">
<activity
android:name=".PermissionsRationaleActivity"
android:theme="@style/Theme.AppCompat"
android:exported="true">
<intent-filter>
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
</intent-filter>
<meta-data
android:name="health_permissions"
android:resource="@array/health_permissions" />
</activity>
</config-file>

<!-- HealthFitness Plugin -->
<source-file src="src/android/com/outsystems/plugins/healthfitness/OSHealthFitness.kt" target-dir="app/src/main/kotlin/com/outsystems/plugins/healthfitness"/>
Expand Down
Loading

0 comments on commit 8f24133

Please sign in to comment.