How to implement the Sweat Loss Monitor application

The following flow chart represents the order of invocation of the Health Services main functions and the Health Tracking Service ones, and the interaction between both the libraries.


The following image represents an overview of the main classes used in the sweat loss example application.


The SweatingBucketsService is a foreground service, which controls both HealthServicesManager and TrackingServiceManager. It starts when the user presses the Start button and it stops after user stops the exercise, and gets the final sweat loss value.

When the user destroys the Activity by swiping right during the active exercise, the application will continue to work in the background (in the foreground service).

The user knows about the running application from the notification. When the user opens the application again (either via notification or by tapping the application icon), the new Activity will show the current exercise status.

A similar event occurs when the application goes to the background i.e.: when the screen goes into the sleep mode. In this case, the Activity is not destroyed, only get OnStop event. The user can bring the Activity back by tapping the notification to observe the current exercise status and, after tapping the Stop button, to get the final sweat loss result.

SweatingBucketsService is responsible for collecting the results from the Health Services and the Health Tracking Service and exchanging the data between both the services.

It also reports the following status values to the UI:

  • Status for sweat loss measurement
  • Sweat loss value
  • Exercise distance
  • Exercise duration
  • Average steps per minute
  • Current steps per minute
  • Connection errors
  • Not available capabilities

It uses Android's Messenger mechanism to establish a two-way communication with the UI.


Permissions

In order to measure the sweat loss, we need to get the following permissions:

  • BODY_SENSORS to use the Samsung Health Sensor SDK’s sweat loss tracker.
  • ACTIVITY_RECOGNITION for measuring the steps per minute during a running exercise.
    Additionally, we also need:
  • FOREGROUND_SERVICE to be able to continue measurement in the background.
  • INTERNET to resolve the Samsung Health Sensor SDK’s HealthTrackingService connection error.

The application will display the permissions popup immediately after the start.

Capabilities

Before using the Health Services and the Samsung Health Sensor SDK’s APIs, let us check whether the Galaxy Watch can measure the sweat loss.

We need to check availability of the following capabilities:

From Health Services:

  • ExerciseType.RUNNING
  • DataType.STEPS_PER_MINUTE
  • DataType.DISTANCE

From Samsung Health Sensor SDK:

  • HealthTrackerType.SWEAT_LOSS

All the capabilities from both the services should be checked immediately after the application launch so that the user is informed, as soon as possible, about any problems.

Capabilities check with Health Services

Health Services is used to measure a running exercise. In order to check the capabilities of the running exercise measurement with Health Services, we first need to obtain the ExerciseClient object.

exerciseClient = HealthServices.getClient(context).getExerciseClient();

We need to check the capabilities of the RUNNING exercise type, STEPS_PER_MINUTE and DISTANCE.
The capabilities come asynchronously.

public static void checkCapabilities(ExerciseClient exerciseClient, HealthServicesCapabilitiesEventListener listener) {

    Futures.addCallback(
            exerciseClient.getCapabilitiesAsync(), new FutureCallback<ExerciseCapabilities>() {

                @Override
                public void onSuccess(ExerciseCapabilities result) {
                    Log.i(TAG, "setExerciseCapabilities onSuccess()");
                    final boolean runningCapabilityAvailable = result
                            .getSupportedExerciseTypes().contains(ExerciseType.RUNNING);
                    final boolean stepsPerMinuteCapabilityAvailable = result
                            .getExerciseTypeCapabilities(ExerciseType.RUNNING).getSupportedDataTypes().contains(STEPS_PER_MINUTE);
                    final boolean distanceCapabilityAvailable = result.getExerciseTypeCapabilities(ExerciseType.RUNNING)
                            .getSupportedDataTypes().contains(DataType.DISTANCE);
                    listener.onHealthServicesCapabilitiesAvailabilityResult(runningCapabilityAvailable && stepsPerMinuteCapabilityAvailable, distanceCapabilityAvailable);
                }

                @Override
                public void onFailure(@NonNull Throwable t) {
                    Log.i(TAG, "setExerciseCapabilities: onFailure(), Throwable message: " + t.getMessage());

                }
            }, Executors.newSingleThreadExecutor());
}

The onHealthServicesCapabilitiesAvailabilityResult listener informs the ViewModel about the results of the capabilities check.

Capabilities check with Health Tracking Service

Sweat loss is measured with the Samsung Health Sensor SDK after a running exercise. If the version of the Galaxy Watch’s software or the Samsung Health Sensor SDK’s library is old, measuring sweat loss may not be available.

