diff --git a/package.cordovabuild.json b/package.cordovabuild.json index 77da54bdb..182212b49 100644 --- a/package.cordovabuild.json +++ b/package.cordovabuild.json @@ -96,7 +96,9 @@ "cordova-plugin-androidx-adapter": {}, "phonegap-plugin-barcodescanner": { "ANDROID_SUPPORT_V4_VERSION": "27.+" - } + }, + "cordova-plugin-bluetooth-classic-serial-port": {}, + "cordova-custom-config": {} } }, "dependencies": { @@ -118,7 +120,7 @@ "cordova-plugin-app-version": "0.1.14", "cordova-plugin-customurlscheme": "5.0.2", "cordova-plugin-device": "2.1.0", - "cordova-plugin-em-datacollection": "git+https://github.com/e-mission/e-mission-data-collection.git#v1.8.2", + "cordova-plugin-em-datacollection": "git+https://github.com/e-mission/e-mission-data-collection.git#v1.8.3", "cordova-plugin-em-opcodeauth": "git+https://github.com/e-mission/cordova-jwt-auth.git#v1.7.2", "cordova-plugin-em-server-communication": "git+https://github.com/e-mission/cordova-server-communication.git#v1.2.6", "cordova-plugin-em-serversync": "git+https://github.com/e-mission/cordova-server-sync.git#v1.3.2", @@ -132,6 +134,8 @@ "cordova-plugin-ionic-webview": "5.0.0", "cordova-plugin-local-notification-12": "github:e-mission/cordova-plugin-local-notification-12#v0.1.4-fix-android-action", "cordova-plugin-x-socialsharing": "6.0.4", + "cordova-plugin-bluetooth-classic-serial-port": "git+https://github.com/louisg1337/cordova-plugin-bluetooth-classic-serial-port.git", + "cordova-custom-config": "^5.1.1", "core-js": "^2.5.7", "enketo-core": "^6.1.7", "enketo-transformer": "^4.0.0", diff --git a/setup/autoreload/macos-index.js b/setup/autoreload/macos-index.js index 4c2ad690d..23cee52ab 100644 --- a/setup/autoreload/macos-index.js +++ b/setup/autoreload/macos-index.js @@ -2,6 +2,7 @@ const os = require('os'); const nameMap = new Map([ + [23, ['Sonoma', '14.3.1']], [22, ['Ventura', '13']], [21, ['Monterey', '12']], [20, ['Big Sur', '11']], diff --git a/setup/setup_shared_native.sh b/setup/setup_shared_native.sh index 00c72a375..87e652561 100644 --- a/setup/setup_shared_native.sh +++ b/setup/setup_shared_native.sh @@ -23,7 +23,7 @@ sed -i -e "s|/usr/bin/env node|/usr/bin/env node --unhandled-rejections=strict|" npx cordova prepare -EXPECTED_COUNT=23 +EXPECTED_COUNT=25 INSTALLED_COUNT=`npx cordova plugin list | wc -l` echo "Found $INSTALLED_COUNT plugins, expected $EXPECTED_COUNT" if [ $INSTALLED_COUNT -lt $EXPECTED_COUNT ]; diff --git a/www/i18n/en.json b/www/i18n/en.json index 9a1fadec7..ac4c87469 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -29,6 +29,7 @@ "user-data": "User data", "erase-data": "Erase data", "dev-zone": "Developer zone", + "bluetooth-scan": "Scan for Bluetooth", "refresh": "Refresh", "end-trip-sync": "End trip + sync", "check-consent": "Check consent", @@ -230,6 +231,16 @@ "list-datepicker-close": "Close", "list-datepicker-set": "Set", + "bluetooth": { + "scan-debug-title": "Bluetooth Scanner", + "scan-for-bluetooth": "Scan for Devices", + "is-scanning": "Scanning...", + "device-info": { + "id": "ID", + "name": "Name" + } + }, + "service": { "reading-server": "Reading from server...", "reading-unprocessed-data": "Reading unprocessed data..." @@ -409,7 +420,8 @@ "while-repopulating-entry": "While repopulating timeline entry: ", "while-loading-metrics": "While loading metrics: ", "while-log-messages": "While getting messages from the log ", - "while-max-index": "While getting max index " + "while-max-index": "While getting max index ", + "while-scanning-bluetooth": "While scanning for Bluetooth Devices: " }, "consent-text": { "title": "NREL OPENPATH PRIVACY POLICY/TERMS OF USE", diff --git a/www/js/bluetooth/BluetoothCard.tsx b/www/js/bluetooth/BluetoothCard.tsx new file mode 100644 index 000000000..a8c2a4e08 --- /dev/null +++ b/www/js/bluetooth/BluetoothCard.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { Card, List } from 'react-native-paper'; +import { StyleSheet } from 'react-native'; + +type Props = any; +const BluetoothCard = ({ device }: Props) => { + return ( + + } + /> + + ); +}; + +export const cardStyles = StyleSheet.create({ + card: { + position: 'relative', + alignSelf: 'center', + marginVertical: 10, + }, + cardContent: { + flex: 1, + width: '100%', + }, +}); + +export default BluetoothCard; diff --git a/www/js/bluetooth/BluetoothScanPage.tsx b/www/js/bluetooth/BluetoothScanPage.tsx new file mode 100644 index 000000000..e3c21858e --- /dev/null +++ b/www/js/bluetooth/BluetoothScanPage.tsx @@ -0,0 +1,111 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { StyleSheet, Modal, ScrollView, SafeAreaView, View } from 'react-native'; +import gatherBluetoothData from './bluetoothScanner'; +import { logWarn, displayError, displayErrorMsg } from '../plugin/logger'; +import BluetoothCard from './BluetoothCard'; +import { Appbar, useTheme, Button } from 'react-native-paper'; + +/** + * The implementation of this scanner page follows the design of + * `www/js/survey/enketo/EnketoModal.tsx`! + * + * Future work may include refractoring these files to be implementations of a + * single base "pop-up page" component + */ + +const BluetoothScanPage = ({ ...props }: any) => { + const { t } = useTranslation(); + const [logs, setLogs] = useState([]); + const [isScanning, setIsScanning] = useState(false); + const { colors } = useTheme(); + + // Function to run Bluetooth Classic test and update logs + const runBluetoothTest = async () => { + // Classic not currently supported on iOS + if (window['cordova'].platformId == 'ios') { + displayErrorMsg('Sorry, iOS is not supported!', 'OSError'); + return; + } + + try { + let response = await window['cordova'].plugins.BEMDataCollection.bluetoothScanPermissions(); + if (response != 'OK') { + displayErrorMsg('Please Enable Bluetooth!', 'Insufficient Permissions'); + return; + } + } catch (e) { + displayError(e, 'Insufficient Permissions'); + return; + } + + try { + setIsScanning(true); + const newLogs = await gatherBluetoothData(t); + setLogs(newLogs); + } catch (error) { + logWarn(error); + } finally { + setIsScanning(false); + } + }; + + const BluetoothCardList = ({ devices }) => ( +
+ {devices.map((device) => { + if (device) { + return ; + } + return null; + })} +
+ ); + + const BlueScanContent = () => ( +
+ + { + props.onDismiss?.(); + }} + /> + + + + + + +
+ ); + + return ( + <> + + + + + + + + + ); +}; + +const s = StyleSheet.create({ + btnContainer: { + padding: 16, + justifyContent: 'center', + }, + btn: { + height: 38, + fontSize: 11, + margin: 4, + }, +}); + +export default BluetoothScanPage; diff --git a/www/js/bluetooth/bluetoothScanner.ts b/www/js/bluetooth/bluetoothScanner.ts new file mode 100644 index 000000000..2e77c30b0 --- /dev/null +++ b/www/js/bluetooth/bluetoothScanner.ts @@ -0,0 +1,54 @@ +import { logDebug, displayError } from '../plugin/logger'; +import { BluetoothClassicDevice } from '../types/bluetoothTypes'; + +/** + * gatherBluetoothData scans for viewable Bluetooth Classic Devices + * @param t is the i18next translation function + * @returns an array of strings containing device data, formatted ['ID: id Name: name'] + */ +export default function gatherBluetoothData(t): Promise { + return new Promise((resolve, reject) => { + logDebug('Running bluetooth discovery test!'); + + // Device List "I/O" + function updatePairingStatus(pairingType: boolean, devices: Array) { + devices.forEach((device) => { + device.is_paired = pairingType; + }); + return devices; + } + + // Plugin Calls + const unpairedDevicesPromise = new Promise((res, rej) => { + window['bluetoothClassicSerial'].discoverUnpaired( + (devices: Array) => { + res(updatePairingStatus(false, devices)); + }, + (e: Error) => { + displayError(e, 'Error'); + rej(e); + }, + ); + }); + + const pairedDevicesPromise = new Promise((res, rej) => { + window['bluetoothClassicSerial'].list( + (devices: Array) => { + res(updatePairingStatus(true, devices)); + }, + (e: Error) => { + displayError(e, 'Error'); + rej(e); + }, + ); + }); + + Promise.all([unpairedDevicesPromise, pairedDevicesPromise]) + .then((logs: Array) => { + resolve(logs.flat()); + }) + .catch((e) => { + reject(e); + }); + }); +} diff --git a/www/js/control/BluetoothScanSettingRow.tsx b/www/js/control/BluetoothScanSettingRow.tsx new file mode 100644 index 000000000..b32e4cdbf --- /dev/null +++ b/www/js/control/BluetoothScanSettingRow.tsx @@ -0,0 +1,23 @@ +import React, { useState } from 'react'; +import SettingRow from './SettingRow'; +import BluetoothScanPage from '../bluetooth/BluetoothScanPage'; + +const BluetoothScanSettingRow = ({}) => { + const [bluePageVisible, setBluePageVisible] = useState(false); + + async function openPopover() { + setBluePageVisible(true); + } + + return ( + <> + + setBluePageVisible(false)} /> + + ); +}; + +export default BluetoothScanSettingRow; diff --git a/www/js/control/ProfileSettings.tsx b/www/js/control/ProfileSettings.tsx index c6ca8b848..f851a6651 100644 --- a/www/js/control/ProfileSettings.tsx +++ b/www/js/control/ProfileSettings.tsx @@ -6,6 +6,7 @@ import ExpansionSection from './ExpandMenu'; import SettingRow from './SettingRow'; import ControlDataTable from './ControlDataTable'; import DemographicsSettingRow from './DemographicsSettingRow'; +import BluetoothScanSettingRow from './BluetoothScanSettingRow'; import PopOpCode from './PopOpCode'; import ReminderTime from './ReminderTime'; import useAppConfig from '../useAppConfig'; @@ -433,8 +434,8 @@ const ProfileSettings = () => { textKey="control.email-log" iconName="email" action={() => sendEmail('loggerDB')}> - +