nl.welteninstituut.tel.la.importers.fitbit.FitbitTask.java Source code

Java tutorial

Introduction

Here is the source code for nl.welteninstituut.tel.la.importers.fitbit.FitbitTask.java

Source

/*
 * Copyright (C) 2015 Open Universiteit Nederland
 *
 * This library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library.  If not, see <http://www.gnu.org/licenses/>.
 */
package nl.welteninstituut.tel.la.importers.fitbit;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.logging.Logger;

import nl.welteninstituut.tel.la.Configuration;
import nl.welteninstituut.tel.la.importers.ImportTask;
import nl.welteninstituut.tel.la.jdomanager.StatementManager;
import nl.welteninstituut.tel.oauth.jdo.AccountJDO;
import nl.welteninstituut.tel.oauth.jdo.AccountManager;
import nl.welteninstituut.tel.oauth.jdo.OauthConfigurationJDO;
import nl.welteninstituut.tel.oauth.jdo.OauthKeyManager;
import nl.welteninstituut.tel.oauth.jdo.OauthServiceAccount;
import nl.welteninstituut.tel.oauth.jdo.OauthServiceAccountManager;
import nl.welteninstituut.tel.util.StringPool;

import org.apache.commons.codec.binary.Base64;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

/**
 * The FitbitTask fetches the Fitbit heart rate and step count data for the
 * specified user. A single task retrieves and processes max two hours of data
 * starting at the <code>OauthServiceAccount.lastSynced</code> date. The task
 * starts a subsequent task for fetching the next period of data. Data is
 * fetched until the last synchronization time of the fitbit device.
 * 
 * @author Harrie Martens
 *
 */
public class FitbitTask extends ImportTask {

    private static final long serialVersionUID = 2L;
    private static final Logger LOG = Logger.getLogger(FitbitTask.class.getName());

    private String accountId;
    private DateTime start;
    private DateTime deviceLastSynced;

    public FitbitTask() {
    }

    public FitbitTask(final String accountId) {
        this.accountId = accountId;
    }

    private FitbitTask(final String accountId, final DateTime start, final DateTime deviceLastSynced) {
        this(accountId);
        this.start = start;
        this.deviceLastSynced = deviceLastSynced;
    }

