diff --git a/android/app/src/main/java/co/backbonelabs/backbone/DeviceManagementService.java b/android/app/src/main/java/co/backbonelabs/backbone/DeviceManagementService.java index f2b49677..054ad42f 100644 --- a/android/app/src/main/java/co/backbonelabs/backbone/DeviceManagementService.java +++ b/android/app/src/main/java/co/backbonelabs/backbone/DeviceManagementService.java @@ -15,7 +15,6 @@ import java.util.HashMap; import java.util.Map; import java.util.Set; -import java.util.ArrayList; import co.backbonelabs.backbone.util.Constants; import co.backbonelabs.backbone.util.EventEmitter; diff --git a/android/app/src/main/java/co/backbonelabs/backbone/MainActivity.java b/android/app/src/main/java/co/backbonelabs/backbone/MainActivity.java index 16e49e74..ddde5f2e 100644 --- a/android/app/src/main/java/co/backbonelabs/backbone/MainActivity.java +++ b/android/app/src/main/java/co/backbonelabs/backbone/MainActivity.java @@ -11,7 +11,6 @@ public class MainActivity extends ReactActivity { public static Activity currentActivity; - private NotificationService notificationService; private Handler idleTimerHandler = new Handler(); private Runnable idleTimerRunnable = null; @@ -75,6 +74,12 @@ public void onDestroy() { Timber.d("onDestroy"); super.onDestroy(); + if (idleTimerRunnable != null) { + Timber.d("Cancel idle timer"); + idleTimerHandler.removeCallbacks(idleTimerRunnable); + idleTimerRunnable = null; + } + BluetoothService bluetoothService = BluetoothService.getInstance(); // Disconnect from device diff --git a/android/app/src/main/java/co/backbonelabs/backbone/SessionControlService.java b/android/app/src/main/java/co/backbonelabs/backbone/SessionControlService.java index cfd2defa..cee45610 100644 --- a/android/app/src/main/java/co/backbonelabs/backbone/SessionControlService.java +++ b/android/app/src/main/java/co/backbonelabs/backbone/SessionControlService.java @@ -101,17 +101,6 @@ public void onActivityDestroyed(Activity activity) { Intent stopIntent = new Intent(activity, ForegroundService.class); stopIntent.setAction(Constants.ACTIONS.STOP_POSTURE_FOREGROUND_SERVICE); activity.startService(stopIntent); - - // Stop the active session - if (hasActiveSession()) { - forceStoppedSession = true; - - toggleSessionOperation(Constants.SESSION_OPERATIONS.STOP, new Constants.IntCallBack() { - @Override - public void onIntCallBack(int val) { - } - }); - } } }); } @@ -175,8 +164,6 @@ public void start(ReadableMap sessionParam, final Callback callback) { vibrationDuration = sessionParam.getInt("vibrationDuration"); } - sessionDuration *= 60; // Convert to second from minute - toggleSessionOperation(Constants.SESSION_OPERATIONS.START, new Constants.IntCallBack() { @Override public void onIntCallBack(int val) { @@ -318,6 +305,14 @@ public void getSessionState() { bluetoothService.hasCharacteristic(Constants.CHARACTERISTIC_UUIDS.SESSION_STATISTIC_CHARACTERISTIC)) { bluetoothService.readCharacteristic(Constants.CHARACTERISTIC_UUIDS.SESSION_STATISTIC_CHARACTERISTIC); } + else { + WritableMap wm = Arguments.createMap(); + wm.putBoolean("hasActiveSession", false); + wm.putInt("totalDuration", 0); + wm.putInt("slouchTime", 0); + Timber.d("SessionState data %s", wm); + EventEmitter.send(reactContext, "SessionState", wm); + } } private void toggleSessionOperation(int operation, Constants.IntCallBack errCallBack) { @@ -330,14 +325,16 @@ private void toggleSessionOperation(int operation, Constants.IntCallBack errCall boolean status; if (operation == Constants.SESSION_OPERATIONS.START) { + int sessionDurationInSecond = sessionDuration * 60; // Convert to second from minute + byte[] commandBytes = new byte[12]; commandBytes[0] = Constants.SESSION_COMMANDS.START; - commandBytes[1] = Utilities.getByteFromInt(sessionDuration, 3); - commandBytes[2] = Utilities.getByteFromInt(sessionDuration, 2); - commandBytes[3] = Utilities.getByteFromInt(sessionDuration, 1); - commandBytes[4] = Utilities.getByteFromInt(sessionDuration, 0); + commandBytes[1] = Utilities.getByteFromInt(sessionDurationInSecond, 3); + commandBytes[2] = Utilities.getByteFromInt(sessionDurationInSecond, 2); + commandBytes[3] = Utilities.getByteFromInt(sessionDurationInSecond, 1); + commandBytes[4] = Utilities.getByteFromInt(sessionDurationInSecond, 0); commandBytes[5] = Utilities.getByteFromInt(slouchDistanceThreshold, 1); commandBytes[6] = Utilities.getByteFromInt(slouchDistanceThreshold, 0); @@ -450,17 +447,24 @@ public void onReceive(Context context, Intent intent) { Timber.d("CharacteristicRead"); String uuid = intent.getStringExtra(Constants.EXTRA_BYTE_UUID_VALUE); + int flags = 0; + int totalDuration = 0; + int slouchTime = 0; + if (uuid.equals(Constants.CHARACTERISTIC_UUIDS.SESSION_STATISTIC_CHARACTERISTIC.toString())) { byte[] responseArray = intent.getByteArrayExtra(Constants.EXTRA_BYTE_VALUE); - int flags = Utilities.getIntFromByteArray(responseArray, 0); - int totalDuration = Utilities.getIntFromByteArray(responseArray, 4); - int slouchTime = Utilities.getIntFromByteArray(responseArray, 8); - - boolean hasActiveSession = (flags % 2 == 1); + if (responseArray == null || responseArray.length < 12) { + // Invalid response, default to no active session + } + else { + flags = Utilities.getIntFromByteArray(responseArray, 0); + totalDuration = Utilities.getIntFromByteArray(responseArray, 4); + slouchTime = Utilities.getIntFromByteArray(responseArray, 8); + } WritableMap wm = Arguments.createMap(); - wm.putBoolean("hasActiveSession", hasActiveSession); + wm.putBoolean("hasActiveSession", flags % 2 == 1); wm.putInt("totalDuration", totalDuration); wm.putInt("slouchTime", slouchTime); Timber.d("SessionState data %s", wm); diff --git a/app/actions/device.js b/app/actions/device.js index cfa6e6b2..d49ff463 100644 --- a/app/actions/device.js +++ b/app/actions/device.js @@ -7,6 +7,8 @@ import { DEVICE_DISCONNECT, DEVICE_FORGET, DEVICE_GET_INFO, + DEVICE_RESTORE_SAVED_SESSION, + DEVICE_CLEAR_SAVED_SESSION, } from './types'; import store from '../store'; import Fetcher from '../utils/Fetcher'; @@ -232,6 +234,12 @@ const deviceActions = { }), }; }, + restoreSavedSession() { + return { type: DEVICE_RESTORE_SAVED_SESSION }; + }, + clearSavedSession() { + return { type: DEVICE_CLEAR_SAVED_SESSION }; + }, }; export default deviceActions; diff --git a/app/actions/types.js b/app/actions/types.js index 52aae731..e22b5d9f 100644 --- a/app/actions/types.js +++ b/app/actions/types.js @@ -41,6 +41,8 @@ export const DEVICE_FORGET__ERROR = 'DEVICE_FORGET__ERROR'; export const DEVICE_GET_INFO = 'DEVICE_GET_INFO'; export const DEVICE_GET_INFO__START = 'DEVICE_GET_INFO__START'; export const DEVICE_GET_INFO__ERROR = 'DEVICE_GET_INFO__ERROR'; +export const DEVICE_RESTORE_SAVED_SESSION = 'DEVICE_RESTORE_SAVED_SESSION'; +export const DEVICE_CLEAR_SAVED_SESSION = 'DEVICE_CLEAR_SAVED_SESSION'; // posture actions export const SET_SESSION_TIME = 'SET_SESSION_TIME'; diff --git a/app/components/PartialModal.js b/app/components/PartialModal.js index a20a63b5..14583b2d 100644 --- a/app/components/PartialModal.js +++ b/app/components/PartialModal.js @@ -19,20 +19,23 @@ const PartialModal = props => ((props.show && - { - props.dispatch(appActions.hidePartialModal()); - if (isFunction(props.onClose)) { - props.onClose(); - } - }} - > - - + { + !props.hideClose && + { + props.dispatch(appActions.hidePartialModal()); + if (isFunction(props.onClose)) { + props.onClose(); + } + }} + > + + + } {props.children} @@ -44,6 +47,7 @@ PartialModal.propTypes = { children: PropTypes.node, dispatch: PropTypes.func, onClose: PropTypes.func, + hideClose: PropTypes.bool, show: PropTypes.bool, }; diff --git a/app/components/posture/PostureDashboard.js b/app/components/posture/PostureDashboard.js index 1b159303..6b834f26 100755 --- a/app/components/posture/PostureDashboard.js +++ b/app/components/posture/PostureDashboard.js @@ -59,6 +59,7 @@ class PostureDashboard extends Component { device: PropTypes.shape({ isConnected: PropTypes.bool, isConnecting: PropTypes.bool, + hasSavedSession: PropTypes.bool, }), user: PropTypes.shape({ isFetchingSessions: PropTypes.bool, @@ -83,11 +84,14 @@ class PostureDashboard extends Component { componentDidMount() { this.setSessionTime(sessions[0].durationSeconds); + const { hasSavedSession } = this.props.device; + const { _id: userId, seenBaselineSurvey, seenAppRating, seenFeedbackSurvey, lastSession, } = this.props.user.user; - if (!seenBaselineSurvey) { + // Prioritize recovering previous session if exists + if (!hasSavedSession && !seenBaselineSurvey) { // User has not seen the baseline survey modal yet. Display survey modal // and mark as seen in the user profile to prevent it from being shown again. const markSurveySeenAndHideModal = () => { @@ -189,8 +193,12 @@ class PostureDashboard extends Component { } componentWillReceiveProps(nextProps) { - if (this.props.user.isFetchingSessions && !nextProps.user.isFetchingSessions) { - // Finished fetching user sessions. + const { isFetchingSessions } = this.props.user; + + const sessionsFetched = isFetchingSessions && !nextProps.user.isFetchingSessions; + + if (!nextProps.device.hasSavedSession && sessionsFetched) { + // Finished fetching user sessions and display popUp when needed if (!this.props.user.user.seenFeedbackSurvey || !this.props.user.user.seenAppRating) { const createdDate = new Date(this.props.user.user.createdAt); const timeThreshold = 7 * 24 * 60 * 60 * 1000; // 7 days converted to milliseconds diff --git a/app/components/posture/PostureMonitor.js b/app/components/posture/PostureMonitor.js index c3c478a4..2fc59df3 100755 --- a/app/components/posture/PostureMonitor.js +++ b/app/components/posture/PostureMonitor.js @@ -109,6 +109,12 @@ class PostureMonitor extends Component { slouchDistanceThreshold: PropTypes.number, vibrationSpeed: PropTypes.number, vibrationPattern: PropTypes.oneOf([1, 2, 3]), + showSummary: PropTypes.bool, + previousSessionEvent: PropTypes.shape({ + hasActiveSession: PropTypes.bool, + totalDuration: PropTypes.number, + slouchTime: PropTypes.number, + }), }), user: PropTypes.shape({ settings: PropTypes.shape({ @@ -156,6 +162,9 @@ class PostureMonitor extends Component { this.sessionDataListener = null; this.slouchListener = null; this.statsListener = null; + this.deviceStateListener = null; + this.sessionStateListener = null; + this.sessionControlStateListener = null; // Debounce update of user posture threshold setting to limit the number of API requests this.updateUserPostureThreshold = debounce(this.updateUserPostureThreshold, 1000); this.backAndroidListener = null; @@ -255,7 +264,9 @@ class PostureMonitor extends Component { // ANDROID ONLY: Listen to the hardware back button if (!isiOS) { this.backAndroidListener = BackAndroid.addEventListener('hardwareBackPress', () => { - if (this.state.sessionState !== sessionStates.STOPPED + if (this.props.sessionState && this.props.sessionState.showSummary) { + this.props.navigator.resetTo(routes.postureDashboard); + } else if (this.state.sessionState !== sessionStates.STOPPED && !this.state.hasPendingSessionOperation) { // Back button was pressed during an active session. // Check if PostureMonitor is the current scene. @@ -283,19 +294,25 @@ class PostureMonitor extends Component { }); } - const { sessionState } = this.state; - if (sessionState === sessionStates.PAUSED) { - // There is an active session that's paused - // Sync app to a paused state - this.pauseSession(); - } else if (sessionState === sessionStates.RUNNING) { - // There is an active session that's running - // Sync app to a running state - this.resumeSession(); + if (this.props.sessionState && this.props.sessionState.showSummary) { + this.setState({ forceStoppedSession: true }, () => { + this.statsHandler(this.props.sessionState.previousSessionEvent); + }); } else { - // There is no active session - // Automatically start a new session - this.startSession(); + const { sessionState } = this.state; + if (sessionState === sessionStates.PAUSED) { + // There is an active session that's paused + // Sync app to a paused state + this.pauseSession(); + } else if (sessionState === sessionStates.RUNNING) { + // There is an active session that's running + // Sync app to a running state + this.resumeSession(); + } else { + // There is no active session + // Automatically start a new session + this.startSession(); + } } } @@ -326,10 +343,13 @@ class PostureMonitor extends Component { } componentWillUnmount() { - // End the session if it's running - SessionControlService.stop(() => { - // no-op - }); + const { forceStoppedSession, sessionState } = this.state; + // End the session if it's running and not yet stopped + if (!forceStoppedSession && sessionState !== sessionStates.STOPPED) { + SessionControlService.stop(() => { + // no-op + }); + } // Remove listeners this.sessionDataListener.remove(); @@ -399,6 +419,7 @@ class PostureMonitor extends Component { }); } else if (state === sessionStates.STOPPED) { // Remove session state from local storage + this.props.dispatch(deviceActions.clearSavedSession()); SensitiveInfo.deleteItem(storageKeys.SESSION_STATE); } this.setState({ sessionState: state }, () => { diff --git a/app/containers/Application.js b/app/containers/Application.js index a302f043..76f10b48 100644 --- a/app/containers/Application.js +++ b/app/containers/Application.js @@ -29,6 +29,7 @@ import PartialModal from '../components/PartialModal'; import SecondaryText from '../components/SecondaryText'; import Spinner from '../components/Spinner'; import TitleBar from '../components/TitleBar'; +import BodyText from '../components/BodyText'; import Banner from '../components/Banner'; import routes from '../routes'; import styles from '../styles/application'; @@ -71,6 +72,7 @@ class Application extends Component { showPartial: PropTypes.bool, content: PropTypes.node, onClose: PropTypes.func, + hideClose: PropTypes.bool, }), }), user: PropTypes.shape({ @@ -90,6 +92,7 @@ class Application extends Component { initializing: true, initialRoute: null, hasDisplayedLowBatteryWarning: false, + isFetchingSessionState: false, }; this.navigator = null; // Components should use this custom navigator object @@ -103,8 +106,9 @@ class Application extends Component { // ANDROID ONLY: Listen to the hardware back button to either navigate back or exit app if (!isiOS) { this.backAndroidListener = BackAndroid.addEventListener('hardwareBackPress', () => { - if (this.props.app.modal.showFull || this.props.app.modal.showPartial) { - // There is a modal being displayed, hide it + const { showFull, showPartial, hideClose } = this.props.app.modal; + if ((showFull || showPartial) && !hideClose) { + // There is a modal being displayed, hide it when allowed this.props.dispatch(appActions.hideFullModal()); this.props.dispatch(appActions.hidePartialModal()); return true; @@ -169,8 +173,8 @@ class Application extends Component { // Retrieve device info this.props.dispatch(deviceActions.getInfo()); - // Retrieve session state - SessionControlService.getSessionState(); + // Check for previous session + this.checkActiveSession(); break; case deviceStatuses.DISCONNECTED: // Dispatch disconnect action when the device is disconnected @@ -183,59 +187,77 @@ class Application extends Component { // Handle SessionState events this.sessionStateListener = SessionControlServiceEvents.addListener('SessionState', event => { - if (event.hasActiveSession) { - // There is an active session, check if we're on the PostureMonitor scene - if (this.navigator) { - const routeStack = this.navigator.getCurrentRoutes(); - // Stay on the current scene if postureMonitor is still in the stack. - // Only navigate to it when the user's currently not accessing the monitor - let shouldGoToPostureMonitor = true; - for (let i = 0; i < routeStack.length; i++) { - if (routeStack[i].name === routes.postureMonitor.name) { - shouldGoToPostureMonitor = false; - break; + if (this.state.isFetchingSessionState) { + const routeStack = this.navigator.getCurrentRoutes(); + const postureRouteName = routes.postureMonitor.name; + const isPostureMonitorActive = routeStack.some(route => route.name === postureRouteName); + + // Check if we are not yet in the postureMonitor + if (!isPostureMonitorActive) { + if (event.hasActiveSession) { + // There is an active session, + if (this.navigator) { + // Not currently on the PostureMonitor scene + // Navigate to PostureMonitor to resume session using previous session parameters + SensitiveInfo.getItem(storageKeys.SESSION_STATE) + .then(prevSessionState => { + const parameters = {}; + if (prevSessionState) { + Object.assign(parameters, prevSessionState.parameters); + this.props.dispatch( + postureActions.setSessionTime(parameters.sessionDuration * 60) + ); + + // Hacky workaround: + // When the device gets disconnected while on the PostureMonitor scene and + // the user decides to leave the scene, and then reconnects to the device + // outside the PostureMonitor scene, the DeviceConnect scene will call + // popToRoute or replace on the Navigator. However, we get into a race condition + // where this navigate action may cause the PostureMonitor to be inserted into + // the route stack before popToRoute or replace is called. If that happens, the + // PostureMonitor scene will be unmounted. + // This hack will navigate to PostureMonitor after a short delay to minimize + // the chances of such a race condition. + setTimeout(() => { + this.navigate({ + ...routes.postureMonitor, + props: { + sessionState: { + ...parameters, + sessionState: prevSessionState.state, + timeElapsed: event.totalDuration, + slouchTime: event.slouchTime, + }, + }, + }); + }, 250); + } + }); } - } - - if (shouldGoToPostureMonitor) { - // Not currently on the PostureMonitor scene - // Navigate to PostureMonitor to resume session using previous session parameters + } else { + // Redirect to the postureMonitor to show the summary SensitiveInfo.getItem(storageKeys.SESSION_STATE) .then(prevSessionState => { - const parameters = {}; if (prevSessionState) { - Object.assign(parameters, prevSessionState.parameters); - this.props.dispatch( - postureActions.setSessionTime(prevSessionState.parameters.sessionDuration * 60) - ); - } - - // Hacky workaround: - // When the device gets disconnected while on the PostureMonitor scene and - // the user decides to leave the scene, and then reconnects to the device - // outside the PostureMonitor scene, the DeviceConnect scene will call - // popToRoute or replace on the Navigator. However, we get into a race condition - // where this navigate action may cause the PostureMonitor to be inserted into - // the route stack before popToRoute or replace is called. If that happens, the - // PostureMonitor scene will be unmounted. - // This hack will navigate to PostureMonitor after a short delay to minimize - // the chances of such a race condition. - setTimeout(() => { - this.navigate({ - ...routes.postureMonitor, - props: { - sessionState: { - ...parameters, - sessionState: prevSessionState.state, - timeElapsed: event.totalDuration, - slouchTime: event.slouchTime, + setTimeout(() => { + this.navigate({ + ...routes.postureMonitor, + props: { + sessionState: { + showSummary: true, + previousSessionEvent: event, + }, }, - }, - }); - }, 250); + }); + }, 250); + } }); } } + + this.setState({ isFetchingSessionState: false }); + + this.props.dispatch(appActions.hidePartialModal()); } }); @@ -274,6 +296,14 @@ class Application extends Component { } ); + // Check if we need to prepare for restoring previously saved session + SensitiveInfo.getItem(storageKeys.SESSION_STATE) + .then(prevSessionState => { + if (prevSessionState) { + this.props.dispatch(deviceActions.restoreSavedSession()); + } + }); + // Listen to when the app switches between foreground and background AppState.addEventListener('change', this.handleAppStateChange); @@ -391,11 +421,70 @@ class Application extends Component { // Fetch device info when app comes back into foreground this.props.dispatch(deviceActions.getInfo()); - // Retrieve session state - SessionControlService.getSessionState(); + // Skip the session check if the app's not yet ready + if (this.navigator) { + // Check if we really have to check for active session. + // Skip this when the user's still on the posture monitor + const routeStack = this.navigator.getCurrentRoutes(); + const postureRouteName = routes.postureMonitor.name; + const isPostureMonitorActive = routeStack.some(route => route.name === postureRouteName); + + // Only refresh device data when not on postureMonitor + if (!isPostureMonitorActive) { + // Check for previous session + this.checkActiveSession(); + } + } } } + checkActiveSession() { + SensitiveInfo.getItem(storageKeys.SESSION_STATE) + .then(prevSessionState => { + if (prevSessionState) { + // Check if we really have to check for active session. + // Skip this when the user's still on the posture monitor + const routeStack = this.navigator.getCurrentRoutes(); + const postureRouteName = routes.postureMonitor.name; + const shouldShowLoading = !(routeStack.some(route => route.name === postureRouteName)); + const { showPartial, showFull } = this.props.app.modal; + + // Only display if no other pop-ups are visible + if (shouldShowLoading && !showPartial && !showFull) { + this.props.dispatch(appActions.showPartialModal({ + content: ( + + + + Checking for previous session + + + + + + + ), + hideClose: true, + })); + + // Start fetching the previous session state + setTimeout(() => { + this.setState({ isFetchingSessionState: true }); + SessionControlService.getSessionState(); + + // Time-limit of 4 seconds for fetching it + setTimeout(() => { + if (this.state.isFetchingSessionState) { + this.setState({ isFetchingSessionState: false }); + this.props.dispatch(appActions.hidePartialModal()); + } + }, 4000); + }, 1000); + } + } + }); + } + configureScene() { return CustomSceneConfig; } @@ -522,7 +611,11 @@ class Application extends Component { { route.showBanner && } - + {modalProps.content} { route.showTabBar && TabBar } diff --git a/app/reducers/app.js b/app/reducers/app.js index e628c2bb..d43db716 100644 --- a/app/reducers/app.js +++ b/app/reducers/app.js @@ -16,6 +16,7 @@ export default (state = { modal: { showFull: false, showPartial: false, + hideClose: false, content: null, onClose: null, }, @@ -69,11 +70,12 @@ export default (state = { }; } case SHOW_PARTIAL_MODAL: { - const { content, onClose } = action.payload; + const { content, hideClose, onClose } = action.payload; return { ...state, modal: { showPartial: true, + hideClose, content, onClose, }, @@ -84,6 +86,7 @@ export default (state = { ...state, modal: { showPartial: false, + hideClose: false, content: null, onClose: null, }, diff --git a/app/reducers/device.js b/app/reducers/device.js index 1ebbe19d..3165a2d4 100644 --- a/app/reducers/device.js +++ b/app/reducers/device.js @@ -12,6 +12,8 @@ import { DEVICE_GET_INFO, DEVICE_GET_INFO__START, DEVICE_GET_INFO__ERROR, + DEVICE_RESTORE_SAVED_SESSION, + DEVICE_CLEAR_SAVED_SESSION, } from '../actions/types'; export default (state = { @@ -19,6 +21,7 @@ export default (state = { device: {}, isConnecting: false, isConnected: false, + hasSavedSession: false, errorMessage: null, }, action) => { switch (action.type) { @@ -115,6 +118,18 @@ export default (state = { errorMessage: action.payload.message, }; } + case DEVICE_RESTORE_SAVED_SESSION: { + return { + ...state, + hasSavedSession: true, + }; + } + case DEVICE_CLEAR_SAVED_SESSION: { + return { + ...state, + hasSavedSession: false, + }; + } default: return state; } diff --git a/app/styles/application.js b/app/styles/application.js index bab50218..6cae8246 100644 --- a/app/styles/application.js +++ b/app/styles/application.js @@ -51,4 +51,10 @@ export default EStyleSheet.create({ width: applyWidthDifference(30), resizeMode: 'contain', }, + partialModalBodyText: { + textAlign: 'center', + }, + partialSpinnerContainer: { + height: 50, + }, }); diff --git a/ios/backbone/SessionControlService.m b/ios/backbone/SessionControlService.m index f8acbe1d..9e5c51e1 100644 --- a/ios/backbone/SessionControlService.m +++ b/ios/backbone/SessionControlService.m @@ -90,8 +90,6 @@ - (BOOL)hasActiveSession { vibrationDuration = [[sessionParam objectForKey:@"vibrationDuration"] intValue]; } - sessionDuration *= 60; // Convert to second from minute - DLog(@"SessionParam %@", sessionParam); DLog(@"SessionExtra %d %d %d %d %d %d", sessionDuration, sessionDistanceThreshold, sessionTimeThreshold, vibrationPattern, vibrationSpeed, vibrationDuration); @@ -205,6 +203,13 @@ - (BOOL)hasActiveSession { requestedReadSessionStatistics = YES; [BluetoothServiceInstance.currentDevice readValueForCharacteristic:sessionStatistics]; } + else { + [self sendEventWithName:@"SessionState" body:@{ + @"hasActiveSession": [NSNumber numberWithBool:false], + @"totalDuration" : [NSNumber numberWithInteger:0], + @"slouchTime" : [NSNumber numberWithInteger:0] + }]; + } } - (void)toggleSessionOperation:(int)operation withHandler:(ErrorHandler)handler{ @@ -215,14 +220,16 @@ - (void)toggleSessionOperation:(int)operation withHandler:(ErrorHandler)handler{ // 'Start' and 'Resume' operations require additional parameters to be sent if (operation == SESSION_OPERATION_START) { + int sessionDurationInSecond = sessionDuration * 60; // Convert to second from minute + uint8_t bytes[12]; bytes[0] = SESSION_COMMAND_START; - bytes[1] = [Utilities getByteFromInt:sessionDuration index:3]; - bytes[2] = [Utilities getByteFromInt:sessionDuration index:2]; - bytes[3] = [Utilities getByteFromInt:sessionDuration index:1]; - bytes[4] = [Utilities getByteFromInt:sessionDuration index:0]; + bytes[1] = [Utilities getByteFromInt:sessionDurationInSecond index:3]; + bytes[2] = [Utilities getByteFromInt:sessionDurationInSecond index:2]; + bytes[3] = [Utilities getByteFromInt:sessionDurationInSecond index:1]; + bytes[4] = [Utilities getByteFromInt:sessionDurationInSecond index:0]; bytes[5] = [Utilities getByteFromInt:sessionDistanceThreshold index:1]; bytes[6] = [Utilities getByteFromInt:sessionDistanceThreshold index:0]; @@ -372,12 +379,22 @@ - (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(C else if ([characteristic.UUID isEqual:SESSION_STATISTIC_CHARACTERISTIC_UUID]) { uint8_t *dataPointer = (uint8_t*) [characteristic.value bytes]; - int flags = [Utilities convertToIntFromBytes:dataPointer offset:0]; - int totalDuration = [Utilities convertToIntFromBytes:dataPointer offset:4]; - int slouchTime = [Utilities convertToIntFromBytes:dataPointer offset:8]; + int flags = 0; + int totalDuration = 0; + int slouchTime = 0; + bool hasActiveSession = false; - // Check the Least-Significant Bit of the flags to retrieve the current session state - bool hasActiveSession = (flags % 2 == 1); + if (characteristic.value == nil || [characteristic.value length] < 12) { + // Invalid response data, use default values + } + else { + flags = [Utilities convertToIntFromBytes:dataPointer offset:0]; + totalDuration = [Utilities convertToIntFromBytes:dataPointer offset:4]; + slouchTime = [Utilities convertToIntFromBytes:dataPointer offset:8]; + + // Check the Least-Significant Bit of the flags to retrieve the current session state + hasActiveSession = (flags % 2 == 1); + } if (requestedReadSessionStatistics) { // Session statistics were retrieved from a read request, emit SessionState event