/* 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.app; import static com.concentricsky.android.khanacademy.Constants.PARAM_TOPIC_ID; import static com.concentricsky.android.khanacademy.Constants.PARAM_VIDEO_ID; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; import java.sql.SQLException; import java.util.Arrays; import java.util.HashMap; import java.util.List; import oauth.signpost.exception.OAuthCommunicationException; import oauth.signpost.exception.OAuthExpectationFailedException; import oauth.signpost.exception.OAuthMessageSignerException; import org.apache.http.Header; import org.apache.http.client.methods.HttpGet; import android.annotation.SuppressLint; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.webkit.WebSettings.ZoomDensity; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.FrameLayout; 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.Topic; import com.concentricsky.android.khanacademy.data.db.User; import com.concentricsky.android.khanacademy.data.db.Video; import com.concentricsky.android.khanacademy.data.remote.KAAPIAdapter; import com.concentricsky.android.khanacademy.util.Log; import com.concentricsky.android.khanacademy.util.ObjectCallback; import com.j256.ormlite.dao.Dao; import com.j256.ormlite.stmt.QueryBuilder; /** * This just displays the profile page as loaded from Khan servers. * * @author austinlally * */ public class ShowProfileActivity extends KADataServiceProviderActivityBase { //TODO done button public static final String LOG_TAG = ShowProfileActivity.class.getSimpleName(); private static final int WEBVIEW_LOAD_TIMEOUT = 5000; private View spinnerView; private WebView webView; private Handler handler = new Handler(); private KAAPIAdapter api; private KADataService dataService; private boolean destroyed = false; /** * Get the current user, and fail if there is none. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); destroyed = false; this.webViewTimeoutPromptDialog = new AlertDialog.Builder(this) .setMessage("The page is taking a long time to respond. Stop loading?") .setPositiveButton("Stop", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { if (webView != null) { webView.stopLoading(); finish(); } } }) .setNegativeButton("Wait", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { stopWebViewLoadTimeout(); } }) .setOnCancelListener(new DialogInterface.OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { startWebViewLoadTimeout(); } }).create(); getActionBar().setDisplayHomeAsUpEnabled(true); setContentView(R.layout.profile); setTitle(getString(R.string.profile_title)); webView = (WebView) findViewById(R.id.web_view); webView.setMinimumWidth(800); enableJavascript(webView); webView.getSettings().setDefaultZoom(ZoomDensity.FAR); webView.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView webView, String url) { Log.d(LOG_TAG, "shouldOverrideUrlLoading: " + url); URL parsed = null; try { parsed = new URL(url); } catch (MalformedURLException e) { // Let the webview figure that one out. return false; } // Only ka links will load in this webview. if (parsed.getHost().equals("www.khanacademy.org")) { // Video urls should link into video detail. See below for another video url format. if (parsed.getPath().equals("/video")) { String query = parsed.getQuery(); if (query != null && query.length() > 0) { String[] items = query.split("&"); String videoId = null; for (String item : items) { String[] parts = item.split("=", 2); if (parts.length > 1) { if ("v".equals(parts[0])) { videoId = parts[1]; break; } } } if (videoId != null) { String[] ids = normalizeVideoAndTopicId(videoId, ""); if (ids != null) { launchVideoDetailActivity(ids[0], ids[1]); return true; } } } // There was no ?v= or something weird is going on. Allow the page // load, which should hit KA's nice "no video found" page. showSpinner(); startWebViewLoadTimeout(); return false; } if (parsed.getPath().startsWith("/profile")) { // navigation within the profile makes sense here. showSpinner(); startWebViewLoadTimeout(); return false; } // Embedded logout option (in upper left menu) can be intercepted and cause the app to be logged out as well. if (parsed.getPath().equals("/logout")) { if (dataService != null) { dataService.getAPIAdapter().logout(); } finish(); return false; // try to let the webview hit logout to get the cookies cleared as we exit } // There is a new kind of video url now.. thanks guys.. // http://www.khanacademy.org/video/subtraction-2 // redirects to // http://www.khanacademy.org/math/arithmetic/addition-subtraction/two_dig_add_sub/v/subtraction-2 String[] path = parsed.getPath().split("/"); List<String> parts = Arrays.asList(path); if (parts.contains("v")) { String videoId = null; String topicId = null; for (int i=path.length-1; i>=0; --i) { if (path[i].equals("v")) { continue; } if (videoId == null) { videoId = path[i]; } else if (topicId == null) { topicId = path[i]; } else { break; } } if (videoId != null && topicId != null && dataService != null) { // Looks like a video url. Double check that we have the topic and video before launching detail activity. Log.d(LOG_TAG, "video and topic ids found; looks like a video url."); String[] ids = normalizeVideoAndTopicId(videoId, topicId); if (ids != null) { launchVideoDetailActivity(ids[0], ids[1]); return true; } } } else if (parsed.getPath().startsWith("/video")) { String videoId = path[path.length-1]; String[] ids = normalizeVideoAndTopicId(videoId, ""); if (ids != null) { launchVideoDetailActivity(ids[0], ids[1]); return true; } } // showSpinner(); // startWebViewLoadTimeout(); // return false; } // All other urls should launch in the browser instead of here, except hash changes if we can distinguish. loadInBrowser(url); return true; } @Override public void onPageFinished(WebView view, String url) { Log.d(LOG_TAG, "onPageFinished"); stopWebViewLoadTimeout(); if (!destroyed) { hideSpinner(); } } @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { Log.d(LOG_TAG, "onPageStarted"); stopWebViewLoadTimeout(); // view.loadUrl("javascript:document.addEventListener( 'DOMContentLoaded',function() {AndroidApplication.jqueryReady()} );"); // handler.postDelayed(new Runnable() { // @Override // public void run() { // if (!destroyed) { // hijack(); // } // } // }, 100); } }); // webView.addJavascriptInterface(new Object() { // public void log(String msg) { // Log.d(LOG_TAG, "JSLOG: " + msg); // } // public void jqueryReady() { // Log.d(LOG_TAG, "javascript: jqueryReady"); // hijack(); // } // }, "AndroidApplication"); // showSpinner(); requestDataService(new ObjectCallback<KADataService>() { @Override public void call(KADataService service) { dataService = service; api = service.getAPIAdapter(); String[] credentials = getCurrentLoginCredentials(); loginUser(credentials[0], credentials[1]); } }); } @SuppressLint("SetJavaScriptEnabled") private void enableJavascript(WebView webView) { webView.getSettings().setJavaScriptEnabled(true); } private String[] normalizeVideoAndTopicId(String videoId, String topicId) { Log.d(LOG_TAG, "videoAndTopicExist: " + videoId + ", " + topicId); if (videoId != null && topicId != null && dataService != null) { Dao<Video, String> videoDao; Dao<Topic, String> topicDao; try { videoDao = dataService.getHelper().getVideoDao(); QueryBuilder<Video, String> q = videoDao.queryBuilder(); q.where().eq("youtube_id", videoId).or().eq("readable_id", videoId); Video video = videoDao.queryForFirst(q.prepare()); if (video != null) { Log.d(LOG_TAG, " video found"); topicDao = dataService.getHelper().getTopicDao(); Topic parent = topicDao.queryForId(topicId); if (parent != null) { Log.d(LOG_TAG, " topic found"); return new String[] {video.getReadable_id(), parent.getId()}; } else { parent = topicDao.queryRaw( "select topic._id from topic, topicvideo where topicvideo.video_id=? and topicvideo.topic_id = topic._id limit 1", topicDao.getRawRowMapper(), new String[] {video.getReadable_id()}).getFirstResult(); if (parent != null) { Log.d(LOG_TAG, " another topic found"); return new String[] {video.getReadable_id(), parent.getId()}; } } } } catch (SQLException e) { e.printStackTrace(); } } return null; } private void launchVideoDetailActivity(String videoId, String topicId) { Intent intent = new Intent(this, VideoDetailActivity.class); intent.putExtra(PARAM_VIDEO_ID, videoId); intent.putExtra(PARAM_TOPIC_ID, topicId); startActivity(intent); } private void loadInBrowser(String url) { startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))); } private Runnable timeoutPromptRunnable = new Runnable() { @Override public void run() { promptLoadTimeout(); } }; private AlertDialog webViewTimeoutPromptDialog; private void startWebViewLoadTimeout() { stopWebViewLoadTimeout(); handler.postDelayed(timeoutPromptRunnable, WEBVIEW_LOAD_TIMEOUT); } private void stopWebViewLoadTimeout() { handler.removeCallbacks(timeoutPromptRunnable); } private void promptLoadTimeout() { stopWebViewLoadTimeout(); Log.d(LOG_TAG, "showing dialog"); webViewTimeoutPromptDialog.show(); } @Override protected void onDestroy() { destroyed = true; if (webView != null) { webView.destroy(); webView = null; } super.onDestroy(); } private KAAPIAdapter.UserLoginHandler userLoginHandler = new KAAPIAdapter.UserLoginHandler() { @Override public void onUserLogin(final User user) { if (destroyed) return; if (user == null) { // No user logged in, likely thanks to bad credentials. launchSignInActivity(); // TODO // Could also be network trouble. } else { // Success. requestDataService(new ObjectCallback<KADataService>() { @Override public void call(KADataService dataService) { dataService.getAPIAdapter().requestUserVideoUpdate(user); } }); loadProfilePage(user); } } }; private void launchSignInActivity() { Intent intent = new Intent(this, SignInActivity.class); startActivityForResult(intent, Constants.REQUEST_CODE_USER_LOGIN); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { Log.d(LOG_TAG, "onActivityResult"); switch (requestCode) { case Constants.REQUEST_CODE_USER_LOGIN: handleLoginResult(resultCode, intent); break; default: // ??? break; } } private void handleLoginResult(int resultCode, Intent intent) { Log.d(LOG_TAG, "request code was user login, result code is " + resultCode); if (destroyed) return; // Sent by SignInActivity switch (resultCode) { case Constants.RESULT_CODE_SUCCESS: String token = intent == null ? null : intent.getStringExtra(Constants.PARAM_OAUTH_TOKEN); String secret = intent == null ? null : intent.getStringExtra(Constants.PARAM_OAUTH_SECRET); loginUser(token, secret); break; case Constants.RESULT_CODE_FAILURE: // Network error, user declines authorization, activity closed before finishing. Toast.makeText(this, "Login failed", Toast.LENGTH_SHORT).show(); // fall through default: // User exits the login dialog without finishing the process. finish(); } } // Begin the login process here. We try to log in with whatever token,secret we have saved. // We will get a callback from the API Adapter after this with a user object. If the user // isn't null, then we're logged in. If it is, that indicates we need to launch the // SignInActivity to get a new set of credentials and try again. private void loginUser(final String token, final String secret) { requestDataService(new ObjectCallback<KADataService>() { @Override public void call(KADataService dataService) { dataService.getAPIAdapter().login(token, secret, userLoginHandler); } }); } private String[] getCurrentLoginCredentials() { Log.d(LOG_TAG, "getCurrentLoginCredentials"); // Make sure this.api is non-null before trying this. User user = api.getCurrentUser(); String[] result = new String[2]; if (user != null) { result[0] = user.getToken(); result[1] = user.getSecret(); } else { result[0] = ""; result[1] = ""; } Log.d(LOG_TAG, String.format(" --> [%s, %s]", result[0], result[1])); return result; } private void loadProfilePage(User user) { String url = "http://www.khanacademy.org/api/auth/token_to_session?continue="; try { url += URLEncoder.encode("/profile", "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); url += "/profile"; // Works ok as of 03/22/2013, but encoding it is of course the right way. } HashMap<String, String> headers = new HashMap<String, String>(); HttpGet request = new HttpGet(url); try { api.getConsumer(user).sign(request); for (Header h : request.getAllHeaders()) { headers.put(h.getName(), h.getValue()); } } catch (OAuthMessageSignerException e) { e.printStackTrace(); } catch (OAuthExpectationFailedException e) { e.printStackTrace(); } catch (OAuthCommunicationException e) { e.printStackTrace(); } webView.loadUrl(url, headers); } /** * Show a loading indicator. */ protected void showSpinner() { if (spinnerView == null) { spinnerView = getLayoutInflater().inflate(R.layout.spinner, null, false); } handler.post(new Runnable() { @Override public void run() { ViewGroup parent = (ViewGroup) spinnerView.getParent(); if (parent != null) parent.removeView(spinnerView); ((FrameLayout) findViewById(R.id.popover_view)).addView(spinnerView); } }); } /** * Hide any loading indicator. */ protected void hideSpinner() { if (spinnerView != null) { handler.post(new Runnable() { @Override public void run() { ViewGroup parent = (ViewGroup) spinnerView.getParent(); if (parent != null) parent.removeView(spinnerView); } }); } } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: finish(); return true; default: return super.onOptionsItemSelected(item); } } @Override public void onBackPressed() { if (webView != null && webView.canGoBack()) { webView.goBack(); } else { super.onBackPressed(); } } }