Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RMET- 3142 H&F Plugin - Hook for permissions #106

Merged
merged 46 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
48d119c
feat: first implementation of androidCopyPreferencesPermissions hook
alexgerardojacinto Feb 26, 2024
ce8c7fb
fix: fix path to hook file
alexgerardojacinto Feb 26, 2024
6d5adb7
fix: add dependency to xmldom
alexgerardojacinto Feb 26, 2024
25f59c5
feat: implement first version of hook processing preferences
alexgerardojacinto Feb 26, 2024
42f8e26
refactor: remove unnecessary code
alexgerardojacinto Feb 26, 2024
66a0579
fix: fix comparator in condition
alexgerardojacinto Feb 27, 2024
3a8c9a4
test: add log for troubleshooting
alexgerardojacinto Feb 27, 2024
2a82512
test: add log for troubleshooting
alexgerardojacinto Feb 27, 2024
0f5f490
test: add log for troubleshooting
alexgerardojacinto Feb 27, 2024
66233bb
fix: use "" instead of null in comparison
alexgerardojacinto Feb 27, 2024
ce06243
refactor: remove logs
alexgerardojacinto Feb 27, 2024
1b24274
refactor: remove logs and comments
alexgerardojacinto Feb 27, 2024
93bb61f
feat: add permissions code for Android <= 13
alexgerardojacinto Feb 27, 2024
06aea9e
fix: fix variable name
alexgerardojacinto Feb 27, 2024
4d5fe19
feat: add background permissions to AndroidManifest.xml file
alexgerardojacinto Feb 27, 2024
67f789a
fix: properly pass DOMParser to functions
alexgerardojacinto Feb 27, 2024
6c76b26
fix: fix condition in if for background job permissions
alexgerardojacinto Feb 27, 2024
3639f4c
feat: remove unnecessary permissions
alexgerardojacinto Feb 27, 2024
b73c364
test: add logs for troubleshooting
alexgerardojacinto Feb 27, 2024
81005c4
fix: fix if condition
alexgerardojacinto Feb 27, 2024
6c6eaa5
feat: copy notification content to strings.xml
alexgerardojacinto Feb 27, 2024
27bdfc4
fix: fix query selector
alexgerardojacinto Feb 27, 2024
326b296
fix: properly look for string tags
alexgerardojacinto Feb 27, 2024
2b627fc
test: add logs for troubleshooting
alexgerardojacinto Feb 27, 2024
57ef033
misc: add log for troubleshooting
alexgerardojacinto Feb 27, 2024
1cd29ce
misc: add logs for troubleshooting
alexgerardojacinto Feb 27, 2024
d6c9209
feat: use different way of setting texts in strings.xml
alexgerardojacinto Feb 27, 2024
2cfb12b
refactor: remove logs and comments
alexgerardojacinto Feb 27, 2024
b2645e0
fix: replace const with var
alexgerardojacinto Feb 27, 2024
dd48586
feat: add PermissionsRationaleActivity to AndroidManifest.xml
alexgerardojacinto Feb 27, 2024
71d6e9b
feat: also use default value for notificationDescription
alexgerardojacinto Feb 27, 2024
a5e5350
fix: add missing permissions for background jobs
alexgerardojacinto Feb 27, 2024
e854541
Merge branch 'development' into feat/RMET-3142/hook-permissions
alexgerardojacinto Feb 27, 2024
1f35540
chore: update changelog
alexgerardojacinto Feb 27, 2024
806814e
refactor: use correct english term
alexgerardojacinto Feb 27, 2024
ec7554d
fix: include necessary dependencies
alexgerardojacinto Feb 27, 2024
82b3cdb
fix: include dependencies for Jetpack Compose in build.gradle
alexgerardojacinto Feb 27, 2024
cdb5c6c
refactor: fix typo
alexgerardojacinto Feb 28, 2024
a583d54
feat: add helper method to avoid code replication
alexgerardojacinto Feb 28, 2024
7c928dc
fix: pass XML Documents to helper function
alexgerardojacinto Feb 28, 2024
aa02e3f
fix: pass necessary parameter to helper function
alexgerardojacinto Feb 28, 2024
251451e
refactor: use helper methods to avoid code repetition
alexgerardojacinto Feb 28, 2024
ea92c44
refactor: use helper functions and maps to avoid code repetition and …
alexgerardojacinto Feb 28, 2024
eedc73c
feat: populate objects with correct values
alexgerardojacinto Feb 29, 2024
90ea19a
refactor: remove unused requires
alexgerardojacinto Feb 29, 2024
f75b62b
Merge branch 'development' into feat/RMET-3142/hook-permissions
alexgerardojacinto Feb 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
Comment on lines +289 to +293
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this type of util function is going to be useful across multiple repos/plugins maybe we pull these functions out into a npm typed lib package?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For sure, if we have to use this kind of function again I think we should do that!


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
Loading