    @Override
    public void run() {
        try {
            OauthServiceAccount account = getAccount(AccountJDO.FITBITCLIENT, accountId);
            if (account != null) {

                if (start == null) {

                    if (account.getLastSynced() == null) {
                        String startDate = Configuration.get(Configuration.STARTDATE);
                        if (startDate != null) {
                            start = new DateTime(startDate + "T00:00");
                        } else {
                            LOG.severe(Configuration.STARTDATE + " is missing from configuration");
                        }
                    } else {
                        start = new DateTime(account.getLastSynced()).withSecondOfMinute(0).withMillisOfSecond(0)
                                .plusMinutes(1);
                    }
                }

                if (deviceLastSynced == null) {
                    deviceLastSynced = getDeviceLastSynced(account);
                }

                if (start != null && deviceLastSynced != null && start.isBefore(deviceLastSynced)) {

                    DateTime end = start.plusHours(4);
                    if (end.isAfter(deviceLastSynced)) {
                        end = new DateTime(deviceLastSynced).withSecondOfMinute(0).withMillisOfSecond(0)
                                .plusMinutes(1);
                    }

                    // check if the period spans to next day
                    if (end.getDayOfMonth() != start.getDayOfMonth()) {
                        // if so reset to start of day because heart rate can
                        // only
                        // be retrieved for a single calendar day
                        end = end.withTimeAtStartOfDay();
                    }

                    System.out.println("processing from " + start + " to " + end);

                    JSONObject stepCountData = new JSONObject(readURL(getStepcountURL(start, end), account));
                    JSONObject heartRateData = new JSONObject(readURL(getHeartrateURL(start, end), account));

                    if (!stepCountData.has("errors") && !heartRateData.has("errors")) {
                        AccountJDO pa = AccountManager.getAccount(account.getPrimaryAccount());
                        String mbox = pa != null ? pa.getEmail() : null;

                        String xapiTemplate = "{\"timestamp\":\"%s\","
                                + "\"actor\": {\"objectType\": \"Agent\",\"mbox\":\"mailto:" + mbox + "\"},"
                                + "\"verb\":{\"id\":\"https://brindlewaye.com/xAPITerms/verbs/walked\","
                                + "\"display\":{\"en-US\":\"indicates the user walked number of steps\"}},"
                                + "\"object\":{\"objectType\":\"Activity\",\"id\":\"StepCount\",\"definition\":{\"name\":{\"en-US\":\"step count\"},"
                                + "\"description\":{\"en-US\":\"step count\"},\"type\":\"http://activitystrea.ms/schema/1.0/event\"}},"
                                + "\"result\":{\"response\":\"%d\"}}";

                        processData(stepCountData, "activities-steps", xapiTemplate);

                        xapiTemplate = "{\"timestamp\":\"%s\","
                                + "\"actor\": {\"objectType\": \"Agent\",\"mbox\":\"mailto:" + mbox + "\"},"
                                + "\"verb\":{\"id\":\"http://adlnet.gov/expapi/verbs/experienced\","
                                + "\"display\":{\"en-US\":\"indicates the user experienced something\"}},"
                                + "\"object\":{\"objectType\":\"Activity\",\"id\":\"HeartRate\",\"definition\":{\"name\":{\"en-US\":\"heart rate\"},"
                                + "\"description\":{\"en-US\":\"heart rate\"},\"type\":\"http://activitystrea.ms/schema/1.0/event\"}},"
                                + "\"result\":{\"response\":\"%d\"}}";

                        processData(heartRateData, "activities-heart", xapiTemplate);
                    }

                    // store time of last block data that was
                    // synchronized
                    account.setLastSynced(end.toDate());
                    OauthServiceAccountManager.updateOauthServiceAccount(account);

                    // Daisy chain task for next period
                    if (end.isBefore(deviceLastSynced)) {
                        ImportTask.scheduleTask(new FitbitTask(accountId, end, deviceLastSynced));
                    }
                }

            } else {
                LOG.severe("no fitbit service account found for " + accountId);
            }
        } catch (JSONException | IOException e) {
            LOG.severe("aborting fitbit import for " + accountId + " reason: " + e.getMessage());
        }

    }

    private void processData(final JSONObject json, final String name, final String xapiTemplate)
            throws JSONException {

        if (json.has(name + "-intraday")) {
            JSONArray dataset = json.getJSONObject(name + "-intraday").getJSONArray("dataset");

            if (dataset.length() > 0) {

                String fitbitDate = getDateFromFitbit(json, name);

                for (int i = 0; i < dataset.length(); i++) {
                    JSONObject datapoint = dataset.getJSONObject(i);

                    DateTime logDate = new DateTime(fitbitDate + "T" + datapoint.getString("time"));

                    if (isTimeAllowed(logDate)) {
                        StatementManager.addStatement(
                                String.format(xapiTemplate, logDate.toString(), datapoint.getInt("value")),
                                "fitbit");
                    }
                }
            }
        }
    }

    private DateTime getDeviceLastSynced(final OauthServiceAccount account)
            throws JSONException, MalformedURLException, IOException {
        DateTime result = null;

        JSONArray devices;
        devices = new JSONArray(readURL(new URL("https://api.fitbit.com/1/user/-/devices.json"), account));
        JSONObject device = null;
        if (devices.length() == 1) {
            device = devices.getJSONObject(0);
        } else {
            for (int i = 0; i < devices.length(); i++) {
                if ("Charge HR".equals(devices.getJSONObject(i).getString("deviceVersion"))) {
                    device = devices.getJSONObject(i);
                    break;
                }
            }
        }

        if (device == null) {
            LOG.severe("No Fitbit device found for user");
        } else {
            result = new DateTime(device.getString("lastSyncTime"));
        }

        System.out.println("device last synced @ " + result);
        return result;
    }

    private String getDateFromFitbit(final JSONObject data, final String name) throws JSONException {
        JSONArray ah = data.getJSONArray(name);
        String result = ah.getJSONObject(0).getString("dateTime");

        if ("today".equals(result)) {
            result = new LocalDate().toString();
        }

        return result;
    }

