/*
Viewer for Khan Academy
Copyright (C) 2012 Concentric Sky, Inc.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.concentricsky.android.khanacademy.data.remote;
import java.io.IOException;
import java.net.MalformedURLException;
import java.sql.SQLException;
import java.util.List;
import java.util.Locale;
import oauth.signpost.OAuthConsumer;
import oauth.signpost.exception.OAuthCommunicationException;
import oauth.signpost.exception.OAuthExpectationFailedException;
import oauth.signpost.exception.OAuthMessageSignerException;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.DefaultHttpClient;
import android.os.AsyncTask;
import com.concentricsky.android.khanacademy.data.KADataService;
import com.concentricsky.android.khanacademy.data.db.Badge;
import com.concentricsky.android.khanacademy.data.db.User;
import com.concentricsky.android.khanacademy.data.db.UserVideo;
import com.concentricsky.android.khanacademy.data.db.Video;
import com.concentricsky.android.khanacademy.util.Log;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.j256.ormlite.dao.Dao;
class VideoProgressPostTask extends AsyncTask<UserVideo, Void, User> {
public static final String LOG_TAG = VideoProgressPostTask.class.getSimpleName();
private String url = "http://www.khanacademy.org/api/v1/user/videos/%s/log";
private KADataService dataService;
private OAuthConsumer consumer;
public VideoProgressPostTask(KADataService dataService) {
this.dataService = dataService;
}
@Override
protected User doInBackground(UserVideo... params) {
/*
*
* A Giant waste of time deserves a Giant comment block.
*
* To properly sign post requests, we need to send the payload as a querystring.
*
* Some alternatives that did not work:
* Set the data as params onto the request
* Set the data as params onto the OAuthConsumer
* Set the data onto the request as headers (?!)
* Write the data into the request entity
*
* Tried all of those before and after signing, and with various Accept / Content-Type headers.
*
* This work-around comes from a comment in this bug report related to HttpURLConnection:
* http://code.google.com/p/oauth-signpost/issues/detail?id=15
*
* According to the other comments, this should not be necessary when using the apache http
* library, as we do get a chance to sign the request after specifying content but before
* opening the connection. However, I had no luck.
*
*/
UserVideo userVideo = params[0];
String videoId = null;
User user = null;
Dao<User, String> userDao = null;
Dao<Video, String> videoDao = null;
try {
user = userVideo.getUser();
userDao = dataService.getHelper().getUserDao();
userDao.refresh(user);
consumer = dataService.getAPIAdapter().getConsumer(user);
videoDao = dataService.getHelper().getVideoDao();
Video video = videoDao.queryForFirst(videoDao.queryBuilder().where().eq("readable_id", userVideo.getVideo_id()).prepare());
videoId = video.getYoutube_id();
url = String.format(url, videoId);
} catch (SQLException e) {
// Fail silently when trying to post progress updates.
e.printStackTrace();
return null;
}
final User existingUser = user;
final UserVideo existingUserVideo = userVideo;
VideoProgressUpdate payload = new VideoProgressUpdate(userVideo);
VideoProgressResult result = remoteFetch(payload);
if (result != null) {
ActionResults results = result.getAction_results();
// Update User and UserVideo, save, and fire callbacks.
// User.
User returnedUser = results.getUser_data();
// The returned user object is more current than the existing (total seconds watched at the least).
// Set onto the new user the fields that won't appear in the response. Nickname (and hence id) is already correct.
returnedUser.setToken(existingUser.getToken());
returnedUser.setSecret(existingUser.getSecret());
try {
dataService.getHelper().getUserDao().update(returnedUser);
} catch (SQLException e) {
e.printStackTrace();
}
// Badges.
try {
List<Badge> badges = results.getBadges_earned().getBadges();
// DEBUG
// for (Badge b : badges) {
// try {
// Log.d(LOG_TAG, new ObjectMapper().writeValueAsString(b));
// } catch (JsonProcessingException e) {
// e.printStackTrace();
// }
// }
// It's possible to get a response with multiple earned badges. In fact, this always happens
// when the user earns "Awesome Listener" for watching an hour of a topic; the user also earns
// a "Great Listener" for 30 minutes and a "Nice Listener" for 15 minutes at the same time.
// We don't want a 10-minute badge-toaststravaganza, so just toast the last one listed.
if (badges != null && badges.size() > 0) {
Badge b = badges.get(badges.size() - 1);
dataService.getAPIAdapter().doBadgeEarned(b);
}
} catch (NullPointerException e) {
// No badges were earned.
}
// Now UserVideo.
UserVideo returnedVideo = results.getUser_video();
// Again, the returned object is fresher than our existing one.
returnedVideo.setId(existingUserVideo.getId());
try {
dataService.getHelper().getUserVideoDao().update(returnedVideo);
} catch (SQLException e) {
e.printStackTrace();
}
// Do the user update here, after the UserVideo has been saved.
return returnedUser;
} else {
Log.e(LOG_TAG, "null result in postVideoProgress");
return null;
}
}
private VideoProgressResult remoteFetch(VideoProgressUpdate update) {
VideoProgressResult result = null;
String q = String.format(Locale.US, "last_second_watched=%d&seconds_watched=%d", update.getLast_second_watched(), update.getSeconds_watched());
url = String.format("%s?%s", url, q);
Log.d(KAAPIAdapter.LOG_TAG, "posting video progress: " + url);
// Use this! The response is chunked, so don't try to get the Content-Length and read a buffer of that length.
// However, the response is small, so it isn't a big deal that this handler blocks until it's done.
ResponseHandler<String> h = new BasicResponseHandler();
HttpClient httpClient = new DefaultHttpClient();
HttpPost request = new HttpPost(url);
ObjectMapper mapper = new ObjectMapper();
try {
consumer.sign(request);
String response = httpClient.execute(request, h);
result = mapper.readValue(response, VideoProgressResult.class);
// DEBUG
// User ud = result.action_results.user_data;
// UserVideo uv = result.action_results.user_video;
// if (result.action_results.badges_earned != null) {
// List<Badge> badges = result.action_results.badges_earned.getBadges();
// if (badges != null) {
// Log.d(KAAPIAdapter.LOG_TAG, "Badges: ");
// for (Badge b : badges) {
// Log.d(KAAPIAdapter.LOG_TAG, " " + b.getDescription() + " (" + b.getPoints() + " points)");
// }
// } else {
// Log.d(KAAPIAdapter.LOG_TAG, "badges was null");
// }
// } else {
// Log.d(KAAPIAdapter.LOG_TAG, "badges was null");
// }
//
// Log.d(KAAPIAdapter.LOG_TAG, "url was " + url);
// Log.d(KAAPIAdapter.LOG_TAG, "got data for " + ud.getNickname() + ", video points: " + uv.getPoints());
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (OAuthMessageSignerException e) {
e.printStackTrace();
} catch (OAuthExpectationFailedException e) {
e.printStackTrace();
} catch (OAuthCommunicationException e) {
e.printStackTrace();
} catch (HttpResponseException e) {
e.printStackTrace();
} catch (ClientProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
@JsonIgnoreProperties(ignoreUnknown=true)
public static class VideoProgressUpdate {
int last_second_watched;
int seconds_watched;
public VideoProgressUpdate(UserVideo userVideo) {
last_second_watched = userVideo.getLast_second_watched();
seconds_watched = userVideo.getSeconds_watched();
}
/**
* @return the last_second_watched
*/
public int getLast_second_watched() {
return last_second_watched;
}
/**
* @param last_second_watched the last_second_watched to set
*/
public void setLast_second_watched(int last_second_watched) {
this.last_second_watched = last_second_watched;
}
/**
* @return the seconds_watched
*/
public int getSeconds_watched() {
return seconds_watched;
}
/**
* @param seconds_watched the seconds_watched to set
*/
public void setSeconds_watched(int seconds_watched) {
this.seconds_watched = seconds_watched;
}
}
// This came as 'action_results': {... 'badges_earned': { 'badges': [ /* in here */ ] } ... } .
/**
* When ActionResults contain some badges earned, they are wrapped in this object.
*/
@JsonIgnoreProperties(ignoreUnknown=true)
public static class BadgesEarned {
List<Badge> badges;
public List<Badge> getBadges() { return badges; }
public void setBadges(List<Badge> badges) { this.badges = badges; }
}
@JsonIgnoreProperties(ignoreUnknown=true)
public static class ActionResults {
User user_data;
UserVideo user_video;
BadgesEarned badges_earned;
public User getUser_data() { return user_data; }
public void setUser_data(User user_data) { this.user_data = user_data; }
public UserVideo getUser_video() { return user_video; }
public void setUser_video(UserVideo user_video) { this.user_video = user_video; }
public BadgesEarned getBadges_earned() { return badges_earned; }
public void setBadges_earned(BadgesEarned badges_earned) { this.badges_earned = badges_earned; }
// tutorial_node_progress
// user_info_html
}
@JsonIgnoreProperties(ignoreUnknown=true)
public static class VideoProgressResult {
ActionResults action_results;
public ActionResults getAction_results() {
return action_results;
}
public void setAction_results(ActionResults action_results) {
this.action_results = action_results;
}
// time_watched
}
}