com.concentricsky.android.khanacademy.data.remote.VideoProgressPostTask.java Source code

Java tutorial

Introduction

Here is the source code for com.concentricsky.android.khanacademy.data.remote.VideoProgressPostTask.java

Source

/*
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
    }

}