This tutorial will explain how to allow a user to sign into YouTube, getting your application authorised so you can use the YouTube API. This will allow you to upload / like / post comments etc for the YouTube user. This uses OAuth in the standard way i.e. requesting and receiving an access token.
References I’ve used for this tutorial are here:
YouTube API here.
Developers Guide OAuth2
Guide Installed Applications
Google API Console
First off you have to register a client with google API’s to get a client ID. This is explained here: Part 1. Once you’ve got a client ID we can get on with creating the app.
What we are going to do:
- Allow a user to login to YouTube using a WebView
- Capture the response from YouTube
- Parse this response to gain an authorisation code
- Do a Http Request to exchange the auth code for an access token
The access token is your golden key, this is what you use in all YouTube API requests that require a user to be logged in.
Here .. we .. go.
First off we just have a nice little intro activity, this has your sign in button for the user to sign in to YouTube. Clicking this button starts our login webview. It uses annotations to highlight the xml entry point, further explanation here. When signing in has finished we get the result, successful or not. If we have signed in to YouTube we print the result to LogCat. If the user has failed to sign in we start another activity explaining why they should sign in.
MainActivity.java
package com.blundell.youtubesignin.ui; import com.blundell.youtubesignin.FromXML; import com.blundell.youtubesignin.R; import com.blundell.youtubesignin.domain.Tokens; import com.blundell.youtubesignin.util.Log; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.Toast; /** * Main Activity holding a sign into YouTube button * @author paul.blundell * */ public class MainActivity extends Activity { private static final int REQ_OAUTH = 123; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } @FromXML public void onSignInClick(View button){ startAuthorisationForYouTube(); } private void startAuthorisationForYouTube() { Intent intent = new Intent(this, OAuthActivity.class); startActivityForResult(intent, REQ_OAUTH); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if(requestCode == REQ_OAUTH){ if(resultCode == RESULT_OK){ dealWithResult(data); } else if(resultCode == RESULT_CANCELED){ startRefusalActivity(); } } } private void dealWithResult(Intent data) { Tokens tokens = (Tokens) data.getSerializableExtra(OAuthActivity.EXTRA_TOKENS); Toast.makeText(this, "Access Token retrieved. See your LogCat.", Toast.LENGTH_SHORT).show(); logResult(tokens); } private void startRefusalActivity() { Intent intent = new Intent(this, RefusedAuthActivity.class); startActivity(intent); } /** * Instead of logging the result you would probably save it to shared preferences. * Then you can use your access token whenever you call the YouTube API */ private static void logResult(Tokens tokens) { Log.i("Got access token: "+ tokens.getAccessToken()); Log.i("Got refresh token: "+ tokens.getRefreshToken()); Log.i("Got token type: "+ tokens.getTokenType()); Log.i("Got expires in: "+ tokens.getExpiresIn()); } }
After clicking sign in a new activity with a webview is presented. The webview is pretty lightweight for this tutorial and I haven’t even added a loading animation in between webpages so it may look like it isn’t doing anything but it is. Once the user is signed in it will then ask if they want to allow your app access to their YouTube account.
When either “allow access” or “no thanks” is clicked the webview is redirected to the url that is in your api console. In this tutorial it is set to “https://localhost” which is a pretty crap and non-unique name. If I was doing this again I’d pick something more unique. Perhaps even a real url on my server so a nice page is displayed (if only for a split second).
The difference between “allow access” and “no thanks” is what is appended after your redirect url. If they allow access it adds a code paramater, if they clicked no thanks an access_denied paramater is added. We capture these urls in our webviews client. This allows us to change the application flow for access or denial.
OAuthActivity.java
package com.blundell.youtubesignin.ui; import static com.blundell.youtubesignin.oauth.Constants.OAUTH_URL; import com.blundell.youtubesignin.R; import com.blundell.youtubesignin.domain.Tokens; import com.blundell.youtubesignin.oauth.OAuthWebViewClient; import com.blundell.youtubesignin.oauth.OnOAuthListener; import com.blundell.youtubesignin.oauth.ParamChecker; import com.blundell.youtubesignin.oauth.tokens.GetTokensTask; import com.blundell.youtubesignin.oauth.tokens.TokenRetrievedListener; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.webkit.WebView; import android.widget.Toast; /** * Activity incorporates signing into YouTube and Retrieving the access_token for YouTube API access in the future * @author paul.blundell * */ public class OAuthActivity extends Activity implements OnOAuthListener, TokenRetrievedListener { public static final String EXTRA_TOKENS = "com.blundell.youtubesignin.ui.OAuthActivity.EXTRA_TOKENS"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_oauth); setResult(RESULT_CANCELED); WebView webview = (WebView) findViewById(R.id.webview); webview.setWebViewClient(new OAuthWebViewClient(new ParamChecker(this))); webview.loadUrl(OAUTH_URL); Toast.makeText(this, "Loading .. just wait", Toast.LENGTH_LONG).show(); } @Override public void onAuthorized(String authCode) { dealWithAccessGranted(authCode); } @Override public void onRefused() { dealWithRefusal(); } private void dealWithAccessGranted(String authCode) { // You'd probably want to call this in a service https://blog.blundellapps.co.uk/tut-networking-off-the-ui-thread-solid-architecture/ new Thread(new GetTokensTask(authCode, this)).start(); } private void dealWithRefusal() { setResult(RESULT_CANCELED); finish(); } @Override public void onTokensRetrieved(Tokens tokens) { Intent intent = createSendableBundle(tokens); setResult(RESULT_OK, intent); finish(); } private static Intent createSendableBundle(Tokens tokens) { Intent intent = new Intent(); intent.putExtra(EXTRA_TOKENS, tokens); return intent; } }
OAuthWebViewClient.java
package com.blundell.youtubesignin.oauth; import static com.blundell.youtubesignin.oauth.Constants.CALLBACK_URL; import android.view.View; import android.webkit.WebView; import android.webkit.WebViewClient; /** * Our webview client takes care of capturing any URL that is loaded, * it keeps a lookout for the redirect url and when this is loaded we * inform the listener (our ParamChecker) that either * "allow access" or "no thanks" has been clicked ( we don't know which yet) * @author paul.blundell * */ public class OAuthWebViewClient extends WebViewClient { private final OnOAuthCallbackListener oAuthCallbackListener; public OAuthWebViewClient(OnOAuthCallbackListener oAuthCallbackListener) { this.oAuthCallbackListener = oAuthCallbackListener; } @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if(weHaveReceivedAnOAuthCallback(url)){ // Because the oAuthCallback is our redirect url // and this url is not a real webpage // the webview shows 'page not found' for a split second, // to hide this, we hide the webview (we've finished with it anyway) view.setVisibility(View.GONE); String reply = retrieveParamaters(url); oAuthCallbackListener.onOAuthCallback(reply); } return false; } private static boolean weHaveReceivedAnOAuthCallback(String url) { return url.startsWith(CALLBACK_URL); } private static String retrieveParamaters(String url) { return url.replace(CALLBACK_URL, ""); }; }
OnOAuthCallbackListener.java
package com.blundell.youtubesignin.oauth; /** * Callback that our redirect url has been loaded * @author paul.blundell * */ public interface OnOAuthCallbackListener { void onOAuthCallback(String params); }
Creating a listener for when the redirect url is being loaded helps us keep to the single responsibility principle, the activity is kept cleaner and our webviewclient is lightweight. When the user selects “allow access” and the code paramater is added, we need to capture this code, it is used for authorisation later on. Capturing this code is easy as splitting it off the end of the url. This job is delegated to our ParamChecker class.
ParamChecker.java
package com.blundell.youtubesignin.oauth; /** * This class checks what paramater's our redirect url has informing our listener * it also passes the auth code if access is granted. * * Google Documentation: * If the user granted access to your application, * Google will have appended a code parameter to the redirect_uri. * This value is a temporary authorization code that you can exchange for an access token. * example : https://localhost/oauth2callback?code=4/ux5gNj-_mIu4DOD_gNZdjX9EtOFf * * If the user refused to grant access to your application, * Google will have included the access_denied error message in the hash fragment of the redirect_uri. * example : https://localhost/oauth2callback#error=access_denied * * @author paul.blundell * */ public class ParamChecker implements OnOAuthCallbackListener { private final OnOAuthListener onOAuth; public ParamChecker(OnOAuthListener onOAuth) { this.onOAuth = onOAuth; } @Override public void onOAuthCallback(String params) { if(params.contains("access_denied")){ // User said no onOAuth.onRefused(); } else { // User auth'd us String authCode = extractAuthCode(params); onOAuth.onAuthorized(authCode); } } private static String extractAuthCode(String params) { return params.substring(Constants.AUTH_CODE_PARAM.length()+1); } }
The param checker is also responsible for indicating if our app was allowed access or not. This uses a callback listener, our OAuthActivity is informed of the result.
OnOAuthListener.java
package com.blundell.youtubesignin.oauth; /** * Callback to determine which option the user selected when asking for OAuth * @author paul.blundell * */ public interface OnOAuthListener { void onAuthorized(String authCode); void onRefused(); }
When our OAuthActivity is told we have been refused access it loads another activity to tell the user our app can’t do much else if you deny access.
When our OAuthActivity is told we have been granted access (and is passed the auth code) we then start our final task to get the access token. Don’t forget the access token is our golden key, allowing us to perform YouTube API calls on behalf of the user. The final task is our GetTokensTask, this sends a request to the Google servers swapping our auth code for the access token. This is in the form of a POST request, you also have to add some other parameters to the POST request. These include your client id that you get from registering your app (see start of this post).
GetTokensTask.java
package com.blundell.youtubesignin.oauth.tokens; import static com.blundell.youtubesignin.oauth.Constants.*; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.List; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.message.BasicNameValuePair; import org.json.JSONException; import org.json.JSONObject; import com.blundell.youtubesignin.domain.Tokens; import com.blundell.youtubesignin.util.Log; import com.blundell.youtubesignin.util.StreamConverter; /** * This task exchanges the authorization code for an Access Token and a Refresh Token, * when complete it will Log these out to the console, in a development environment you would * save them to the shared preferences so you can use them for other calls to the YouTube API * * @author paul.blundell */ public class GetTokensTask implements Runnable { // The authcode you obtained when the user granted your app access to there YouTube account private final String authCode; private final TokenRetrievedListener retrievedListener; public GetTokensTask(String authCode, TokenRetrievedListener retrievedListener) { this.authCode = authCode; this.retrievedListener = retrievedListener; } @Override public void run() { try { HttpResponse response = requestYouTubeAccessTokens(); Tokens tokens = parseYouTubeAccessTokens(response); retrievedListener.onTokensRetrieved(tokens); } catch (ClientProtocolException e) { Log.e("ClientProtocolException", e); } catch (IOException e) { Log.e("IOException", e); } catch (IllegalStateException e) { Log.e("IllegalStateException", e); } catch (JSONException e) { Log.e("JSONException", e); } } /** * Fires off the request for an access token to the Google servers * @return the response which should contain JSON holding the access token */ private HttpResponse requestYouTubeAccessTokens() throws IOException, ClientProtocolException { HttpPost post = createTokenRetrievalPost(); DefaultHttpClient client = new DefaultHttpClient(); HttpResponse response = client.execute(post); return response; } /** * To gain an access token we have to send google our auth code and client credential's, * these are passed into this task found in your API console https://code.google.com/apis/console respectively * * @return returns the Post request that we can then execute */ private HttpPost createTokenRetrievalPost() throws UnsupportedEncodingException { HttpPost post = new HttpPost(TOKENS_URL); post.setHeader("content-type", "application/x-www-form-urlencoded"); List<NameValuePair> nameValuePair = new ArrayList<NameValuePair>(4); nameValuePair.add(new BasicNameValuePair("code", authCode)); nameValuePair.add(new BasicNameValuePair("client_id", CLIENT_ID)); nameValuePair.add(new BasicNameValuePair("redirect_uri", CALLBACK_URL)); nameValuePair.add(new BasicNameValuePair("grant_type", "authorization_code")); post.setEntity(new UrlEncodedFormEntity(nameValuePair)); return post; } /** * @param response The response from the YouTUbe post request we just made * @return Our TokenParser so we can read the fields off it (just for logging) */ private static Tokens parseYouTubeAccessTokens(HttpResponse response) throws JSONException, IOException { JSONObject jsonObject = StreamConverter.convertStreamToJsonObject(response.getEntity().getContent()); return new TokenParser(jsonObject).getTokens(); } }
When you send the POST request to exchange your auth code for the access token, Google sends back JSON, this needs to be parsed to retrieve the access token. In this JSON is the access token, a refresh token and a expiry time. The refresh token is used if your access_token expires, you’ll use it to gain another (new) access token. We capture these tokens into a domain object, that way we can pass it between activities.
TokenParser.java
package com.blundell.youtubesignin.oauth.tokens; import org.json.JSONException; import org.json.JSONObject; import com.blundell.youtubesignin.domain.Tokens; import com.blundell.youtubesignin.util.Log; /** * Parses the JSON recieved from Google when we are swapping our auth code for access * * Example: * { * "access_token" : "ya29.AHES6ZTtm7SuokEB-RGtbBty9IIlNiP9-eNMMQKtXdMP3sfjL1Fc", * "token_type" : "Bearer", * "expires_in" : 3600, * "refresh_token" : "1/HKSmLFXzqP0leUihZp2xUt3-5wkU7Gmu2Os_eBnzw74" * } * @author paul.blundell * */ public class TokenParser { private final JSONObject jsonObject; private Tokens tokens; public TokenParser(JSONObject jsonObject) throws JSONException { this.jsonObject = jsonObject; parse(); } private void parse() throws JSONException{ Log.d(jsonObject.toString()); String accessToken = jsonObject.getString("access_token"); String tokenType = jsonObject.getString("token_type"); int expiresIn = jsonObject.getInt("expires_in"); String refreshToken = jsonObject.getString("refresh_token"); tokens = new Tokens(accessToken, refreshToken, expiresIn, tokenType); } public Tokens getTokens() { return tokens; } }
Tokens.java
package com.blundell.youtubesignin.domain; import java.io.Serializable; /** * Wrapper for holding the access and other tokens * @author paul.blundell * */ public class Tokens implements Serializable { private final String accessToken; private final String refreshToken; private final int expiresIn; private final String tokenType; public Tokens(String accessToken, String refreshToken, int expiresIn, String tokenType) { this.accessToken = accessToken; this.refreshToken = refreshToken; this.expiresIn = expiresIn; this.tokenType = tokenType; } public String getAccessToken() { return accessToken; } public String getRefreshToken() { return refreshToken; } public int getExpiresIn() { return expiresIn; } public String getTokenType() { return tokenType; }; }
In this tutorial when you gain your access token it is just printed to your LogCat console. In your app you will want to store the access and refresh tokens (probably to sharedpreferences) you can then use it for your API calls.
Thats it! You have now been OAuth’d to manage your users YouTube account.
Any questions just ask.
Sources:
Probably found answer
webview.getSettings().setJavaScriptEnabled(true);
Hello, Paul. Nice tutorial. Thanks.
I’ve faced with problem on this example.
When I submit login and pass then browser brings me to page with next message
“You’ve reached this page because we have detected that Javascript is disabled in your browser. The page you attempted to load cannot display properly if scripts are disabled.
Please enable scripts and retry the operation or go back in your browser.”
On default browser Javascript is enabled. Maybe I’ve missed smth? Any thoughts? Thanks.
Hello! After implementing your code, every time, I sign in with my Google account in my app and grant it access to my youtube account, all I get is a WebView, showing
“Please copy this code, switch to your application and paste it there: [A LONG CREEPY CODE]”
I think they may have changed they’re API 🙁