Therefore, checking the capabilities of Samsung Health Sensor SDK is required before starting the measurement.
First, get the HealthTrackingService object and set a connection listener to connect to the Samsung Health Sensor SDK’s Health Tracking Service.

healthTrackingService = new HealthTrackingService(healthTrackingServiceConnectionListener, MyApp.getAppContext());

The connection listener will be covered in the following sections.
After connecting to the Tracking Service, we can check the capabilities (this time it is a synchronous call):

public static boolean hasCapabilities(Context context) {

    if (HealthTrackingServiceConnection.getInstance(context).isConnected()) {
        final List<HealthTrackerType> availableTrackers = HealthTrackingServiceConnection.getInstance(context)
        .getHealthTrackingService().getTrackingCapability()
        .getSupportHealthTrackerTypes();

        if (availableTrackers.contains(HealthTrackerType.SWEAT_LOSS)) {
            Log.i(TAG, "The system DOES support Sweat Loss tracking");
            return true;
        }
    }

    Log.e(TAG, "The system does NOT support Sweat Loss tracking");
    return false;
}

If any of the mandatory capabilities are not available, the application cannot work correctly. We should display a failure message to the user.

Setting up and starting the exercise

When the user starts the exercise, he taps the Start button. The request goes to the SweatingBucketsService, which in turn requests the HealthServicesManager to start the exercise. We need to configure the exercise to provide us with the steps per minute and distance using ExerciseConfig:

  • Setting the exercise type: ExerciseType.RUNNING
  • Setting interesting data types for a running exercise: DataType.STEPS_PER_MINUTE and DataType.DISTANCE
  • Then, invoke ExerciseClient.startExerciseAsync().

The implementation of the process looks like this:

protected void startExercise() throws RuntimeException {

    final ExerciseConfig.Builder exerciseConfigBuilder =
            ExerciseConfig.builder(ExerciseType.RUNNING)
                    .setDataTypes(new HashSet<>(Arrays.asList(DataType.DISTANCE, STEPS_PER_MINUTE)))
                    .setIsAutoPauseAndResumeEnabled(false)
                    .setIsGpsEnabled(false);

    Log.i(TAG, "Calling startExerciseAsync");

    Futures.addCallback(exerciseClient.startExerciseAsync(exerciseConfigBuilder.build()), new FutureCallback<Void>() {
        @Override
        public void onSuccess(@Nullable Void result) {
            Log.i(TAG, "startExerciseAsync: onSuccess().");
        }

        @Override
        public void onFailure(@NonNull Throwable t) {
            Log.i(TAG, "startExerciseAsync onFailure() starting exercise : " + t.getMessage());
            healthServicesEventListener.onExerciseError(R.string.exercise_starting_error);
        }
    }, Executors.newSingleThreadExecutor());
}

We show the distance on the watch’s screen and the steps per minute data are sent to the HealthTracker object for further processing (more on this in below sections).
Steps per minute is an instantaneous value, which is fine when we pass it to the HealthTracker, as it expects such format. However, we should process it to display the preferred average value to the user.
The distance value received indicates the distance between the current and the previous readings. To display the distance value to the user, we first need to sum all the values from the beginning of the exercise.

Subscribing to updated exercise data

Health Services enables a watch application to receive the running exercise’s data update and to manage the exercise’s lifetime.
We obtained the ExerciseClient in capabilities check. Now, we set update callback to receive the exercise results including steps per minute and distance:

exerciseClient.setUpdateCallback(exerciseUpdateCallback);

In the callback, we receive the updated exercise state information and the latest exercise data.

private final ExerciseUpdateCallback =
new ExerciseUpdateCallback() {

    @Override
    public void onExerciseUpdateReceived(ExerciseUpdate update) {
        Log.i(TAG, "ExerciseUpdate update :" + update);

        exerciseState = update.getExerciseStateInfo().getState();
        Log.i(TAG, "Exercise State: " + exerciseState);

        if (exerciseState == ExerciseState.ACTIVE) {
            if (!wasExerciseActive) {
                healthServicesEventListener.onExerciseBecomeActive();
            }
        } else {
            wasExerciseActive = false;
            if(exerciseState == ExerciseState.ENDED) {

                switch (update.getExerciseStateInfo().getEndReason()) {
                    case ExerciseEndReason.USER_END:
                        healthServicesEventListener.onExerciseUserEnded();

                        break;
                    case ExerciseEndReason.AUTO_END_SUPERSEDED:
                        Log.i(TAG, "Exercise Terminated. It was superseded by other exercise, started by other client.");
                        healthServicesEventListener.onExerciseAutoEnded(R.string.error_exercise_terminated);
                        break;
                    case ExerciseEndReason.AUTO_END_PERMISSION_LOST:
                        Log.i(TAG, "Exercise Permission Lost.");
                        healthServicesEventListener.onExerciseAutoEnded(R.string.error_exercise_permission_lost);
                        break;
                }
            }
        }
        wasExerciseActive = exerciseState == ExerciseState.ACTIVE;

        updateIntervalDataPoints(update.getLatestMetrics().getIntervalDataPoints());
        updateSampleDataPoints(update.getLatestMetrics().getSampleDataPoints());
    }
};