    private URL getHeartrateURL(DateTime start, DateTime end) throws MalformedURLException {
        StringBuilder sb = new StringBuilder("https://api.fitbit.com/1/user/-/activities/heart/date/");
        sb.append(DateTimeFormat.forPattern("yyyy-MM-dd").print(start));
        sb.append("/1d/1sec/time/");
        DateTimeFormatter fitbitTimePattern = DateTimeFormat.forPattern("HH:mm");
        sb.append(fitbitTimePattern.print(start));
        sb.append(StringPool.SLASH);
        String endTime = fitbitTimePattern.print(end);
        sb.append(endTime.equals("00:00") ? "23:59" : endTime);
        sb.append(".json");

        return new URL(sb.toString());
    }

    private URL getStepcountURL(DateTime start, DateTime end) throws MalformedURLException {
        StringBuilder sb = new StringBuilder("https://api.fitbit.com/1/user/-/activities/steps/date/");
        sb.append(DateTimeFormat.forPattern("yyyy-MM-dd").print(start));
        sb.append("/1d/1min/time/");
        DateTimeFormatter fitbitTimePattern = DateTimeFormat.forPattern("HH:mm");
        sb.append(fitbitTimePattern.print(start));
        sb.append(StringPool.SLASH);
        String endTime = fitbitTimePattern.print(end);
        sb.append(endTime.equals("00:00") ? "23:59" : endTime);
        sb.append(".json");

        return new URL(sb.toString());
    }

    private String readURL(final URL url, final OauthServiceAccount account) throws IOException, JSONException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        // InputStream is = url.openStream();

        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestProperty("Authorization", "Bearer " + account.getAccessToken());

        InputStream is = connection.getInputStream();
        int r;
        while ((r = is.read()) != -1) {
            baos.write(r);
        }

        int responseCode = connection.getResponseCode();
        if (responseCode == 401) {
            // refresh access token using refresh token
            exchangeCodeForAccessToken(account);

            baos = new ByteArrayOutputStream();
            // InputStream is = url.openStream();

            connection = (HttpURLConnection) url.openConnection();
            connection.setRequestProperty("Authorization", "Bearer " + account.getAccessToken());

            is = connection.getInputStream();
            while ((r = is.read()) != -1) {
                baos.write(r);
            }

        }

        return new String(baos.toByteArray());
    }

    private void exchangeCodeForAccessToken(final OauthServiceAccount account) throws JSONException {
        OauthConfigurationJDO jdo = OauthKeyManager.getConfigurationObject(account.getServiceId());
        String client_id = jdo.getClient_id();
        String client_secret = jdo.getClient_secret();

        String result = postUrl("https://api.fitbit.com/oauth2/token",
                "grant_type=refresh_token&refresh_token=" + account.getRefreshToken(),
                client_id + ":" + client_secret);

        JSONObject resultJson = new JSONObject(result);
        if (!resultJson.has("errors")) {
            String accessToken = resultJson.getString("access_token");
            String refreshToken = resultJson.getString("refresh_token");

            account.setAccessToken(accessToken);
            account.setRefreshToken(refreshToken);
            OauthServiceAccountManager.updateOauthServiceAccount(account);

        } else {
            JSONArray errors = resultJson.getJSONArray("errors");
            throw new JSONException(errors.getJSONObject(0).getString("message"));
        }
    }

    public String postUrl(String url, String data, String authorization) {
        StringBuilder result = new StringBuilder();

        try {
            URLConnection conn = new URL(url).openConnection();
            // conn.setConnectTimeout(30);
            conn.setDoOutput(true);

            if (authorization != null)
                conn.setRequestProperty("Authorization",
                        "Basic " + new String(new Base64().encode(authorization.getBytes())));
            OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream());
            wr.write(data);
            wr.flush();

            BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            String line;
            while ((line = rd.readLine()) != null) {
                result.append(line);
            }
            wr.close();
            rd.close();

        } catch (Exception e) {
        }

        return result.toString();
    }

}