package org.fluxtream.connectors.sleep_as_android;
import com.google.api.client.auth.oauth2.TokenResponseException;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson.JacksonFactory;
import com.google.gdata.util.common.base.Pair;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.BasicResponseHandler;
import org.fluxtream.core.connectors.annotations.Updater;
import org.fluxtream.core.connectors.updaters.AbstractUpdater;
import org.fluxtream.core.connectors.updaters.UpdateFailedException;
import org.fluxtream.core.connectors.updaters.UpdateInfo;
import org.fluxtream.core.domain.AbstractFacet;
import org.fluxtream.core.domain.ApiKey;
import org.fluxtream.core.domain.Notification;
import org.fluxtream.core.services.ApiDataService;
import org.fluxtream.core.services.impl.BodyTrackHelper;
import org.fluxtream.core.utils.Utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
@Component
@Updater(prettyName = "Sleep_As_Android", value = 351, objectTypes={SleepFacet.class}, bodytrackResponder=SleepAsAndroidBodytrackResponder.class,
defaultChannels = {"Sleep_As_Android.sleep","Sleep_As_Android.actiGraph","Sleep_As_Android.cycles"})
public class SleepAsAndroidUpdater extends AbstractUpdater {
@Autowired
BodyTrackHelper bodyTrackHelper;
@Override
protected void updateConnectorDataHistory(UpdateInfo updateInfo) throws Exception {
updateConnectorData(updateInfo);
}
@Override
protected void updateConnectorData(UpdateInfo updateInfo) throws Exception {
Long latestData = getLatestFacetTime(updateInfo);
fetchSleeps(updateInfo, latestData, null);
}
@Override
public void setDefaultChannelStyles(ApiKey apiKey) {
BodyTrackHelper.ChannelStyle channelStyle = new BodyTrackHelper.ChannelStyle();
channelStyle.timespanStyles = new BodyTrackHelper.MainTimespanStyle();
channelStyle.timespanStyles.defaultStyle = new BodyTrackHelper.TimespanStyle();
channelStyle.timespanStyles.defaultStyle.fillColor = "#33b5e5";
channelStyle.timespanStyles.defaultStyle.borderColor = "#33b5e5";
channelStyle.timespanStyles.defaultStyle.borderWidth = 0;
channelStyle.timespanStyles.defaultStyle.top = 1.00;
channelStyle.timespanStyles.defaultStyle.bottom = 0.67;
channelStyle.timespanStyles.values = new HashMap();
BodyTrackHelper.TimespanStyle stylePart = new BodyTrackHelper.TimespanStyle();
stylePart.top = 0.67;
stylePart.bottom = 1.00;
stylePart.fillColor = "#33b5e5";
stylePart.borderColor = "#33b5e5";
channelStyle.timespanStyles.values.put("light",stylePart);
stylePart = new BodyTrackHelper.TimespanStyle();
stylePart.top = 0.33;
stylePart.bottom = 0.67;
stylePart.fillColor = "#0099cc";
stylePart.borderColor = "#0099cc";
channelStyle.timespanStyles.values.put("deep",stylePart);
stylePart = new BodyTrackHelper.TimespanStyle();
stylePart.top = 0.00;
stylePart.bottom = 0.33;
stylePart.fillColor = "#ff8800";
stylePart.borderColor = "#ff8800";
channelStyle.timespanStyles.values.put("rem",stylePart);
bodyTrackHelper.setBuiltinDefaultStyle(apiKey.getGuestId(), "sleep_as_android", "sleep", channelStyle);
}
private void fetchSleeps(UpdateInfo updateInfo, Long latestData, String cursor) throws UpdateFailedException, IOException {
long then = System.currentTimeMillis();
GoogleCredential credentials = getCredentials(updateInfo.apiKey);
String url = "https://sleep-cloud.appspot.com/fetchRecords?actigraph=true&labels=true&tags=true&comments=true";
if (latestData != null)
url += "×tamp=" + latestData;
if (cursor != null)
url += "&cursor=" + cursor;
HttpGet get = new HttpGet(url);
get.setHeader("Authorization","Bearer " + credentials.getAccessToken());
get.getMethod();
HttpResponse response = env.getHttpClient().execute(get);
String content;
int statusCode = response.getStatusLine().getStatusCode();
String statusMessage = response.getStatusLine().getReasonPhrase();
if (statusCode != HttpStatus.SC_OK) {
throw new UpdateFailedException("Got status code " + statusCode);
}
ResponseHandler<String> responseHandler = new BasicResponseHandler();
content = responseHandler.handleResponse(response);
try{
Long newLatestTime = createOrUpdateFacets(updateInfo,content);
if (newLatestTime != null && (latestData == null || newLatestTime > latestData)){
updateLatestFacetTime(updateInfo, newLatestTime);
}
countSuccessfulApiCall(updateInfo.apiKey, updateInfo.objectTypes, then, url);
JSONObject topLevelObject = JSONObject.fromObject(content);
String nextCursor = topLevelObject.has("cursor") ? topLevelObject.getString("cursor") : null;
if (nextCursor != null) {
fetchSleeps(updateInfo,latestData,cursor);
}
}
catch (Exception e){
countFailedApiCall(updateInfo.apiKey,updateInfo.objectTypes,then,url, ExceptionUtils.getStackTrace(e),statusCode,statusMessage);
e.printStackTrace();
throw new UpdateFailedException(e.getMessage());
}
}
private Long getLatestFacetTime(UpdateInfo updateInfo){
ApiKey apiKey = updateInfo.apiKey;
String updateKeyName = "SleepAsAndroid.latestData";
String latestDataString = guestService.getApiKeyAttribute(apiKey, updateKeyName);
return latestDataString == null ? null : Long.parseLong(latestDataString);
}
private void updateLatestFacetTime(UpdateInfo updateInfo, long timestamp){
String updateKeyName = "SleepAsAndroid.latestData";
guestService.setApiKeyAttribute(updateInfo.apiKey, updateKeyName, Long.toString(timestamp));
}
private Long createOrUpdateFacets(UpdateInfo updateInfo, String json) throws Exception {
JSONObject topLevelObject = JSONObject.fromObject(json);
JSONArray sleepsArray = topLevelObject.getJSONArray("sleeps");
Long oldestFacetTime = null;
List<AbstractFacet> newFacets = new LinkedList<AbstractFacet>();
for (int i = 0; i < sleepsArray.size(); i++){
SleepFacet newFacet = createOrUpdateFacet(updateInfo, sleepsArray.getJSONObject(i));
newFacets.add(newFacet);
oldestFacetTime = oldestFacetTime == null ? newFacet.end : Math.max(oldestFacetTime, newFacet.end);
}
bodyTrackStorageService.storeApiData(updateInfo.apiKey,newFacets);
return oldestFacetTime;
}
private SleepFacet createOrUpdateFacet(final UpdateInfo updateInfo, final JSONObject sleepObject) throws Exception {
return apiDataService.createOrReadModifyWrite(SleepFacet.class,new ApiDataService.FacetQuery(
"e.apiKeyId = ? AND e.start = ? AND e.end = ?",
updateInfo.apiKey.getId(), sleepObject.getLong("fromTime"), sleepObject.getLong("toTime")),
new ApiDataService.FacetModifier<SleepFacet>() {
@Override
public SleepFacet createOrModify(SleepFacet facet, Long apiKeyId) {
if (facet == null) {
facet = new SleepFacet(updateInfo.apiKey.getId());
facet.api = updateInfo.apiKey.getConnector().value();
facet.start = sleepObject.getLong("fromTime");
facet.end = sleepObject.getLong("toTime");
}
if (sleepObject.has("cycles"))
facet.cycles = sleepObject.getInt("cycles");
else
facet.cycles = 0;
if (sleepObject.has("deepSleep"))
facet.ratioDeepSleep = sleepObject.getDouble("deepSleep");
else
facet.ratioDeepSleep = 0;
facet.rating = sleepObject.getDouble("rating");
facet.noiseLevel = sleepObject.getDouble("noiseLevel");
if (sleepObject.has("snroingSeconds")) {
facet.snoringSeconds = sleepObject.getInt("snoringSeconds");
} else {
facet.snoringSeconds = 0;
}
if (sleepObject.has("comment")) {
facet.sleepComment = sleepObject.getString("comment");
} else {
facet.sleepComment = null;
}
List<String> sleepTags = new LinkedList<String>();
if (sleepObject.has("tags")) {
JSONArray tags = sleepObject.getJSONArray("tags");
for (int i = 0; i < tags.size(); i++) {
sleepTags.add(tags.getString(i));
}
}
facet.setSleepTags(sleepTags);
Object actiGraphObject = sleepObject.get("actigraph");
if (actiGraphObject != null) {
JSONArray actigraphArray = new JSONArray();
if (actiGraphObject instanceof JSONObject)
actigraphArray.add(actiGraphObject);
else if (actiGraphObject instanceof JSONArray)
actigraphArray = (JSONArray)actiGraphObject;
List<Double> actiGraph = new LinkedList<Double>();
for (int i = 0; i < actigraphArray.size(); i++) {
actiGraph.add(actigraphArray.getDouble(i));
}
facet.setActiGraph(actiGraph);
}
if (sleepObject.has("labels")) {
List<Pair<String, Long>> eventLabels = new LinkedList<Pair<String, Long>>();
JSONArray labels = sleepObject.getJSONArray("labels");
for (int i = 0; i < labels.size(); i++) {
JSONObject label = labels.getJSONObject(i);
eventLabels.add(new Pair<String, Long>(label.getString("label"), label.getLong("timestamp")));
}
facet.setEventLabels(eventLabels);
}
return facet;
}
},updateInfo.apiKey.getId());
}
private GoogleCredential getCredentials(ApiKey apiKey) throws UpdateFailedException {
HttpTransport httpTransport = new NetHttpTransport();
JacksonFactory jsonFactory = new JacksonFactory();
// Get all the attributes for this connector's oauth token from the stored attributes
String accessToken = guestService.getApiKeyAttribute(apiKey, "accessToken");
final String refreshToken = guestService.getApiKeyAttribute(apiKey, "refreshToken");
final String clientId = guestService.getApiKeyAttribute(apiKey, "google.client.id");
final String clientSecret = guestService.getApiKeyAttribute(apiKey,"google.client.secret");
final GoogleCredential.Builder builder = new GoogleCredential.Builder();
builder.setTransport(httpTransport);
builder.setJsonFactory(jsonFactory);
builder.setClientSecrets(clientId, clientSecret);
GoogleCredential credential = builder.build();
final Long tokenExpires = Long.valueOf(guestService.getApiKeyAttribute(apiKey, "tokenExpires"));
credential.setExpirationTimeMilliseconds(tokenExpires);
credential.setAccessToken(accessToken);
credential.setRefreshToken(refreshToken);
try {
if (tokenExpires<System.currentTimeMillis()) {
boolean tokenRefreshed = false;
// Don't worry about checking if we are running on a mirrored test instance.
// Refreshing tokens independently on both the main server and a mirrored instance
// seems to work just fine.
// Try to swap the expired access token for a fresh one.
tokenRefreshed = credential.refreshToken();
if(tokenRefreshed) {
Long newExpireTime = credential.getExpirationTimeMilliseconds();
// Update stored expire time
guestService.setApiKeyAttribute(apiKey, "accessToken", credential.getAccessToken());
guestService.setApiKeyAttribute(apiKey, "tokenExpires", newExpireTime.toString());
}
}
}
catch (TokenResponseException e) {
// Notify the user that the tokens need to be manually renewed
notificationsService.addNamedNotification(apiKey.getGuestId(), Notification.Type.WARNING, connector().statusNotificationName(),
"Heads Up. We failed in our attempt to automatically refresh your Google authentication tokens.<br>" +
"Please head to <a href=\"javascript:App.manageConnectors()\">Manage Connectors</a>,<br>" +
"scroll to the Google Calendar connector, and renew your tokens (look for the <i class=\"icon-resize-small icon-large\"></i> icon)");
// Record permanent update failure since this connector is never
// going to succeed
guestService.setApiKeyStatus(apiKey.getId(), ApiKey.Status.STATUS_PERMANENT_FAILURE, Utils.stackTrace(e), ApiKey.PermanentFailReason.NEEDS_REAUTH);
throw new UpdateFailedException("refresh token attempt permanently failed due to a bad token refresh response", e, true, ApiKey.PermanentFailReason.NEEDS_REAUTH);
}
catch (IOException e) {
// Notify the user that the tokens need to be manually renewed
throw new UpdateFailedException("refresh token attempt failed", e, true, ApiKey.PermanentFailReason.NEEDS_REAUTH);
}
return credential;
}
}