The detailed exercise state is returned with the update callback as update.getExerciseStateInfo().getState();.

We can start tracking when the state becomes ACTIVE and stop tracking when it becomes ENDED.
The ENDED status can have several reasons (available in update.getExerciseStateInfo().getEndReason() ):

  • USER_END: the exercise ended by the user
  • AUTO_END_SUPERSEDED: the exercise object has been superseded by another application
  • AUTO_END_PERMISSION_LOST: the permission has been revoked by the user
    We should display an error message when the reason is not USER_END.

The exercise results updates are typically received every second, but when the application goes to the background after a couple of seconds, the battery saving mechanism is activated, resulting in the data being updated every 150 secs, i.e. 2.5 min. The results' resolution remains the same, but the data come in batches.

In the example, the application data is sent to the SweatingBucketsService via callbacks (onDistanceDataReceived, onStepsPerMinuteDataReceived). The data is sent immediately to the Health Tracking Service.

Connecting to the Health Tracking Service

First, connect to the Health Tracking Service.

private static final ConnectionListener connectionListener = new ConnectionListener() {

    @Override
    public void onConnectionSuccess() {

        Log.i(TAG, "Connection to HealthTrackingService Succeeded.");
        connected = true;
        healthTrackingServiceConnectionListener.onHealthTrackingServiceConnectionResult(true);
    }

    @Override
    public void onConnectionEnded() {
        Log.i(TAG, "Connection to HealthTrackingService Ended.");
        connected = false;
    }

    @Override
    public void onConnectionFailed(HealthTrackerException e) {
        connected = false;
        Log.i(TAG, "Connection to HealthTrackingService failed. Error message: " + e.getMessage());

        if (e.hasResolution()) {
            healthTrackingServiceConnectionListener.onHealthTrackingServiceConnectionErrorWithResolution(e);
        } else {
            healthTrackingServiceConnectionListener.onHealthTrackingServiceConnectionResult(false);
        }
    }
};

private static void initHealthTrackingService() {
    if (healthTrackingService == null) {
        healthTrackingService = new HealthTrackingService(connectionListener, MyApp.getAppContext());
    }
}

We are informing the SweatLossViewModel about the result using callback with onHealthTrackingServiceConnectionResult(boolean result) in the Sweat Loss Monitor application.

The Samsung Health Sensor SDK provides APIs to resolve connection exceptions if the watch’s Health Platform is not installed, or its version is old to run the SDK’s library. In the error cases, we can check the error’s resolution and resolve them by calling the SDK’s HealthTrackerException.hasResolution() and HealthTrackerException.resolve().

To check the error’s resolution, the INTERNET permission is required. Resolving the exception entails being redirected to a view, which downloads the Health Platform application on the watch.

Setting an event listener to get a sweat loss result

Once we connect, it is time to acquire the HealthTracker object to use the sweat loss feature.

try {
    sweatLossTracker = HealthTrackingServiceConnection.getInstance(context).getHealthTrackingService()
    .getHealthTracker(HealthTrackerType.SWEAT_LOSS, new UserProfile().getTrackerUserProfile(), com.samsung.android.service.health.tracking.data.ExerciseType.RUNNING);

} catch (final IllegalArgumentException e) {
    Log.i(TAG, "Tracker not created, an exception: " + e.getMessage());
}

The sweat loss amount varies depending on the user’s height, weight, age, and gender. Set the user profile information with the UserProfile object. We can define the TrackerUserProfile as:

public class UserProfile {

    public TrackerUserProfile getTrackerUserProfile() {

        // the following User Profile values are needed for sweat loss calculations.
        // in the real market-ready application the numbers should be editable in UI.

        // height in cm
        final float height = 182f;
        // weight in kg
        final float weight = 80f;
        // gender 0: woman, 1: man
        final int gender = 1;
        final int age = 45;

        final TrackerUserProfile.Builder builder = new TrackerUserProfile.Builder();

        builder.setHeight(height);
        builder.setWeight(weight);
        builder.setGender(gender);
        builder.setAge(age);

        return builder.build();
    }
}

