/* 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 static com.concentricsky.android.khanacademy.Constants.ACTION_BADGE_EARNED; import static com.concentricsky.android.khanacademy.Constants.EXTRA_BADGE; import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import oauth.signpost.OAuthConsumer; import oauth.signpost.commonshttp.CommonsHttpOAuthConsumer; import android.content.Context; import android.content.Intent; import android.support.v4.content.LocalBroadcastManager; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import com.concentricsky.android.khan.R; import com.concentricsky.android.khanacademy.Constants; import com.concentricsky.android.khanacademy.data.KADataService; import com.concentricsky.android.khanacademy.data.db.Badge; import com.concentricsky.android.khanacademy.data.db.BadgeCategory; import com.concentricsky.android.khanacademy.data.db.User; import com.concentricsky.android.khanacademy.data.db.UserVideo; import com.concentricsky.android.khanacademy.util.Log; import com.j256.ormlite.dao.Dao; public class KAAPIAdapter { public static final String LOG_TAG = KAAPIAdapter.class.getSimpleName(); private final String consumerKey; private final String consumerSecret; private KADataService dataService; private User currentUser; public KAAPIAdapter(KADataService dataService, String consumerKey, String consumerSecret) { this.dataService = dataService; this.consumerKey = consumerKey; this.consumerSecret = consumerSecret; } public interface EntityCallback<T extends Object> { public void call(T entity); } public interface UserLoginHandler { public void onUserLogin(User user); } /* ********************** User Update Listener ************************/ private List<UserUpdateListener> listeners = new ArrayList<UserUpdateListener>(); public interface UserUpdateListener { public void onUserUpdate(User user); } public void registerUserUpdateListener(UserUpdateListener l) { listeners.add(l); } public void unregisterUserUpdateListener(UserUpdateListener l) { listeners.remove(l); } private void doUserUpdate(User user) { for (UserUpdateListener l: listeners) { l.onUserUpdate(user); } } // Called by VideoProgressPostTask /*package*/ void doBadgeEarned(Badge badge) { Log.d(LOG_TAG, "doBadgeEarned"); Intent intent = new Intent(ACTION_BADGE_EARNED); intent.putExtra(EXTRA_BADGE, badge); LocalBroadcastManager.getInstance(dataService).sendBroadcast(intent); } /** * Call me from the UI thread, please. * * Meant for use by broadcast receivers receiving ACTION_BADGE_EARNED. * */ public void toastBadge(Badge badge) { BadgeCategory category = badge.getCategory(); // Dao<BadgeCategory, Integer> dao = dataService.getHelper().getDao(BadgeCategory.class); // dao.refresh(category); Toast toast = new Toast(dataService); View content = LayoutInflater.from(dataService).inflate(R.layout.badge, null, false); ImageView iconView = (ImageView) content.findViewById(R.id.badge_image); TextView pointsView = (TextView) content.findViewById(R.id.badge_points); TextView titleView = (TextView) content.findViewById(R.id.badge_title); TextView descView = (TextView) content.findViewById(R.id.badge_description); iconView.setImageResource(category.getIconResourceId()); int points = badge.getPoints(); if (points > 0) { pointsView.setText(points + ""); } else { pointsView.setVisibility(View.GONE); } titleView.setText(badge.getDescription()); descView.setText(badge.getSafe_extended_description()); toast.setView(content); toast.setDuration(Toast.LENGTH_LONG); toast.setGravity(Gravity.TOP, 0, 200); toast.show(); } public void testBadgeEarned() { Badge testBadge = new Badge(); BadgeCategory testCategory = new BadgeCategory(1); testBadge.setPoints(500); testBadge.setDescription("Going Transonic"); testBadge.setCategory(testCategory); testBadge.setSafe_extended_description("Quickly & correctly answer 10 exercise problems in a row (time limit depends on exercise difficulty)"); doBadgeEarned(testBadge); } private String getCurrentUserId() { Log.d(LOG_TAG, "getCurrentUserId"); String result = dataService.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE) .getString(Constants.SETTING_USERID, null); Log.d(LOG_TAG, " --> " + result == null ? "null" : result); return result; } /** * Get the current user. * * This will return the last user authenticated as long as they have not logged out. Specifically, when we launch * the application, we do not explicitly check their credentials. Point totals, video progress, etc., will show * until we make an explicit check (profile page or a video progress update). * * Once we actually have to hit the api, if authentication fails, it will cause the current user to be set * null and a user update will go out. At that point the entire app will behave as if the user is logged out. * * @return the user or null. */ public User getCurrentUser() { if (currentUser == null) { // We may have just launched, and this isn't yet cached. Check for a non-null id in preferences. String userid = getCurrentUserId(); if (userid == null) { // no user saved return null; } try { currentUser = dataService.getHelper().getUserDao().queryForId(userid); } catch (SQLException e) { // That's fine; pretend no user was logged in. e.printStackTrace(); } } return currentUser; } public OAuthConsumer getConsumer(User user) { OAuthConsumer consumer = new CommonsHttpOAuthConsumer(consumerKey, consumerSecret); if (user != null) { String token = user.getToken(); String secret = user.getSecret(); if (token != null && secret != null) { consumer.setTokenWithSecret(token, secret); } } return consumer; } /** * Set the current user id in shared preferences. Usually, you'll want to call loginCurrentUser after this. * * @param userid The user id to set. */ private void setCurrentUserId(String userid) { Log.d(LOG_TAG, "setCurrentUserId: " + userid); dataService.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE) .edit() .putString(Constants.SETTING_USERID, userid) .apply(); } private void setCurrentUser(User user) { // if (user == null) { // String url = "http://www.khanacademy.org/"; // // TODO: Clear WebView cookies, so the "My Account" page is logged out as well. // // The idea is, use getCookie(url) and parse to get each cookie name, then setCookie with each "name=;" // // and possibly an expiration in the past. // CookieManager m = CookieManager.getInstance(); // // // segfault (really?) on next line, unless CookieSyncManager.createInstance has been called. // String existing = m.getCookie(url); // // sample return value: // // fkey=1.0_FtAEhPDlC87EYQ==_1355510081; KAID="aHR0cDovL2dvb2dsZWlkLmtoYW5hY2FkZW15Lm9yZy8xMTgyMzkxMjIxOTM5MDEwNTQwNjUKMjAxMjM0OTE4MzQ1Ni45Mjk5MjAKODYyNGRmNzA5MGY1OGE2NzdiYjZlZTgxYTU4YjI4NmJmNTAwZTc2ZmNlNWE5MTkwYzNmYWZlNmY2MjMyMDVmMQ=="; gae_b_id=; GOOGAPPUID=23; return_visits_http%3A%2F%2Fgoogleid.khanacademy.org%2F118239122193901054065=1355510097.12996; __utma=3422703.2067802560.1355510099.1355510099.1355510099.1; __utmb=3422703.3.10.1355510099; __utmc=3422703; __utmz=3422703.1355510099.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); __utmv=3422703.|1=User%20Type=Logged%20In=1^2=User%20Points=17300=1^3=User%20Videos=18=1^4=User%20Exercises=0=1 // Log.d(LOG_TAG, existing); // // m.setCookie(url, ""); // // // Naive solution, but this also clears facebook/google cookies, etc. // m.removeAllCookie(); // } currentUser = user; setCurrentUserId(user == null ? null : user.getNickname()); } public void login(final String token, final String secret, final UserLoginHandler handler) { OAuthConsumer consumer = new CommonsHttpOAuthConsumer(consumerKey, consumerSecret); consumer.setTokenWithSecret(token, secret); fetchUser(consumer, new EntityCallback<User>() { @Override public void call(User returnedUser) { if (returnedUser != null) { returnedUser.setToken(token); returnedUser.setSecret(secret); try { Dao<User, String> userDao = dataService.getHelper().getUserDao(); if (userDao.getConnectionSource().isOpen()) { userDao.createOrUpdate(returnedUser); } } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } handler.onUserLogin(returnedUser); currentUser = returnedUser; setCurrentUser(returnedUser); doUserUpdate(returnedUser); } }); } /** * Log the current user out. * * @return true if a user was logged in, false otherwise. */ public boolean logout() { boolean result = this.isAuthenticated(); setCurrentUser(null); doUserUpdate(null); Toast.makeText(dataService, dataService.getString(R.string.msg_logged_out), Toast.LENGTH_SHORT).show(); return result; } /** * Do we have an authenticated user registered? * * Does not perform a request to validate the authentication; just returns * true if we have what looks to be a valid consumer. * * @return true if a user is logged in, false otherwise. */ public boolean isAuthenticated() { return this.currentUser != null; } // /api/v1/user /** * Fetch data for the currently logged-in user. Requires prior authentication. * * @param callback */ public void fetchUser(final OAuthConsumer consumer, final EntityCallback<User> callback) { String url = "http://www.khanacademy.org/api/v1/user"; new KAEntityFetcherTask<User>(url, consumer) { @Override protected void onPostExecute(User result) { callback.call(result); } }.execute(); } public void requestUserVideoUpdate(final User user) { fetchUserVideos(getConsumer(user), new EntityCallback<List<UserVideo>>() { @Override public void call(List<UserVideo> userVideos) { int numChanged = 0; if (userVideos == null) { // TODO } Dao<UserVideo, Integer> userVideoDao = null; try { userVideoDao = dataService.getHelper().getUserVideoDao(); } catch (SQLException e) { // Without a Dao we won't be able to do any updates, but probably shouldn't crash just yet. e.printStackTrace(); return; } Map<String, Object> values = new HashMap<String, Object>(); for (UserVideo v : userVideos) { // TODO : is it important to remove old userVideos that don't appear in this response? values.put("user_id", v.getUser().getNickname()); values.put("video_id", v.getVideo_id()); try { // createOrUpdate would be slick here, but since ORMLite does not support // multi-valued primary keys we can't key on User + Video List<UserVideo> existing = userVideoDao.queryForFieldValues(values); if (existing.size() > 0) { UserVideo it = existing.get(0); // the newly downloaded video v is most current. v.setId(it.getId()); userVideoDao.update(v); } else { userVideoDao.create(v); } numChanged++; } catch (SQLException e) { // Can't update this video, but no need to crash. e.printStackTrace(); } catch (IllegalStateException e) { // Can occur when a video update is coming back while we shut down the app, if the db is already closed. // TODO : the real fix is to use startService for this task. e.printStackTrace(); } } if (numChanged > 0) { // This will trigger the video fragments to refresh the UserVideo related to this User and their current video. doUserUpdate(user); } } }); } // /api/v1/user/videos public void fetchUserVideos(final OAuthConsumer consumer, final EntityCallback<List<UserVideo>> callback) { String url = "http://www.khanacademy.org/api/v1/user/videos"; new KAEntityCollectionFetcherTask<UserVideo>(UserVideo.class, url, consumer) { @Override protected void onPostExecute(List<UserVideo> result) { if (result == null) { exception.printStackTrace(); // But don't crash. } callback.call(result); } }.execute(); } public void postVideoProgress(final UserVideo userVideo, final Runnable successHandler, final Runnable errorHandler) { Log.d(LOG_TAG, "postVideoProgress"); new VideoProgressPostTask(dataService) { @Override protected void onPostExecute(User returnedUser) { // if returnedUser is null, the post failed. if (returnedUser != null) { doUserUpdate(returnedUser); if (successHandler != null) { successHandler.run(); } } else { if (errorHandler != null) { errorHandler.run(); } } } }.execute(userVideo); } }