/* 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 java.net.MalformedURLException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import oauth.signpost.OAuth; import oauth.signpost.OAuthConsumer; import oauth.signpost.OAuthProvider; import oauth.signpost.commonshttp.CommonsHttpOAuthProvider; import oauth.signpost.exception.OAuthCommunicationException; import oauth.signpost.exception.OAuthExpectationFailedException; import oauth.signpost.exception.OAuthMessageSignerException; import oauth.signpost.exception.OAuthNotAuthorizedException; import org.apache.http.Header; import org.apache.http.client.methods.HttpGet; import android.annotation.SuppressLint; import android.content.Intent; import android.net.Uri; import android.net.UrlQuerySanitizer; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.FrameLayout; import com.concentricsky.android.khan.R; import com.concentricsky.android.khanacademy.Constants; import com.concentricsky.android.khanacademy.data.KADataService; import com.concentricsky.android.khanacademy.util.Log; import com.concentricsky.android.khanacademy.util.ObjectCallback; /** * We need a token from khanacademy.org, so we load the authentication page from there. That page * redirects the user to Google or Facebook and asks them to authenticate. Google or Facebook calls * back to khanacademy.org with an authenticated token. At this point, we receive a callback with * an authenticated request token from khanacademy, which we then exchange for an access token. * The access token is long-lived, and we store it to log back in automatically on subsequent runs. * * @author austinlally * */ public class SignInActivity extends KADataServiceProviderActivityBase { public static final String LOG_TAG = SignInActivity.class.getSimpleName(); private static final String KHAN_API_URL = "http://www.khanacademy.org/api"; private static final String OAUTH_REQUEST_URL = KHAN_API_URL + "/auth/request_token"; private static final String OAUTH_ACCESS_TOKEN_URL = KHAN_API_URL + "/auth/access_token"; private static final String OAUTH_CALLBACK_URL = "khan-oauth:///"; public static final String ACTION_OAUTH = "com.concentricsky.android.khanacademy.ACTION_OAUTH"; private OAuthConsumer consumer; @SuppressWarnings("rawtypes") private List<AsyncTask> currentTasks = new ArrayList<AsyncTask>(); private View spinnerView; private Handler handler = new Handler(); private WebView webView; /** * Set up a url filter to catch our callback, then start a request token operation. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getActionBar().setDisplayHomeAsUpEnabled(true); setTitle("Log in"); // Show no activity transition animation; we want to appear as if we're the same activity as the underlying ShowProfileActivity. overridePendingTransition(0, 0); setContentView(R.layout.popover_web); webView = (WebView) findViewById(R.id.web_view); enableJavascript(webView); webView.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView webView, String url) { showSpinner(); Log.d(LOG_TAG, ">>>>>>>>>>>>> url: " + url); if (url.contains(OAUTH_CALLBACK_URL)) { currentTasks.add(new AccessTokenFetcher().execute(url)); return true; } return false; } @Override public void onPageFinished(WebView view, String url) { hideSpinner(); } }); showSpinner(); requestDataService(new ObjectCallback<KADataService>() { @Override public void call(KADataService service) { consumer = service.getAPIAdapter().getConsumer(null); currentTasks.add(new RequestTokenFetcher().execute()); } }); } @SuppressLint("SetJavaScriptEnabled") private void enableJavascript(WebView webView) { webView.getSettings().setJavaScriptEnabled(true); } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.sign_in, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: finish(); return true; case R.id.register: // Launch KA's registration page in the browser. startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.url_register)))); return true; default: return super.onOptionsItemSelected(item); } } /** * Cancel tasks on pause. */ @Override protected void onPause() { super.onPause(); // Show no activity transition animation; we want to appear as if we're the same activity as the underlying ShowProfileActivity. overridePendingTransition(0, 0); for (@SuppressWarnings("rawtypes") AsyncTask t : currentTasks) { t.cancel(true); } } /** * Signs a request with our consumer token and consumer secret, and loads * khanacademy's authentication page with it. * * @author austinlally */ private class RequestTokenFetcher extends AsyncTask<Void, Void, Void> { @Override protected Void doInBackground(Void... params) { String requestUrl = OAUTH_REQUEST_URL; requestUrl += "?view=mobile&oauth_callback=" + OAUTH_CALLBACK_URL; final HttpGet request = new HttpGet(requestUrl); try { synchronized (consumer) { consumer.sign(request); } final Map<String, String> headers = new HashMap<String, String>(); Log.d(LOG_TAG, "request line: " + request.getRequestLine().toString()); for (Header h : request.getAllHeaders()) { Log.d(LOG_TAG, h.getName() + ": " + h.getValue()); headers.put(h.getName(), h.getValue()); } handler.post(new Runnable() { @Override public void run() { try { webView.loadUrl(request.getURI().toURL().toString(), headers); } catch (MalformedURLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }); } catch (OAuthMessageSignerException e1) { setResult(Constants.RESULT_CODE_FAILURE); e1.printStackTrace(); } catch (OAuthExpectationFailedException e1) { setResult(Constants.RESULT_CODE_FAILURE); e1.printStackTrace(); } catch (OAuthCommunicationException e1) { setResult(Constants.RESULT_CODE_FAILURE); e1.printStackTrace(); } return null; } @Override public void onPostExecute(Void result) { currentTasks.remove(this); } } /** * Given our callback url with oauth-related query string containing an * authenticated request token, secret, and verifier, as received from khan, * this task requests an access token and then launches a user info task and * a user videos task. * * @author austinlally * */ private class AccessTokenFetcher extends AsyncTask<String, Void, String[]> { @Override protected String[] doInBackground(String... params) { String url = params[0]; UrlQuerySanitizer sanitizer = new UrlQuerySanitizer(); String[] parameters = new String[] { OAuth.OAUTH_TOKEN, OAuth.OAUTH_TOKEN_SECRET, OAuth.OAUTH_VERIFIER }; UrlQuerySanitizer.ValueSanitizer valueSanitizer = new UrlQuerySanitizer.ValueSanitizer() { @Override public String sanitize(String value) { return value; } }; sanitizer.registerParameters(parameters, valueSanitizer); sanitizer.parseUrl(url); synchronized (consumer) { consumer.setTokenWithSecret(sanitizer.getValue(OAuth.OAUTH_TOKEN), sanitizer.getValue(OAuth.OAUTH_TOKEN_SECRET)); } OAuthProvider provider = new CommonsHttpOAuthProvider(null, OAUTH_ACCESS_TOKEN_URL, null); //since we aren't using provider.retrieveRequestToken, this isn't set for us provider.setOAuth10a(true); Log.d(LOG_TAG, "endpoint " + provider.getAccessTokenEndpointUrl()); Log.d(LOG_TAG, "setting token " + sanitizer.getValue(OAuth.OAUTH_TOKEN) + " and secret " + sanitizer.getValue(OAuth.OAUTH_TOKEN_SECRET) + " and verifier " + sanitizer.getValue(OAuth.OAUTH_VERIFIER)); try { provider.retrieveAccessToken(consumer, sanitizer.getValue(OAuth.OAUTH_VERIFIER)); } catch (OAuthMessageSignerException e) { setResult(Constants.RESULT_CODE_FAILURE); e.printStackTrace(); } catch (OAuthNotAuthorizedException e) { setResult(Constants.RESULT_CODE_FAILURE); e.printStackTrace(); } catch (OAuthExpectationFailedException e) { setResult(Constants.RESULT_CODE_FAILURE); e.printStackTrace(); } catch (OAuthCommunicationException e) { setResult(Constants.RESULT_CODE_FAILURE); e.printStackTrace(); } Log.d(LOG_TAG, "now token is " + consumer.getToken() + " and secret is " + consumer.getTokenSecret()); // Consider ourselves logged in. return new String[] {consumer.getToken(), consumer.getTokenSecret()}; } @Override public void onPostExecute(String[] result) { currentTasks.remove(this); Intent resultIntent = new Intent(); resultIntent.putExtra(Constants.PARAM_OAUTH_TOKEN, result[0]); resultIntent.putExtra(Constants.PARAM_OAUTH_SECRET, result[1]); SignInActivity.this.setResult(Constants.RESULT_CODE_SUCCESS, resultIntent); finish(); } } /** * Show a loading indicator. */ protected void showSpinner() { if (spinnerView == null) { spinnerView = getLayoutInflater().inflate(R.layout.spinner, null, false); } handler.post(new Runnable() { 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() { public void run() { ViewGroup parent = (ViewGroup) spinnerView.getParent(); if (parent != null) parent.removeView(spinnerView); } }); } } }