To get the sweat loss result, set a TrackerEventListener:

private final HealthTracker.TrackerEventListener trackerEventListener = new HealthTracker.TrackerEventListener() {

    @Override
    public void onDataReceived(@NonNull List<DataPoint> list) {

        Log.i(TAG, "onDataReceived invoked.");

        final ValueKey<Float> SweatLossValueKey = ValueKey.SweatLossSet.SWEAT_LOSS;
        final ValueKey<Integer> StatusValueKey = ValueKey.SweatLossSet.STATUS;

        // there is always up to 1 element in the list
        if (!list.isEmpty()) {
            Log.i(TAG, "onDataReceived List size is " + list.size() + ", Timestamp: " + list.get(0).getTimestamp());

            final float sweatLoss = list.get(0).getValue(SweatLossValueKey);
            final int status = list.get(0).getValue(StatusValueKey);
            sweatLossStatus = status;
            sweatLossValue = sweatLoss;
            Log.i(TAG, "Sweat Loss : " + sweatLoss + "  Status : " + status);

            final boolean finalResult = finalResult(status, sweatLoss);

            //unset Listener RIGHT after getting final result
            if (finalResult) unsetTrackerUpdateListener();
            trackingServiceEventListener.onSweatLossDataReceived(status, sweatLoss, finalResult);

        } else {
            Log.i(TAG, "onDataReceived List size is null");
        }
    }

    @Override
    public void onFlushCompleted() {
        Log.i(TAG, " onFlushCompleted called");
    }

    @Override
    public void onError(HealthTracker.TrackerError trackerError) {
        if (trackerError == HealthTracker.TrackerError.PERMISSION_ERROR) {
            trackingServiceEventListener.onHealthTrackerError(R.string.tracker_error_permission_error);
        }
        if (trackerError == HealthTracker.TrackerError.SDK_POLICY_ERROR) {
            trackingServiceEventListener.onHealthTrackerError(R.string.tracker_error_sdk_policy_denied);
        }
    }
};

Start an exercise and setting the exercise’s state to START

When the user taps the START button on the Sweat Loss Monitor application, we start the exercise in Health Services by calling startExerciseAsync(). When the exercise starts, we should set the sweatLossTracker’s exercise state to START.

sweatLossTracker.setExerciseState(ExerciseState.START);

During the exercise, the watch’s related sensors measure the exercise and Health Services updates the exercise’s steps per minute and distance in the background. The updated steps per minute must be sent to the sweat loss HealthTracker with HealthTracker.setExerciseData().

public void feedTrackerWithStepsPerMinuteData(float[] stepsPerMinuteValues, long[] stepsPerMinuteTimeStamps) {

    Log.i(TAG, "feedTrackerWithStepsPerMinuteData()");

    final DataType STEPS_PER_MINUTE = DataType.STEP_PER_MINUTE;
    try {
        Log.i(TAG, "Feed Tracker with StepsPerMinute data: " + Arrays.toString(stepsPerMinuteValues));
        sweatLossTracker.setExerciseData(STEPS_PER_MINUTE, stepsPerMinuteValues, stepsPerMinuteTimeStamps);

    } catch (final IllegalArgumentException e) {
        Log.i(TAG, " Error Data SPM: " + Arrays.toString(stepsPerMinuteValues) + " utcTime : " + stepsPerMinuteTimeStamps[0]);

    } catch (final IllegalStateException e) {

        Log.i(TAG, e.getMessage());
    }
}

Get a sweat loss measurement result after the exercise

When the user taps the STOP button on the Sweat Loss Monitor application to end the exercise, stop the exercise with Health Services as follows:

  • Invoke ExerciseClient.stopExerciseAsync().
  • Wait until the status of the exercise (read in update callback) changes to ENDED
  • Inform the HealthTracker about the stopped exercise

To set the sweat loss HealthTracker’s exercise state to STOP, just invoke:

sweatLossTracker.setExerciseState(ExerciseState.STOP);

It gives the final sweat loss measurement result through TrackerEventListener that we have set in setting an event listener to get a sweat loss result.
The result includes the sweat loss amount and the sweat loss measurement result’s status flag. See the Samsung Health Sensor SDK’s API Reference document for the sweat loss status details.

Unset the sweat loss event listener

After receiving the sweat loss result, unset the sweat loss HealthTracker’s event listener.

sweatLossTracker.unsetEventListener();

After unsetting the update listener, the watch’s sensors used by HealthTracker will stop working.