* directly integrated LiveSDK files to allow modification and simplify build

* added initializeSynchronous method to LiveSDK
* added UserInteractionRequiredException, now thrown by GDrive+SkyDrive
This commit is contained in:
Philipp Crocoll 2014-07-30 06:02:35 +02:00
parent 3831dbb345
commit 7fa3dd191f
56 changed files with 7062 additions and 13 deletions

2
src/java/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/InputStickAPI - Kopie
/DemoBT

View File

@ -14,4 +14,3 @@ proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.
target=android-17
android.library=true
android.library.reference.1=..\\..\\..\\..\\..\\..\\..\\AppData\\Local\\Android\\android-sdk\\extras\\google\\google_play_services\\libproject\\google-play-services_lib
android.library.reference.2=../../../../LiveSDK-for-Android/src

View File

@ -0,0 +1,77 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import java.util.List;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.message.BasicNameValuePair;
import android.text.TextUtils;
import com.microsoft.live.OAuth.GrantType;
/**
* AccessTokenRequest represents a request for an Access Token.
* It subclasses the abstract class TokenRequest, which does most of the work.
* This class adds the proper parameters for the access token request via the
* constructBody() hook.
*/
class AccessTokenRequest extends TokenRequest {
/**
* REQUIRED. The authorization code received from the
* authorization server.
*/
private final String code;
/** REQUIRED. Value MUST be set to "authorization_code". */
private final GrantType grantType;
/**
* REQUIRED, if the "redirect_uri" parameter was included in the
* authorization request as described in Section 4.1.1, and their
* values MUST be identical.
*/
private final String redirectUri;
/**
* Constructs a new AccessTokenRequest, and initializes its member variables
*
* @param client the HttpClient to make HTTP requests on
* @param clientId the client_id of the calling application
* @param redirectUri the redirect_uri to be called back
* @param code the authorization code received from the AuthorizationRequest
*/
public AccessTokenRequest(HttpClient client,
String clientId,
String redirectUri,
String code) {
super(client, clientId);
assert !TextUtils.isEmpty(redirectUri);
assert !TextUtils.isEmpty(code);
this.redirectUri = redirectUri;
this.code = code;
this.grantType = GrantType.AUTHORIZATION_CODE;
}
/**
* Adds the "code", "redirect_uri", and "grant_type" parameters to the body.
*
* @param body the list of NameValuePairs to be placed in the body of the HTTP request
*/
@Override
protected void constructBody(List<NameValuePair> body) {
body.add(new BasicNameValuePair(OAuth.CODE, this.code));
body.add(new BasicNameValuePair(OAuth.REDIRECT_URI, this.redirectUri));
body.add(new BasicNameValuePair(OAuth.GRANT_TYPE,
this.grantType.toString().toLowerCase()));
}
}

View File

@ -0,0 +1,252 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.auth.AUTH;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.message.BasicHeader;
import org.json.JSONException;
import org.json.JSONObject;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
/**
* ApiRequest is an abstract base class that represents an Http Request made by the API.
* It does most of the Http Request work inside of the execute method, and provides a
* an abstract factory method for subclasses to choose the type of Http Request to be
* created.
*/
abstract class ApiRequest<ResponseType> {
public interface Observer {
public void onComplete(HttpResponse response);
}
public enum Redirects {
SUPPRESS {
@Override
protected void setQueryParameterOn(UriBuilder builder) {
Redirects.setQueryParameterOn(builder, Boolean.TRUE);
}
}, UNSUPPRESSED {
@Override
protected void setQueryParameterOn(UriBuilder builder) {
Redirects.setQueryParameterOn(builder, Boolean.FALSE);
}
};
/**
* Sets the suppress_redirects query parameter by removing all existing ones
* and then appending it on the given UriBuilder.
*/
protected abstract void setQueryParameterOn(UriBuilder builder);
private static void setQueryParameterOn(UriBuilder builder, Boolean value) {
// The Live SDK is designed to use our value of suppress_redirects.
// If it uses any other value it could cause issues. Remove any previously
// existing suppress_redirects and use ours.
builder.removeQueryParametersWithKey(QueryParameters.SUPPRESS_REDIRECTS);
builder.appendQueryParameter(QueryParameters.SUPPRESS_REDIRECTS, value.toString());
}
}
public enum ResponseCodes {
SUPPRESS {
@Override
protected void setQueryParameterOn(UriBuilder builder) {
ResponseCodes.setQueryParameterOn(builder, Boolean.TRUE);
}
}, UNSUPPRESSED {
@Override
protected void setQueryParameterOn(UriBuilder builder) {
ResponseCodes.setQueryParameterOn(builder, Boolean.FALSE);
}
};
/**
* Sets the suppress_response_codes query parameter by removing all existing ones
* and then appending it on the given UriBuilder.
*/
protected abstract void setQueryParameterOn(UriBuilder builder);
private static void setQueryParameterOn(UriBuilder builder, Boolean value) {
// The Live SDK is designed to use our value of suppress_response_codes.
// If it uses any other value it could cause issues. Remove any previously
// existing suppress_response_codes and use ours.
builder.removeQueryParametersWithKey(QueryParameters.SUPPRESS_RESPONSE_CODES);
builder.appendQueryParameter(QueryParameters.SUPPRESS_RESPONSE_CODES, value.toString());
}
}
private static final Header LIVE_LIBRARY_HEADER =
new BasicHeader("X-HTTP-Live-Library", "android/" + Build.VERSION.RELEASE + "_" +
Config.INSTANCE.getApiVersion());
private static final int SESSION_REFRESH_BUFFER_SECS = 30;
private static final int SESSION_TOKEN_SEND_BUFFER_SECS = 3;
/**
* Constructs a new instance of a Header that contains the
* @param accessToken to construct inside the Authorization header
* @return a new instance of a Header that contains the Authorization access_token
*/
private static Header createAuthroizationHeader(LiveConnectSession session) {
assert session != null;
String accessToken = session.getAccessToken();
assert !TextUtils.isEmpty(accessToken);
String tokenType = OAuth.TokenType.BEARER.toString().toLowerCase();
String value = TextUtils.join(" ", new String[]{tokenType, accessToken});
return new BasicHeader(AUTH.WWW_AUTH_RESP, value);
}
private final HttpClient client;
private final List<Observer> observers;
private final String path;
private final ResponseHandler<ResponseType> responseHandler;
private final LiveConnectSession session;
protected final UriBuilder requestUri;
/** The original path string parsed into a Uri object. */
protected final Uri pathUri;
public ApiRequest(LiveConnectSession session,
HttpClient client,
ResponseHandler<ResponseType> responseHandler,
String path) {
this(session, client, responseHandler, path, ResponseCodes.SUPPRESS, Redirects.SUPPRESS);
}
/**
* Constructs a new instance of an ApiRequest and initializes its member variables
*
* @param session that contains the access_token
* @param client to make Http Requests on
* @param responseHandler to handle the response
* @param path of the request. it can be relative or absolute.
*/
public ApiRequest(LiveConnectSession session,
HttpClient client,
ResponseHandler<ResponseType> responseHandler,
String path,
ResponseCodes responseCodes,
Redirects redirects) {
assert session != null;
assert client != null;
assert responseHandler != null;
assert !TextUtils.isEmpty(path);
this.session = session;
this.client = client;
this.observers = new ArrayList<Observer>();
this.responseHandler = responseHandler;
this.path = path;
UriBuilder builder;
this.pathUri = Uri.parse(path);
if (this.pathUri.isAbsolute()) {
// if the path is absolute we will just use that entire path
builder = UriBuilder.newInstance(this.pathUri);
} else {
// if it is a relative path then we should use the config's API URI,
// which is usually something like https://apis.live.net/v5.0
builder = UriBuilder.newInstance(Config.INSTANCE.getApiUri())
.appendToPath(this.pathUri.getEncodedPath())
.query(this.pathUri.getQuery());
}
responseCodes.setQueryParameterOn(builder);
redirects.setQueryParameterOn(builder);
this.requestUri = builder;
}
public void addObserver(Observer observer) {
this.observers.add(observer);
}
/**
* Performs the Http Request and returns the response from the server
*
* @return an instance of ResponseType from the server
* @throws LiveOperationException if there was an error executing the HttpRequest
*/
public ResponseType execute() throws LiveOperationException {
// Let subclass decide which type of request to instantiate
HttpUriRequest request = this.createHttpRequest();
request.addHeader(LIVE_LIBRARY_HEADER);
if (this.session.willExpireInSecs(SESSION_REFRESH_BUFFER_SECS)) {
this.session.refresh();
}
// if the session will soon expire, try to send the request without a token.
// the request *may* not need the token, let's give it a try rather than
// risk a request with an invalid token.
if (!this.session.willExpireInSecs(SESSION_TOKEN_SEND_BUFFER_SECS)) {
request.addHeader(createAuthroizationHeader(this.session));
}
try {
HttpResponse response = this.client.execute(request);
for (Observer observer : this.observers) {
observer.onComplete(response);
}
return this.responseHandler.handleResponse(response);
} catch (ClientProtocolException e) {
throw new LiveOperationException(ErrorMessages.SERVER_ERROR, e);
} catch (IOException e) {
// The IOException could contain a JSON object body
// (see InputStreamResponseHandler.java). If it does,
// we want to throw an exception with its message. If it does not, we want to wrap
// the IOException.
try {
new JSONObject(e.getMessage());
throw new LiveOperationException(e.getMessage());
} catch (JSONException jsonException) {
throw new LiveOperationException(ErrorMessages.SERVER_ERROR, e);
}
}
}
/** @return the HTTP method being performed by the request */
public abstract String getMethod();
/** @return the path of the request */
public String getPath() {
return this.path;
}
public void removeObserver(Observer observer) {
this.observers.remove(observer);
}
/**
* Factory method that allows subclasses to choose which type of request will
* be performed.
*
* @return the HttpRequest to perform
* @throws LiveOperationException if there is an error creating the HttpRequest
*/
protected abstract HttpUriRequest createHttpRequest() throws LiveOperationException;
}

View File

@ -0,0 +1,175 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import java.util.ArrayList;
import android.os.AsyncTask;
import com.microsoft.live.EntityEnclosingApiRequest.UploadProgressListener;
/**
* ApiRequestAsync performs an async ApiRequest by subclassing AsyncTask
* and executing the request inside of doInBackground and giving the
* response to the appropriate listener on the main/UI thread.
*/
class ApiRequestAsync<ResponseType> extends AsyncTask<Void, Long, Runnable>
implements UploadProgressListener {
public interface Observer<ResponseType> {
public void onComplete(ResponseType result);
public void onError(LiveOperationException e);
}
public interface ProgressObserver {
public void onProgress(Long... values);
}
private class OnCompleteRunnable implements Runnable {
private final ResponseType response;
public OnCompleteRunnable(ResponseType response) {
assert response != null;
this.response = response;
}
@Override
public void run() {
for (Observer<ResponseType> observer : observers) {
observer.onComplete(this.response);
}
}
}
private class OnErrorRunnable implements Runnable {
private final LiveOperationException exception;
public OnErrorRunnable(LiveOperationException exception) {
assert exception != null;
this.exception = exception;
}
@Override
public void run() {
for (Observer<ResponseType> observer : observers) {
observer.onError(this.exception);
}
}
}
/**
* Static constructor. Prefer to use this over the normal constructor, because
* this will infer the generic types, and be less verbose.
*
* @param request
* @return a new ApiRequestAsync
*/
public static <T> ApiRequestAsync<T> newInstance(ApiRequest<T> request) {
return new ApiRequestAsync<T>(request);
}
/**
* Static constructor. Prefer to use this over the normal constructor, because
* this will infer the generic types, and be less verbose.
*
* @param request
* @return a new ApiRequestAsync
*/
public static <T> ApiRequestAsync<T> newInstance(EntityEnclosingApiRequest<T> request) {
return new ApiRequestAsync<T>(request);
}
private final ArrayList<Observer<ResponseType>> observers;
private final ArrayList<ProgressObserver> progressListeners;
private final ApiRequest<ResponseType> request;
{
this.observers = new ArrayList<Observer<ResponseType>>();
this.progressListeners = new ArrayList<ProgressObserver>();
}
/**
* Constructs a new ApiRequestAsync object and initializes its member variables.
*
* This method attaches a progress observer to the EntityEnclosingApiRequest, and call
* publicProgress when ever there is an on progress event.
*
* @param request
*/
public ApiRequestAsync(EntityEnclosingApiRequest<ResponseType> request) {
assert request != null;
// Whenever the request has upload progress we need to publish the progress, so
// listen to progress events.
request.addListener(this);
this.request = request;
}
/**
* Constructs a new ApiRequestAsync object and initializes its member variables.
*
* @param operation to launch in an asynchronous manner
*/
public ApiRequestAsync(ApiRequest<ResponseType> request) {
assert request != null;
this.request = request;
}
public boolean addObserver(Observer<ResponseType> observer) {
return this.observers.add(observer);
}
public boolean addProgressObserver(ProgressObserver observer) {
return this.progressListeners.add(observer);
}
@Override
public void onProgress(long totalBytes, long numBytesWritten) {
publishProgress(Long.valueOf(totalBytes), Long.valueOf(numBytesWritten));
}
public boolean removeObserver(Observer<ResponseType> observer) {
return this.observers.remove(observer);
}
public boolean removeProgressObserver(ProgressObserver observer) {
return this.progressListeners.remove(observer);
}
@Override
protected Runnable doInBackground(Void... args) {
ResponseType response;
try {
response = this.request.execute();
} catch (LiveOperationException e) {
return new OnErrorRunnable(e);
}
return new OnCompleteRunnable(response);
}
@Override
protected void onPostExecute(Runnable result) {
super.onPostExecute(result);
result.run();
}
@Override
protected void onProgressUpdate(Long... values) {
for (ProgressObserver listener : this.progressListeners) {
listener.onProgress(values);
}
}
}

View File

@ -0,0 +1,507 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.apache.http.client.HttpClient;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.net.Uri;
import android.net.http.SslError;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
import android.webkit.SslErrorHandler;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
/**
* AuthorizationRequest performs an Authorization Request by launching a WebView Dialog that
* displays the login and consent page and then, on a successful login and consent, performs an
* async AccessToken request.
*/
class AuthorizationRequest implements ObservableOAuthRequest, OAuthRequestObserver {
/**
* OAuthDialog is a Dialog that contains a WebView. The WebView loads the passed in Uri, and
* loads the passed in WebViewClient that allows the WebView to be observed (i.e., when a page
* loads the WebViewClient will be notified).
*/
private class OAuthDialog extends Dialog implements OnCancelListener {
/**
* AuthorizationWebViewClient is a static (i.e., does not have access to the instance that
* created it) class that checks for when the end_uri is loaded in to the WebView and calls
* the AuthorizationRequest's onEndUri method.
*/
private class AuthorizationWebViewClient extends WebViewClient {
private final CookieManager cookieManager;
private final Set<String> cookieKeys;
public AuthorizationWebViewClient() {
// I believe I need to create a syncManager before I can use a cookie manager.
CookieSyncManager.createInstance(getContext());
this.cookieManager = CookieManager.getInstance();
this.cookieKeys = new HashSet<String>();
}
/**
* Call back used when a page is being started.
*
* This will check to see if the given URL is one of the end_uris/redirect_uris and
* based on the query parameters the method will either return an error, or proceed with
* an AccessTokenRequest.
*
* @param view {@link WebView} that this is attached to.
* @param url of the page being started
*/
@Override
public void onPageFinished(WebView view, String url) {
Uri uri = Uri.parse(url);
// only clear cookies that are on the logout domain.
if (uri.getHost().equals(Config.INSTANCE.getOAuthLogoutUri().getHost())) {
this.saveCookiesInMemory(this.cookieManager.getCookie(url));
}
Uri endUri = Config.INSTANCE.getOAuthDesktopUri();
boolean isEndUri = UriComparator.INSTANCE.compare(uri, endUri) == 0;
if (!isEndUri) {
return;
}
this.saveCookiesToPreferences();
AuthorizationRequest.this.onEndUri(uri);
OAuthDialog.this.dismiss();
}
/**
* Callback when the WebView received an Error.
*
* This method will notify the listener about the error and dismiss the WebView dialog.
*
* @param view the WebView that received the error
* @param errorCode the error code corresponding to a WebViewClient.ERROR_* value
* @param description the String containing the description of the error
* @param failingUrl the url that encountered an error
*/
@Override
public void onReceivedError(WebView view,
int errorCode,
String description,
String failingUrl) {
AuthorizationRequest.this.onError("", description, failingUrl);
OAuthDialog.this.dismiss();
}
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
// TODO: Android does not like the SSL certificate we use, because it has '*' in
// it. Proceed with the errors.
handler.proceed();
}
private void saveCookiesInMemory(String cookie) {
// Not all URLs will have cookies
if (TextUtils.isEmpty(cookie)) {
return;
}
String[] pairs = TextUtils.split(cookie, "; ");
for (String pair : pairs) {
int index = pair.indexOf(EQUALS);
String key = pair.substring(0, index);
this.cookieKeys.add(key);
}
}
private void saveCookiesToPreferences() {
SharedPreferences preferences =
getContext().getSharedPreferences(PreferencesConstants.FILE_NAME,
Context.MODE_PRIVATE);
// If the application tries to login twice, before calling logout, there could
// be a cookie that was sent on the first login, that was not sent in the second
// login. So, read the cookies in that was saved before, and perform a union
// with the new cookies.
String value = preferences.getString(PreferencesConstants.COOKIES_KEY, "");
String[] valueSplit = TextUtils.split(value, PreferencesConstants.COOKIE_DELIMITER);
this.cookieKeys.addAll(Arrays.asList(valueSplit));
Editor editor = preferences.edit();
value = TextUtils.join(PreferencesConstants.COOKIE_DELIMITER, this.cookieKeys);
editor.putString(PreferencesConstants.COOKIES_KEY, value);
editor.commit();
// we do not need to hold on to the cookieKeys in memory anymore.
// It could be garbage collected when this object does, but let's clear it now,
// since it will not be used again in the future.
this.cookieKeys.clear();
}
}
/** Uri to load */
private final Uri requestUri;
/**
* Constructs a new OAuthDialog.
*
* @param context to construct the Dialog in
* @param requestUri to load in the WebView
* @param webViewClient to be placed in the WebView
*/
public OAuthDialog(Uri requestUri) {
super(AuthorizationRequest.this.activity, android.R.style.Theme_Translucent_NoTitleBar);
this.setOwnerActivity(AuthorizationRequest.this.activity);
assert requestUri != null;
this.requestUri = requestUri;
}
/** Called when the user hits the back button on the dialog. */
@Override
public void onCancel(DialogInterface dialog) {
LiveAuthException exception = new LiveAuthException(ErrorMessages.SIGNIN_CANCEL);
AuthorizationRequest.this.onException(exception);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setOnCancelListener(this);
FrameLayout content = new FrameLayout(this.getContext());
LinearLayout webViewContainer = new LinearLayout(this.getContext());
WebView webView = new WebView(this.getContext());
webView.setWebViewClient(new AuthorizationWebViewClient());
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
webView.loadUrl(this.requestUri.toString());
webView.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT,
LayoutParams.FILL_PARENT));
webView.setVisibility(View.VISIBLE);
webViewContainer.addView(webView);
webViewContainer.setVisibility(View.VISIBLE);
content.addView(webViewContainer);
content.setVisibility(View.VISIBLE);
content.forceLayout();
webViewContainer.forceLayout();
this.addContentView(content,
new LayoutParams(LayoutParams.FILL_PARENT,
LayoutParams.FILL_PARENT));
}
}
/**
* Compares just the scheme, authority, and path. It does not compare the query parameters or
* the fragment.
*/
private enum UriComparator implements Comparator<Uri> {
INSTANCE;
@Override
public int compare(Uri lhs, Uri rhs) {
String[] lhsParts = { lhs.getScheme(), lhs.getAuthority(), lhs.getPath() };
String[] rhsParts = { rhs.getScheme(), rhs.getAuthority(), rhs.getPath() };
assert lhsParts.length == rhsParts.length;
for (int i = 0; i < lhsParts.length; i++) {
int compare = lhsParts[i].compareTo(rhsParts[i]);
if (compare != 0) {
return compare;
}
}
return 0;
}
}
private static final String AMPERSAND = "&";
private static final String EQUALS = "=";
/**
* Turns the fragment parameters of the uri into a map.
*
* @param uri to get fragment parameters from
* @return a map containing the fragment parameters
*/
private static Map<String, String> getFragmentParametersMap(Uri uri) {
String fragment = uri.getFragment();
String[] keyValuePairs = TextUtils.split(fragment, AMPERSAND);
Map<String, String> fragementParameters = new HashMap<String, String>();
for (String keyValuePair : keyValuePairs) {
int index = keyValuePair.indexOf(EQUALS);
String key = keyValuePair.substring(0, index);
String value = keyValuePair.substring(index + 1);
fragementParameters.put(key, value);
}
return fragementParameters;
}
private final Activity activity;
private final HttpClient client;
private final String clientId;
private final DefaultObservableOAuthRequest observable;
private final String redirectUri;
private final String scope;
public AuthorizationRequest(Activity activity,
HttpClient client,
String clientId,
String redirectUri,
String scope) {
assert activity != null;
assert client != null;
assert !TextUtils.isEmpty(clientId);
assert !TextUtils.isEmpty(redirectUri);
assert !TextUtils.isEmpty(scope);
this.activity = activity;
this.client = client;
this.clientId = clientId;
this.redirectUri = redirectUri;
this.observable = new DefaultObservableOAuthRequest();
this.scope = scope;
}
@Override
public void addObserver(OAuthRequestObserver observer) {
this.observable.addObserver(observer);
}
/**
* Launches the login/consent page inside of a Dialog that contains a WebView and then performs
* a AccessTokenRequest on successful login and consent. This method is async and will call the
* passed in listener when it is completed.
*/
public void execute() {
String displayType = this.getDisplayParameter();
String responseType = OAuth.ResponseType.CODE.toString().toLowerCase();
String locale = Locale.getDefault().toString();
Uri requestUri = Config.INSTANCE.getOAuthAuthorizeUri()
.buildUpon()
.appendQueryParameter(OAuth.CLIENT_ID, this.clientId)
.appendQueryParameter(OAuth.SCOPE, this.scope)
.appendQueryParameter(OAuth.DISPLAY, displayType)
.appendQueryParameter(OAuth.RESPONSE_TYPE, responseType)
.appendQueryParameter(OAuth.LOCALE, locale)
.appendQueryParameter(OAuth.REDIRECT_URI, this.redirectUri)
.build();
OAuthDialog oAuthDialog = new OAuthDialog(requestUri);
oAuthDialog.show();
}
@Override
public void onException(LiveAuthException exception) {
this.observable.notifyObservers(exception);
}
@Override
public void onResponse(OAuthResponse response) {
this.observable.notifyObservers(response);
}
@Override
public boolean removeObserver(OAuthRequestObserver observer) {
return this.observable.removeObserver(observer);
}
/**
* Gets the display parameter by looking at the screen size of the activity.
* @return "android_phone" for phones and "android_tablet" for tablets.
*/
private String getDisplayParameter() {
ScreenSize screenSize = ScreenSize.determineScreenSize(this.activity);
DeviceType deviceType = screenSize.getDeviceType();
return deviceType.getDisplayParameter().toString().toLowerCase();
}
/**
* Called when the response uri contains an access_token in the fragment.
*
* This method reads the response and calls back the LiveOAuthListener on the UI/main thread,
* and then dismisses the dialog window.
*
* See <a href="http://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-1.3.1">Section
* 1.3.1</a> of the OAuth 2.0 spec.
*
* @param fragmentParameters in the uri
*/
private void onAccessTokenResponse(Map<String, String> fragmentParameters) {
assert fragmentParameters != null;
OAuthSuccessfulResponse response;
try {
response = OAuthSuccessfulResponse.createFromFragment(fragmentParameters);
} catch (LiveAuthException e) {
this.onException(e);
return;
}
this.onResponse(response);
}
/**
* Called when the response uri contains an authorization code.
*
* This method launches an async AccessTokenRequest and dismisses the dialog window.
*
* See <a href="http://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-4.1.2">Section
* 4.1.2</a> of the OAuth 2.0 spec for more information.
*
* @param code is the authorization code from the uri
*/
private void onAuthorizationResponse(String code) {
assert !TextUtils.isEmpty(code);
// Since we DO have an authorization code, launch an AccessTokenRequest.
// We do this asynchronously to prevent the HTTP IO from occupying the
// UI/main thread (which we are on right now).
AccessTokenRequest request = new AccessTokenRequest(this.client,
this.clientId,
this.redirectUri,
code);
TokenRequestAsync requestAsync = new TokenRequestAsync(request);
// We want to know when this request finishes, because we need to notify our
// observers.
requestAsync.addObserver(this);
requestAsync.execute();
}
/**
* Called when the end uri is loaded.
*
* This method will read the uri's query parameters and fragment, and respond with the
* appropriate action.
*
* @param endUri that was loaded
*/
private void onEndUri(Uri endUri) {
// If we are on an end uri, the response could either be in
// the fragment or the query parameters. The response could
// either be successful or it could contain an error.
// Check all situations and call the listener's appropriate callback.
// Callback the listener on the UI/main thread. We could call it right away since
// we are on the UI/main thread, but it is probably better that we finish up with
// the WebView code before we callback on the listener.
boolean hasFragment = endUri.getFragment() != null;
boolean hasQueryParameters = endUri.getQuery() != null;
boolean invalidUri = !hasFragment && !hasQueryParameters;
// check for an invalid uri, and leave early
if (invalidUri) {
this.onInvalidUri();
return;
}
if (hasFragment) {
Map<String, String> fragmentParameters =
AuthorizationRequest.getFragmentParametersMap(endUri);
boolean isSuccessfulResponse =
fragmentParameters.containsKey(OAuth.ACCESS_TOKEN) &&
fragmentParameters.containsKey(OAuth.TOKEN_TYPE);
if (isSuccessfulResponse) {
this.onAccessTokenResponse(fragmentParameters);
return;
}
String error = fragmentParameters.get(OAuth.ERROR);
if (error != null) {
String errorDescription = fragmentParameters.get(OAuth.ERROR_DESCRIPTION);
String errorUri = fragmentParameters.get(OAuth.ERROR_URI);
this.onError(error, errorDescription, errorUri);
return;
}
}
if (hasQueryParameters) {
String code = endUri.getQueryParameter(OAuth.CODE);
if (code != null) {
this.onAuthorizationResponse(code);
return;
}
String error = endUri.getQueryParameter(OAuth.ERROR);
if (error != null) {
String errorDescription = endUri.getQueryParameter(OAuth.ERROR_DESCRIPTION);
String errorUri = endUri.getQueryParameter(OAuth.ERROR_URI);
this.onError(error, errorDescription, errorUri);
return;
}
}
// if the code reaches this point, the uri was invalid
// because it did not contain either a successful response
// or an error in either the queryParameter or the fragment
this.onInvalidUri();
}
/**
* Called when end uri had an error in either the fragment or the query parameter.
*
* This method constructs the proper exception, calls the listener's appropriate callback method
* on the main/UI thread, and then dismisses the dialog window.
*
* @param error containing an error code
* @param errorDescription optional text with additional information
* @param errorUri optional uri that is associated with the error.
*/
private void onError(String error, String errorDescription, String errorUri) {
LiveAuthException exception = new LiveAuthException(error,
errorDescription,
errorUri);
this.onException(exception);
}
/**
* Called when an invalid uri (i.e., a uri that does not contain an error or a successful
* response).
*
* This method constructs an exception, calls the listener's appropriate callback on the main/UI
* thread, and then dismisses the dialog window.
*/
private void onInvalidUri() {
LiveAuthException exception = new LiveAuthException(ErrorMessages.SERVER_ERROR);
this.onException(exception);
}
}

View File

@ -0,0 +1,89 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import android.net.Uri;
import android.text.TextUtils;
/**
* Config is a singleton class that contains the values used throughout the SDK.
*/
enum Config {
INSTANCE;
private Uri apiUri;
private String apiVersion;
private Uri oAuthAuthorizeUri;
private Uri oAuthDesktopUri;
private Uri oAuthLogoutUri;
private Uri oAuthTokenUri;
Config() {
// initialize default values for constants
apiUri = Uri.parse("https://apis.live.net/v5.0");
apiVersion = "5.0";
oAuthAuthorizeUri = Uri.parse("https://login.live.com/oauth20_authorize.srf");
oAuthDesktopUri = Uri.parse("https://login.live.com/oauth20_desktop.srf");
oAuthLogoutUri = Uri.parse("https://login.live.com/oauth20_logout.srf");
oAuthTokenUri = Uri.parse("https://login.live.com/oauth20_token.srf");
}
public Uri getApiUri() {
return apiUri;
}
public String getApiVersion() {
return apiVersion;
}
public Uri getOAuthAuthorizeUri() {
return oAuthAuthorizeUri;
}
public Uri getOAuthDesktopUri() {
return oAuthDesktopUri;
}
public Uri getOAuthLogoutUri() {
return oAuthLogoutUri;
}
public Uri getOAuthTokenUri() {
return oAuthTokenUri;
}
public void setApiUri(Uri apiUri) {
assert apiUri != null;
this.apiUri = apiUri;
}
public void setApiVersion(String apiVersion) {
assert !TextUtils.isEmpty(apiVersion);
this.apiVersion = apiVersion;
}
public void setOAuthAuthorizeUri(Uri oAuthAuthorizeUri) {
assert oAuthAuthorizeUri != null;
this.oAuthAuthorizeUri = oAuthAuthorizeUri;
}
public void setOAuthDesktopUri(Uri oAuthDesktopUri) {
assert oAuthDesktopUri != null;
this.oAuthDesktopUri = oAuthDesktopUri;
}
public void setOAuthLogoutUri(Uri oAuthLogoutUri) {
assert oAuthLogoutUri != null;
this.oAuthLogoutUri = oAuthLogoutUri;
}
public void setOAuthTokenUri(Uri oAuthTokenUri) {
assert oAuthTokenUri != null;
this.oAuthTokenUri = oAuthTokenUri;
}
}

View File

@ -0,0 +1,55 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import org.apache.http.HttpEntity;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpUriRequest;
import org.json.JSONObject;
/**
* CopyRequest is a subclass of a BodyEnclosingApiRequest and performs a Copy request.
*/
class CopyRequest extends EntityEnclosingApiRequest<JSONObject> {
public static final String METHOD = HttpCopy.METHOD_NAME;
/**
* Constructs a new CopyRequest and initializes its member variables.
*
* @param session with the access_token
* @param client to make Http requests on
* @param path of the request
* @param entity body of the request
*/
public CopyRequest(LiveConnectSession session,
HttpClient client,
String path,
HttpEntity entity) {
super(session, client, JsonResponseHandler.INSTANCE, path, entity);
}
/** @return the string "COPY" */
@Override
public String getMethod() {
return METHOD;
}
/**
* Factory method override that constructs a HttpCopy and adds a body to it.
*
* @return a HttpCopy with the properly body added to it.
*/
@Override
protected HttpUriRequest createHttpRequest() throws LiveOperationException {
final HttpCopy request = new HttpCopy(this.requestUri.toString());
request.setEntity(this.entity);
return request;
}
}

View File

@ -0,0 +1,55 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import java.util.ArrayList;
import java.util.List;
/**
* Default implementation of an ObserverableOAuthRequest.
* Other classes that need to be observed can compose themselves out of this class.
*/
class DefaultObservableOAuthRequest implements ObservableOAuthRequest {
private final List<OAuthRequestObserver> observers;
public DefaultObservableOAuthRequest() {
this.observers = new ArrayList<OAuthRequestObserver>();
}
@Override
public void addObserver(OAuthRequestObserver observer) {
this.observers.add(observer);
}
/**
* Calls all the Observerable's observer's onException.
*
* @param exception to give to the observers
*/
public void notifyObservers(LiveAuthException exception) {
for (final OAuthRequestObserver observer : this.observers) {
observer.onException(exception);
}
}
/**
* Calls all this Observable's observer's onResponse.
*
* @param response to give to the observers
*/
public void notifyObservers(OAuthResponse response) {
for (final OAuthRequestObserver observer : this.observers) {
observer.onResponse(response);
}
}
@Override
public boolean removeObserver(OAuthRequestObserver observer) {
return this.observers.remove(observer);
}
}

View File

@ -0,0 +1,47 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpUriRequest;
import org.json.JSONObject;
/**
* DeleteRequest is a subclass of an ApiRequest and performs a delete request.
*/
class DeleteRequest extends ApiRequest<JSONObject> {
public static final String METHOD = HttpDelete.METHOD_NAME;
/**
* Constructs a new DeleteRequest and initializes its member variables.
*
* @param session with the access_token
* @param client to perform Http requests on
* @param path of the request
*/
public DeleteRequest(LiveConnectSession session, HttpClient client, String path) {
super(session, client, JsonResponseHandler.INSTANCE, path);
}
/** @return the string "DELETE" */
@Override
public String getMethod() {
return METHOD;
}
/**
* Factory method override that constructs a HttpDelete request
*
* @return a HttpDelete request
*/
@Override
protected HttpUriRequest createHttpRequest() {
return new HttpDelete(this.requestUri.toString());
}
}

View File

@ -0,0 +1,31 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import com.microsoft.live.OAuth.DisplayType;
/**
* The type of the device is used to determine the display query parameter for login.live.com.
* Phones have a display parameter of android_phone.
* Tablets have a display parameter of android_tablet.
*/
enum DeviceType {
PHONE {
@Override
public DisplayType getDisplayParameter() {
return DisplayType.ANDROID_PHONE;
}
},
TABLET {
@Override
public DisplayType getDisplayParameter() {
return DisplayType.ANDROID_TABLET;
}
};
abstract public DisplayType getDisplayParameter();
}

View File

@ -0,0 +1,37 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import java.io.InputStream;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
class DownloadRequest extends ApiRequest<InputStream> {
public static final String METHOD = HttpGet.METHOD_NAME;
public DownloadRequest(LiveConnectSession session, HttpClient client, String path) {
super(session,
client,
InputStreamResponseHandler.INSTANCE,
path,
ResponseCodes.UNSUPPRESSED,
Redirects.UNSUPPRESSED);
}
@Override
public String getMethod() {
return METHOD;
}
@Override
protected HttpUriRequest createHttpRequest() throws LiveOperationException {
return new HttpGet(this.requestUri.toString());
}
}

View File

@ -0,0 +1,184 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import org.apache.http.HttpEntity;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.entity.HttpEntityWrapper;
/**
* EntityEnclosingApiRequest is an ApiRequest with a body.
* Upload progress can be monitored by adding an UploadProgressListener to this class.
*/
abstract class EntityEnclosingApiRequest<ResponseType> extends ApiRequest<ResponseType> {
/**
* UploadProgressListener is a listener that is called during upload progress.
*/
public interface UploadProgressListener {
/**
* @param totalBytes of the upload request
* @param numBytesWritten during the upload request
*/
public void onProgress(long totalBytes, long numBytesWritten);
}
/**
* Wraps the given entity, and intercepts writeTo calls to check the upload progress.
*/
private static class ProgressableEntity extends HttpEntityWrapper {
final List<UploadProgressListener> listeners;
ProgressableEntity(HttpEntity wrapped, List<UploadProgressListener> listeners) {
super(wrapped);
assert listeners != null;
this.listeners = listeners;
}
@Override
public void writeTo(OutputStream outstream) throws IOException {
this.wrappedEntity.writeTo(new ProgressableOutputStream(outstream,
this.getContentLength(),
this.listeners));
// If we don't consume the content, the content will be leaked (i.e., the InputStream
// in the HttpEntity is not closed).
// You'd think the library would call this.
this.wrappedEntity.consumeContent();
}
}
/**
* Wraps the given output stream and notifies the given listeners, when the
* stream is written to.
*/
private static class ProgressableOutputStream extends FilterOutputStream {
final List<UploadProgressListener> listeners;
long numBytesWritten;
long totalBytes;
public ProgressableOutputStream(OutputStream outstream,
long totalBytes,
List<UploadProgressListener> listeners) {
super(outstream);
assert totalBytes >= 0L;
assert listeners != null;
this.listeners = listeners;
this.numBytesWritten = 0L;
this.totalBytes = totalBytes;
}
@Override
public void write(byte[] buffer) throws IOException {
this.out.write(buffer);
this.numBytesWritten += buffer.length;
this.notifyListeners();
}
@Override
public void write(byte[] buffer, int offset, int count) throws IOException {
this.out.write(buffer, offset, count);
this.numBytesWritten += count;
this.notifyListeners();
}
@Override
public void write(int oneByte) throws IOException {
this.out.write(oneByte);
this.numBytesWritten += 1;
this.notifyListeners();
}
private void notifyListeners() {
assert this.numBytesWritten <= this.totalBytes;
for (final UploadProgressListener listener : this.listeners) {
listener.onProgress(this.totalBytes, this.numBytesWritten);
}
}
}
protected final HttpEntity entity;
private final List<UploadProgressListener> listeners;
public EntityEnclosingApiRequest(LiveConnectSession session,
HttpClient client,
ResponseHandler<ResponseType> responseHandler,
String path,
HttpEntity entity) {
this(session,
client,
responseHandler,
path,
entity,
ResponseCodes.SUPPRESS,
Redirects.SUPPRESS);
}
/**
* Constructs a new EntiyEnclosingApiRequest and initializes its member variables.
*
* @param session that contains the access token
* @param client to make Http Requests on
* @param path of the request
* @param entity of the request
*/
public EntityEnclosingApiRequest(LiveConnectSession session,
HttpClient client,
ResponseHandler<ResponseType> responseHandler,
String path,
HttpEntity entity,
ResponseCodes responseCodes,
Redirects redirects) {
super(session, client, responseHandler, path, responseCodes, redirects);
assert entity != null;
this.listeners = new ArrayList<UploadProgressListener>();
this.entity = new ProgressableEntity(entity, this.listeners);
}
/**
* Adds an UploadProgressListener to be called when there is upload progress.
*
* @param listener to add
* @return always true
*/
public boolean addListener(UploadProgressListener listener) {
assert listener != null;
return this.listeners.add(listener);
}
/**
* Removes an UploadProgressListener.
*
* @param listener to be removed
* @return true if the the listener was removed
*/
public boolean removeListener(UploadProgressListener listener) {
assert listener != null;
return this.listeners.remove(listener);
}
}

View File

@ -0,0 +1,36 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
/**
* ErrorMessages is a non-instantiable class that contains all the String constants
* used in for errors and exceptions.
*/
final class ErrorMessages {
public static final String ABSOLUTE_PARAMETER =
"Input parameter '%1$s' is invalid. '%1$s' cannot be absolute.";
public static final String CLIENT_ERROR =
"An error occured on the client during the operation.";
public static final String EMPTY_PARAMETER =
"Input parameter '%1$s' is invalid. '%1$s' cannot be empty.";
public static final String INVALID_URI =
"Input parameter '%1$s' is invalid. '%1$s' must be a valid URI.";
public static final String LOGGED_OUT = "The user has is logged out.";
public static final String LOGIN_IN_PROGRESS =
"Another login operation is already in progress.";
public static final String MISSING_UPLOAD_LOCATION =
"The provided path does not contain an upload_location.";
public static final String NON_INSTANTIABLE_CLASS = "Non-instantiable class";
public static final String NULL_PARAMETER =
"Input parameter '%1$s' is invalid. '%1$s' cannot be null.";
public static final String SERVER_ERROR =
"An error occured while communicating with the server during the operation. " +
"Please try again later.";
public static final String SIGNIN_CANCEL = "The user cancelled the login operation.";
private ErrorMessages() { throw new AssertionError(NON_INSTANTIABLE_CLASS); }
}

View File

@ -0,0 +1,47 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.json.JSONObject;
/**
* GetRequest is a subclass of an ApiRequest and performs a GET request.
*/
class GetRequest extends ApiRequest<JSONObject> {
public static final String METHOD = HttpGet.METHOD_NAME;
/**
* Constructs a new GetRequest and initializes its member variables.
*
* @param session with the access_token
* @param client to perform Http requests on
* @param path of the request
*/
public GetRequest(LiveConnectSession session, HttpClient client, String path) {
super(session, client, JsonResponseHandler.INSTANCE, path);
}
/** @return the string "GET" */
@Override
public String getMethod() {
return METHOD;
}
/**
* Factory method override that constructs a HttpGet request
*
* @return a HttpGet request
*/
@Override
protected HttpUriRequest createHttpRequest() {
return new HttpGet(this.requestUri.toString());
}
}

View File

@ -0,0 +1,42 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import java.net.URI;
import java.net.URISyntaxException;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
/**
* HttpCopy represents an HTTP COPY operation.
* HTTP COPY is not a standard HTTP method and this adds it
* to the HTTP library.
*/
class HttpCopy extends HttpEntityEnclosingRequestBase {
public static final String METHOD_NAME = "COPY";
/**
* Constructs a new HttpCopy with the given uri and initializes its member variables.
*
* @param uri of the request
*/
public HttpCopy(String uri) {
try {
this.setURI(new URI(uri));
} catch (URISyntaxException e) {
final String message = String.format(ErrorMessages.INVALID_URI, "uri");
throw new IllegalArgumentException(message);
}
}
/** @return the string "COPY" */
@Override
public String getMethod() {
return METHOD_NAME;
}
}

View File

@ -0,0 +1,42 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import java.net.URI;
import java.net.URISyntaxException;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
/**
* HttpMove represents an HTTP MOVE operation.
* HTTP MOVE is not a standard HTTP method and this adds it
* to the HTTP library.
*/
class HttpMove extends HttpEntityEnclosingRequestBase {
public static final String METHOD_NAME = "MOVE";
/**
* Constructs a new HttpMove with the given uri and initializes its member variables.
*
* @param uri of the request
*/
public HttpMove(String uri) {
try {
this.setURI(new URI(uri));
} catch (URISyntaxException e) {
final String message = String.format(ErrorMessages.INVALID_URI, "uri");
throw new IllegalArgumentException(message);
}
}
/** @return the string "MOVE" */
@Override
public String getMethod() {
return METHOD_NAME;
}
}

View File

@ -0,0 +1,42 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import java.io.IOException;
import java.io.InputStream;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.util.EntityUtils;
/**
* InputStreamResponseHandler returns an InputStream from an HttpResponse.
* Singleton--use INSTANCE.
*/
enum InputStreamResponseHandler implements ResponseHandler<InputStream> {
INSTANCE;
@Override
public InputStream handleResponse(HttpResponse response) throws ClientProtocolException,
IOException {
HttpEntity entity = response.getEntity();
StatusLine statusLine = response.getStatusLine();
boolean successfulResponse = (statusLine.getStatusCode() / 100) == 2;
if (!successfulResponse) {
// If it was not a successful response, the response body contains a
// JSON error message body. Unfortunately, I have to adhere to the interface
// and I am throwing an IOException in this case.
String responseBody = EntityUtils.toString(entity);
throw new IOException(responseBody);
}
return entity.getContent();
}
}

View File

@ -0,0 +1,33 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import java.io.UnsupportedEncodingException;
import org.apache.http.entity.StringEntity;
import org.apache.http.protocol.HTTP;
import org.json.JSONObject;
/**
* JsonEntity is an Entity that contains a Json body
*/
class JsonEntity extends StringEntity {
public static final String CONTENT_TYPE = "application/json;charset=" + HTTP.UTF_8;
/**
* Constructs a new JsonEntity.
*
* @param body
* @throws UnsupportedEncodingException
*/
JsonEntity(JSONObject body) throws UnsupportedEncodingException {
super(body.toString(), HTTP.UTF_8);
this.setContentType(CONTENT_TYPE);
}
}

View File

@ -0,0 +1,49 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import java.io.IOException;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.util.EntityUtils;
import org.json.JSONException;
import org.json.JSONObject;
import android.text.TextUtils;
/**
* JsonResponseHandler returns a JSONObject from an HttpResponse.
* Singleton--use INSTANCE.
*/
enum JsonResponseHandler implements ResponseHandler<JSONObject> {
INSTANCE;
@Override
public JSONObject handleResponse(HttpResponse response)
throws ClientProtocolException, IOException {
final HttpEntity entity = response.getEntity();
final String stringResponse;
if (entity != null) {
stringResponse = EntityUtils.toString(entity);
} else {
return null;
}
if (TextUtils.isEmpty(stringResponse)) {
return new JSONObject();
}
try {
return new JSONObject(stringResponse);
} catch (JSONException e) {
throw new IOException(e.getLocalizedMessage());
}
}
}

View File

@ -0,0 +1,640 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.net.Uri;
import android.os.AsyncTask;
import android.text.TextUtils;
import android.util.Log;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
import com.microsoft.live.OAuth.ErrorType;
/**
* {@code LiveAuthClient} is a class responsible for retrieving a {@link LiveConnectSession}, which
* can be given to a {@link LiveConnectClient} in order to make requests to the Live Connect API.
*/
public class LiveAuthClient {
private static class AuthCompleteRunnable extends AuthListenerCaller implements Runnable {
private final LiveStatus status;
private final LiveConnectSession session;
public AuthCompleteRunnable(LiveAuthListener listener,
Object userState,
LiveStatus status,
LiveConnectSession session) {
super(listener, userState);
this.status = status;
this.session = session;
}
@Override
public void run() {
listener.onAuthComplete(status, session, userState);
}
}
private static class AuthErrorRunnable extends AuthListenerCaller implements Runnable {
private final LiveAuthException exception;
public AuthErrorRunnable(LiveAuthListener listener,
Object userState,
LiveAuthException exception) {
super(listener, userState);
this.exception = exception;
}
@Override
public void run() {
listener.onAuthError(exception, userState);
}
}
private static abstract class AuthListenerCaller {
protected final LiveAuthListener listener;
protected final Object userState;
public AuthListenerCaller(LiveAuthListener listener, Object userState) {
this.listener = listener;
this.userState = userState;
}
}
/**
* This class observes an {@link OAuthRequest} and calls the appropriate Listener method.
* On a successful response, it will call the
* {@link LiveAuthListener#onAuthComplete(LiveStatus, LiveConnectSession, Object)}.
* On an exception or an unsuccessful response, it will call
* {@link LiveAuthListener#onAuthError(LiveAuthException, Object)}.
*/
private class ListenerCallerObserver extends AuthListenerCaller
implements OAuthRequestObserver,
OAuthResponseVisitor {
public ListenerCallerObserver(LiveAuthListener listener, Object userState) {
super(listener, userState);
}
@Override
public void onException(LiveAuthException exception) {
new AuthErrorRunnable(listener, userState, exception).run();
}
@Override
public void onResponse(OAuthResponse response) {
response.accept(this);
}
@Override
public void visit(OAuthErrorResponse response) {
String error = response.getError().toString().toLowerCase();
String errorDescription = response.getErrorDescription();
String errorUri = response.getErrorUri();
LiveAuthException exception = new LiveAuthException(error,
errorDescription,
errorUri);
new AuthErrorRunnable(listener, userState, exception).run();
}
@Override
public void visit(OAuthSuccessfulResponse response) {
session.loadFromOAuthResponse(response);
new AuthCompleteRunnable(listener, userState, LiveStatus.CONNECTED, session).run();
}
}
/** Observer that will, depending on the response, save or clear the refresh token. */
private class RefreshTokenWriter implements OAuthRequestObserver, OAuthResponseVisitor {
@Override
public void onException(LiveAuthException exception) { }
@Override
public void onResponse(OAuthResponse response) {
response.accept(this);
}
@Override
public void visit(OAuthErrorResponse response) {
if (response.getError() == ErrorType.INVALID_GRANT) {
LiveAuthClient.this.clearRefreshTokenFromPreferences();
}
}
@Override
public void visit(OAuthSuccessfulResponse response) {
String refreshToken = response.getRefreshToken();
if (!TextUtils.isEmpty(refreshToken)) {
this.saveRefreshTokenToPerferences(refreshToken);
}
}
private boolean saveRefreshTokenToPerferences(String refreshToken) {
assert !TextUtils.isEmpty(refreshToken);
Log.w("MYLIVE", "saveRefreshTokenToPerferences");
SharedPreferences settings =
applicationContext.getSharedPreferences(PreferencesConstants.FILE_NAME,
Context.MODE_PRIVATE);
Editor editor = settings.edit();
editor.putString(PreferencesConstants.REFRESH_TOKEN_KEY, refreshToken);
boolean res = editor.commit();
Log.w("MYLIVE", "saveRefreshTokenToPerferences done for token "+refreshToken+" res="+res);
return res;
}
}
/**
* An {@link OAuthResponseVisitor} that checks the {@link OAuthResponse} and if it is a
* successful response, it loads the response into the given session.
*/
private static class SessionRefresher implements OAuthResponseVisitor {
private final LiveConnectSession session;
private boolean visitedSuccessfulResponse;
public SessionRefresher(LiveConnectSession session) {
assert session != null;
this.session = session;
this.visitedSuccessfulResponse = false;
}
@Override
public void visit(OAuthErrorResponse response) {
this.visitedSuccessfulResponse = false;
}
@Override
public void visit(OAuthSuccessfulResponse response) {
this.session.loadFromOAuthResponse(response);
this.visitedSuccessfulResponse = true;
}
public boolean visitedSuccessfulResponse() {
return this.visitedSuccessfulResponse;
}
}
/**
* A LiveAuthListener that does nothing on each of the call backs.
* This is used so when a null listener is passed in, this can be used, instead of null,
* to avoid if (listener == null) checks.
*/
private static final LiveAuthListener NULL_LISTENER = new LiveAuthListener() {
@Override
public void onAuthComplete(LiveStatus status, LiveConnectSession session, Object sender) { }
@Override
public void onAuthError(LiveAuthException exception, Object sender) { }
};
private final Context applicationContext;
private final String clientId;
private boolean hasPendingLoginRequest;
/**
* Responsible for all network (i.e., HTTP) calls.
* Tests will want to change this to mock the network and HTTP responses.
* @see #setHttpClient(HttpClient)
*/
private HttpClient httpClient;
/** saved from initialize and used in the login call if login's scopes are null. */
private Set<String> scopesFromInitialize;
/** One-to-one relationship between LiveAuthClient and LiveConnectSession. */
private final LiveConnectSession session;
{
this.httpClient = new DefaultHttpClient();
this.hasPendingLoginRequest = false;
this.session = new LiveConnectSession(this);
}
/**
* Constructs a new {@code LiveAuthClient} instance and initializes its member variables.
*
* @param context Context of the Application used to save any refresh_token.
* @param clientId The client_id of the Live Connect Application to login to.
*/
public LiveAuthClient(Context context, String clientId) {
LiveConnectUtils.assertNotNull(context, "context");
LiveConnectUtils.assertNotNullOrEmpty(clientId, "clientId");
this.applicationContext = context.getApplicationContext();
this.clientId = clientId;
}
/** @return the client_id of the Live Connect application. */
public String getClientId() {
return this.clientId;
}
/**
* Initializes a new {@link LiveConnectSession} with the given scopes.
*
* The {@link LiveConnectSession} will be returned by calling
* {@link LiveAuthListener#onAuthComplete(LiveStatus, LiveConnectSession, Object)}.
* Otherwise, the {@link LiveAuthListener#onAuthError(LiveAuthException, Object)} will be
* called. These methods will be called on the main/UI thread.
*
* If the wl.offline_access scope is used, a refresh_token is stored in the given
* {@link Activity}'s {@link SharedPerfences}.
*
* @param scopes to initialize the {@link LiveConnectSession} with.
* See <a href="http://msdn.microsoft.com/en-us/library/hh243646.aspx">MSDN Live Connect
* Reference's Scopes and permissions</a> for a list of scopes and explanations.
* @param listener called on either completion or error during the initialize process.
*/
public void initialize(Iterable<String> scopes, LiveAuthListener listener) {
this.initialize(scopes, listener, null);
}
/**
* Initializes a new {@link LiveConnectSession} with the given scopes.
*
* The {@link LiveConnectSession} will be returned by calling
* {@link LiveAuthListener#onAuthComplete(LiveStatus, LiveConnectSession, Object)}.
* Otherwise, the {@link LiveAuthListener#onAuthError(LiveAuthException, Object)} will be
* called. These methods will be called on the main/UI thread.
*
* If the wl.offline_access scope is used, a refresh_token is stored in the given
* {@link Activity}'s {@link SharedPerfences}.
*
* @param scopes to initialize the {@link LiveConnectSession} with.
* See <a href="http://msdn.microsoft.com/en-us/library/hh243646.aspx">MSDN Live Connect
* Reference's Scopes and permissions</a> for a list of scopes and explanations.
* @param listener called on either completion or error during the initialize process
* @param userState arbitrary object that is used to determine the caller of the method.
*/
public void initialize(Iterable<String> scopes, LiveAuthListener listener, Object userState) {
TokenRequestAsync asyncRequest = getInitializeRequest(scopes, listener,
userState);
if (asyncRequest == null)
{
return;
}
asyncRequest.execute();
}
public void initializeSynchronous(Iterable<String> scopes, LiveAuthListener listener, Object userState) {
TokenRequestAsync asyncRequest = getInitializeRequest(scopes, listener,
userState);
if (asyncRequest == null)
{
return;
}
asyncRequest.executeSynchronous();
}
private TokenRequestAsync getInitializeRequest(Iterable<String> scopes,
LiveAuthListener listener, Object userState) {
if (listener == null) {
listener = NULL_LISTENER;
}
if (scopes == null) {
scopes = Arrays.asList(new String[0]);
}
// copy scopes for login
this.scopesFromInitialize = new HashSet<String>();
for (String scope : scopes) {
this.scopesFromInitialize.add(scope);
}
this.scopesFromInitialize = Collections.unmodifiableSet(this.scopesFromInitialize);
String refreshToken = this.getRefreshTokenFromPreferences();
if (refreshToken == null) {
listener.onAuthComplete(LiveStatus.UNKNOWN, null, userState);
return null;
}
RefreshAccessTokenRequest request =
new RefreshAccessTokenRequest(this.httpClient,
this.clientId,
refreshToken,
TextUtils.join(OAuth.SCOPE_DELIMITER, scopes));
TokenRequestAsync asyncRequest = new TokenRequestAsync(request);
asyncRequest.addObserver(new ListenerCallerObserver(listener, userState));
asyncRequest.addObserver(new RefreshTokenWriter());
return asyncRequest;
}
/**
* Initializes a new {@link LiveConnectSession} with the given scopes.
*
* The {@link LiveConnectSession} will be returned by calling
* {@link LiveAuthListener#onAuthComplete(LiveStatus, LiveConnectSession, Object)}.
* Otherwise, the {@link LiveAuthListener#onAuthError(LiveAuthException, Object)} will be
* called. These methods will be called on the main/UI thread.
*
* If the wl.offline_access scope is used, a refresh_token is stored in the given
* {@link Activity}'s {@link SharedPerfences}.
*
* This initialize will use the last successfully used scopes from either a login or initialize.
*
* @param listener called on either completion or error during the initialize process.
*/
public void initialize(LiveAuthListener listener) {
this.initialize(listener, null);
}
/**
* Initializes a new {@link LiveConnectSession} with the given scopes.
*
* The {@link LiveConnectSession} will be returned by calling
* {@link LiveAuthListener#onAuthComplete(LiveStatus, LiveConnectSession, Object)}.
* Otherwise, the {@link LiveAuthListener#onAuthError(LiveAuthException, Object)} will be
* called. These methods will be called on the main/UI thread.
*
* If the wl.offline_access scope is used, a refresh_token is stored in the given
* {@link Activity}'s {@link SharedPerfences}.
*
* This initialize will use the last successfully used scopes from either a login or initialize.
*
* @param listener called on either completion or error during the initialize process.
* @param userState arbitrary object that is used to determine the caller of the method.
*/
public void initialize(LiveAuthListener listener, Object userState) {
this.initialize(null, listener, userState);
}
/**
* Logs in an user with the given scopes.
*
* login displays a {@link Dialog} that will prompt the
* user for a username and password, and ask for consent to use the given scopes.
* A {@link LiveConnectSession} will be returned by calling
* {@link LiveAuthListener#onAuthComplete(LiveStatus, LiveConnectSession, Object)}.
* Otherwise, the {@link LiveAuthListener#onAuthError(LiveAuthException, Object)} will be
* called. These methods will be called on the main/UI thread.
*
* @param activity {@link Activity} instance to display the Login dialog on.
* @param scopes to initialize the {@link LiveConnectSession} with.
* See <a href="http://msdn.microsoft.com/en-us/library/hh243646.aspx">MSDN Live Connect
* Reference's Scopes and permissions</a> for a list of scopes and explanations.
* @param listener called on either completion or error during the login process.
* @throws IllegalStateException if there is a pending login request.
*/
public void login(Activity activity, Iterable<String> scopes, LiveAuthListener listener) {
this.login(activity, scopes, listener, null);
}
/**
* Logs in an user with the given scopes.
*
* login displays a {@link Dialog} that will prompt the
* user for a username and password, and ask for consent to use the given scopes.
* A {@link LiveConnectSession} will be returned by calling
* {@link LiveAuthListener#onAuthComplete(LiveStatus, LiveConnectSession, Object)}.
* Otherwise, the {@link LiveAuthListener#onAuthError(LiveAuthException, Object)} will be
* called. These methods will be called on the main/UI thread.
*
* @param activity {@link Activity} instance to display the Login dialog on
* @param scopes to initialize the {@link LiveConnectSession} with.
* See <a href="http://msdn.microsoft.com/en-us/library/hh243646.aspx">MSDN Live Connect
* Reference's Scopes and permissions</a> for a list of scopes and explanations.
* @param listener called on either completion or error during the login process.
* @param userState arbitrary object that is used to determine the caller of the method.
* @throws IllegalStateException if there is a pending login request.
*/
public void login(Activity activity,
Iterable<String> scopes,
LiveAuthListener listener,
Object userState) {
LiveConnectUtils.assertNotNull(activity, "activity");
if (listener == null) {
listener = NULL_LISTENER;
}
if (this.hasPendingLoginRequest) {
throw new IllegalStateException(ErrorMessages.LOGIN_IN_PROGRESS);
}
// if no scopes were passed in, use the scopes from initialize or if those are empty,
// create an empty list
if (scopes == null) {
if (this.scopesFromInitialize == null) {
scopes = Arrays.asList(new String[0]);
} else {
scopes = this.scopesFromInitialize;
}
}
// if the session is valid and contains all the scopes, do not display the login ui.
boolean showDialog = this.session.isExpired() ||
!this.session.contains(scopes);
if (!showDialog) {
listener.onAuthComplete(LiveStatus.CONNECTED, this.session, userState);
return;
}
String scope = TextUtils.join(OAuth.SCOPE_DELIMITER, scopes);
String redirectUri = Config.INSTANCE.getOAuthDesktopUri().toString();
AuthorizationRequest request = new AuthorizationRequest(activity,
this.httpClient,
this.clientId,
redirectUri,
scope);
request.addObserver(new ListenerCallerObserver(listener, userState));
request.addObserver(new RefreshTokenWriter());
request.addObserver(new OAuthRequestObserver() {
@Override
public void onException(LiveAuthException exception) {
LiveAuthClient.this.hasPendingLoginRequest = false;
}
@Override
public void onResponse(OAuthResponse response) {
LiveAuthClient.this.hasPendingLoginRequest = false;
}
});
this.hasPendingLoginRequest = true;
request.execute();
}
/**
* Logs out the given user.
*
* Also, this method clears the previously created {@link LiveConnectSession}.
* {@link LiveAuthListener#onAuthComplete(LiveStatus, LiveConnectSession, Object)} will be
* called on completion. Otherwise,
* {@link LiveAuthListener#onAuthError(LiveAuthException, Object)} will be called.
*
* @param listener called on either completion or error during the logout process.
*/
public void logout(LiveAuthListener listener) {
this.logout(listener, null);
}
/**
* Logs out the given user.
*
* Also, this method clears the previously created {@link LiveConnectSession}.
* {@link LiveAuthListener#onAuthComplete(LiveStatus, LiveConnectSession, Object)} will be
* called on completion. Otherwise,
* {@link LiveAuthListener#onAuthError(LiveAuthException, Object)} will be called.
*
* @param listener called on either completion or error during the logout process.
* @param userState arbitrary object that is used to determine the caller of the method.
*/
public void logout(LiveAuthListener listener, Object userState) {
if (listener == null) {
listener = NULL_LISTENER;
}
session.setAccessToken(null);
session.setAuthenticationToken(null);
session.setRefreshToken(null);
session.setScopes(null);
session.setTokenType(null);
clearRefreshTokenFromPreferences();
CookieSyncManager cookieSyncManager =
CookieSyncManager.createInstance(this.applicationContext);
CookieManager manager = CookieManager.getInstance();
Uri logoutUri = Config.INSTANCE.getOAuthLogoutUri();
String url = logoutUri.toString();
String domain = logoutUri.getHost();
List<String> cookieKeys = this.getCookieKeysFromPreferences();
for (String cookieKey : cookieKeys) {
String value = TextUtils.join("", new String[] {
cookieKey,
"=; expires=Thu, 30-Oct-1980 16:00:00 GMT;domain=",
domain,
";path=/;version=1"
});
manager.setCookie(url, value);
}
cookieSyncManager.sync();
listener.onAuthComplete(LiveStatus.UNKNOWN, null, userState);
}
/** @return The {@link HttpClient} instance used by this {@code LiveAuthClient}. */
HttpClient getHttpClient() {
return this.httpClient;
}
/** @return The {@link LiveConnectSession} instance that this {@code LiveAuthClient} created. */
LiveConnectSession getSession() {
return session;
}
/**
* Refreshes the previously created session.
*
* @return true if the session was successfully refreshed.
*/
boolean refresh() {
String scope = TextUtils.join(OAuth.SCOPE_DELIMITER, this.session.getScopes());
String refreshToken = this.session.getRefreshToken();
if (TextUtils.isEmpty(refreshToken)) {
return false;
}
RefreshAccessTokenRequest request =
new RefreshAccessTokenRequest(this.httpClient, this.clientId, refreshToken, scope);
OAuthResponse response;
try {
response = request.execute();
} catch (LiveAuthException e) {
return false;
}
SessionRefresher refresher = new SessionRefresher(this.session);
response.accept(refresher);
response.accept(new RefreshTokenWriter());
return refresher.visitedSuccessfulResponse();
}
/**
* Sets the {@link HttpClient} that is used for HTTP requests by this {@code LiveAuthClient}.
* Tests will want to change this to mock the network/HTTP responses.
* @param client The new HttpClient to be set.
*/
void setHttpClient(HttpClient client) {
assert client != null;
this.httpClient = client;
}
/**
* Clears the refresh token from this {@code LiveAuthClient}'s
* {@link Activity#getPreferences(int)}.
*
* @return true if the refresh token was successfully cleared.
*/
private boolean clearRefreshTokenFromPreferences() {
SharedPreferences settings = getSharedPreferences();
Editor editor = settings.edit();
editor.remove(PreferencesConstants.REFRESH_TOKEN_KEY);
return editor.commit();
}
private SharedPreferences getSharedPreferences() {
return applicationContext.getSharedPreferences(PreferencesConstants.FILE_NAME,
Context.MODE_PRIVATE);
}
private List<String> getCookieKeysFromPreferences() {
SharedPreferences settings = getSharedPreferences();
String cookieKeys = settings.getString(PreferencesConstants.COOKIES_KEY, "");
return Arrays.asList(TextUtils.split(cookieKeys, PreferencesConstants.COOKIE_DELIMITER));
}
/**
* Retrieves the refresh token from this {@code LiveAuthClient}'s
* {@link Activity#getPreferences(int)}.
*
* @return the refresh token from persistent storage.
*/
private String getRefreshTokenFromPreferences() {
SharedPreferences settings = getSharedPreferences();
return settings.getString(PreferencesConstants.REFRESH_TOKEN_KEY, null);
}
}

View File

@ -0,0 +1,63 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
/**
* Indicates that an exception occurred during the Auth process.
*/
public class LiveAuthException extends Exception {
private static final long serialVersionUID = 3368677530670470856L;
private final String error;
private final String errorUri;
LiveAuthException(String errorMessage) {
super(errorMessage);
this.error = "";
this.errorUri = "";
}
LiveAuthException(String errorMessage, Throwable throwable) {
super(errorMessage, throwable);
this.error = "";
this.errorUri = "";
}
LiveAuthException(String error, String errorDescription, String errorUri) {
super(errorDescription);
assert error != null;
this.error = error;
this.errorUri = errorUri;
}
LiveAuthException(String error, String errorDescription, String errorUri, Throwable cause) {
super(errorDescription, cause);
assert error != null;
this.error = error;
this.errorUri = errorUri;
}
/**
* @return Returns the authentication error.
*/
public String getError() {
return this.error;
}
/**
* @return Returns the error URI.
*/
public String getErrorUri() {
return this.errorUri;
}
}

View File

@ -0,0 +1,33 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
/**
* Handles callback methods for LiveAuthClient init, login, and logout methods.
* Returns the * status of the operation when onAuthComplete is called. If there was an error
* during the operation, onAuthError is called with the exception that was thrown.
*/
public interface LiveAuthListener {
/**
* Invoked when the operation completes successfully.
*
* @param status The {@link LiveStatus} for an operation. If successful, the status is
* CONNECTED. If unsuccessful, NOT_CONNECTED or UNKNOWN are returned.
* @param session The {@link LiveConnectSession} from the {@link LiveAuthClient}.
* @param userState An arbitrary object that is used to determine the caller of the method.
*/
public void onAuthComplete(LiveStatus status, LiveConnectSession session, Object userState);
/**
* Invoked when the method call fails.
*
* @param exception The {@link LiveAuthException} error.
* @param userState An arbitrary object that is used to determine the caller of the method.
*/
public void onAuthError(LiveAuthException exception, Object userState);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,311 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
/**
* Represents a Live Connect session.
*/
public class LiveConnectSession {
private String accessToken;
private String authenticationToken;
/** Keeps track of all the listeners, and fires the property change events */
private final PropertyChangeSupport changeSupport;
/**
* The LiveAuthClient that created this object.
* This is needed in order to perform a refresh request.
* There is a one-to-one relationship between the LiveConnectSession and LiveAuthClient.
*/
private final LiveAuthClient creator;
private Date expiresIn;
private String refreshToken;
private Set<String> scopes;
private String tokenType;
/**
* Constructors a new LiveConnectSession, and sets its creator to the passed in
* LiveAuthClient. All other member variables are left uninitialized.
*
* @param creator
*/
LiveConnectSession(LiveAuthClient creator) {
assert creator != null;
this.creator = creator;
this.changeSupport = new PropertyChangeSupport(this);
}
/**
* Adds a {@link PropertyChangeListener} to the session that receives notification when any
* property is changed.
*
* @param listener
*/
public void addPropertyChangeListener(PropertyChangeListener listener) {
if (listener == null) {
return;
}
this.changeSupport.addPropertyChangeListener(listener);
}
/**
* Adds a {@link PropertyChangeListener} to the session that receives notification when a
* specific property is changed.
*
* @param propertyName
* @param listener
*/
public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
if (listener == null) {
return;
}
this.changeSupport.addPropertyChangeListener(propertyName, listener);
}
/**
* @return The access token for the signed-in, connected user.
*/
public String getAccessToken() {
return this.accessToken;
}
/**
* @return A user-specific token that provides information to an app so that it can validate
* the user.
*/
public String getAuthenticationToken() {
return this.authenticationToken;
}
/**
* @return The exact time when a session expires.
*/
public Date getExpiresIn() {
// Defensive copy
return new Date(this.expiresIn.getTime());
}
/**
* @return An array of all PropertyChangeListeners for this session.
*/
public PropertyChangeListener[] getPropertyChangeListeners() {
return this.changeSupport.getPropertyChangeListeners();
}
/**
* @param propertyName
* @return An array of all PropertyChangeListeners for a specific property for this session.
*/
public PropertyChangeListener[] getPropertyChangeListeners(String propertyName) {
return this.changeSupport.getPropertyChangeListeners(propertyName);
}
/**
* @return A user-specific refresh token that the app can use to refresh the access token.
*/
public String getRefreshToken() {
return this.refreshToken;
}
/**
* @return The scopes that the user has consented to.
*/
public Iterable<String> getScopes() {
// Defensive copy is not necessary, because this.scopes is an unmodifiableSet
return this.scopes;
}
/**
* @return The type of token.
*/
public String getTokenType() {
return this.tokenType;
}
/**
* @return {@code true} if the session is expired.
*/
public boolean isExpired() {
if (this.expiresIn == null) {
return true;
}
final Date now = new Date();
return now.after(this.expiresIn);
}
/**
* Removes a PropertyChangeListeners on a session.
* @param listener
*/
public void removePropertyChangeListener(PropertyChangeListener listener) {
if (listener == null) {
return;
}
this.changeSupport.removePropertyChangeListener(listener);
}
/**
* Removes a PropertyChangeListener for a specific property on a session.
* @param propertyName
* @param listener
*/
public void removePropertyChangeListener(String propertyName,
PropertyChangeListener listener) {
if (listener == null) {
return;
}
this.changeSupport.removePropertyChangeListener(propertyName, listener);
}
@Override
public String toString() {
return String.format("LiveConnectSession [accessToken=%s, authenticationToken=%s, expiresIn=%s, refreshToken=%s, scopes=%s, tokenType=%s]",
this.accessToken,
this.authenticationToken,
this.expiresIn,
this.refreshToken,
this.scopes,
this.tokenType);
}
boolean contains(Iterable<String> scopes) {
if (scopes == null) {
return true;
} else if (this.scopes == null) {
return false;
}
for (String scope : scopes) {
if (!this.scopes.contains(scope)) {
return false;
}
}
return true;
}
/**
* Fills in the LiveConnectSession with the OAuthResponse.
* WARNING: The OAuthResponse must not contain OAuth.ERROR.
*
* @param response to load from
*/
void loadFromOAuthResponse(OAuthSuccessfulResponse response) {
this.accessToken = response.getAccessToken();
this.tokenType = response.getTokenType().toString().toLowerCase();
if (response.hasAuthenticationToken()) {
this.authenticationToken = response.getAuthenticationToken();
}
if (response.hasExpiresIn()) {
final Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND, response.getExpiresIn());
this.setExpiresIn(calendar.getTime());
}
if (response.hasRefreshToken()) {
this.refreshToken = response.getRefreshToken();
}
if (response.hasScope()) {
final String scopeString = response.getScope();
this.setScopes(Arrays.asList(scopeString.split(OAuth.SCOPE_DELIMITER)));
}
}
/**
* Refreshes this LiveConnectSession
*
* @return true if it was able to refresh the refresh token.
*/
boolean refresh() {
return this.creator.refresh();
}
void setAccessToken(String accessToken) {
final String oldValue = this.accessToken;
this.accessToken = accessToken;
this.changeSupport.firePropertyChange("accessToken", oldValue, this.accessToken);
}
void setAuthenticationToken(String authenticationToken) {
final String oldValue = this.authenticationToken;
this.authenticationToken = authenticationToken;
this.changeSupport.firePropertyChange("authenticationToken",
oldValue,
this.authenticationToken);
}
void setExpiresIn(Date expiresIn) {
final Date oldValue = this.expiresIn;
this.expiresIn = new Date(expiresIn.getTime());
this.changeSupport.firePropertyChange("expiresIn", oldValue, this.expiresIn);
}
void setRefreshToken(String refreshToken) {
final String oldValue = this.refreshToken;
this.refreshToken = refreshToken;
this.changeSupport.firePropertyChange("refreshToken", oldValue, this.refreshToken);
}
void setScopes(Iterable<String> scopes) {
final Iterable<String> oldValue = this.scopes;
// Defensive copy
this.scopes = new HashSet<String>();
if (scopes != null) {
for (String scope : scopes) {
this.scopes.add(scope);
}
}
this.scopes = Collections.unmodifiableSet(this.scopes);
this.changeSupport.firePropertyChange("scopes", oldValue, this.scopes);
}
void setTokenType(String tokenType) {
final String oldValue = this.tokenType;
this.tokenType = tokenType;
this.changeSupport.firePropertyChange("tokenType", oldValue, this.tokenType);
}
boolean willExpireInSecs(int secs) {
final Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND, secs);
final Date future = calendar.getTime();
// if add secs seconds to the current time and it is after the expired time
// then it is almost expired.
return future.after(this.expiresIn);
}
}

View File

@ -0,0 +1,59 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import android.text.TextUtils;
/**
* LiveConnectUtils is a non-instantiable utility class that contains various helper
* methods and constants.
*/
final class LiveConnectUtils {
/**
* Checks to see if the passed in Object is null, and throws a
* NullPointerException if it is.
*
* @param object to check
* @param parameterName name of the parameter that is used in the exception message
* @throws NullPointerException if the Object is null
*/
public static void assertNotNull(Object object, String parameterName) {
assert !TextUtils.isEmpty(parameterName);
if (object == null) {
final String message = String.format(ErrorMessages.NULL_PARAMETER, parameterName);
throw new NullPointerException(message);
}
}
/**
* Checks to see if the passed in is an empty string, and throws an
* IllegalArgumentException if it is.
*
* @param parameter to check
* @param parameterName name of the parameter that is used in the exception message
* @throws IllegalArgumentException if the parameter is empty
* @throws NullPointerException if the String is null
*/
public static void assertNotNullOrEmpty(String parameter, String parameterName) {
assert !TextUtils.isEmpty(parameterName);
assertNotNull(parameter, parameterName);
if (TextUtils.isEmpty(parameter)) {
final String message = String.format(ErrorMessages.EMPTY_PARAMETER, parameterName);
throw new IllegalArgumentException(message);
}
}
/**
* Private to prevent instantiation
*/
private LiveConnectUtils() { throw new AssertionError(ErrorMessages.NON_INSTANTIABLE_CLASS); }
}

View File

@ -0,0 +1,131 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import java.io.InputStream;
import android.text.TextUtils;
/**
* Represents data returned from a download call to the Live Connect Representational State
* Transfer (REST) API.
*/
public class LiveDownloadOperation {
static class Builder {
private ApiRequestAsync<InputStream> apiRequestAsync;
private final String method;
private final String path;
private InputStream stream;
private Object userState;
public Builder(String method, String path) {
assert !TextUtils.isEmpty(method);
assert !TextUtils.isEmpty(path);
this.method = method;
this.path = path;
}
/**
* Set if the operation to build is an async operation.
*
* @param apiRequestAsync
* @return this Builder
*/
public Builder apiRequestAsync(ApiRequestAsync<InputStream> apiRequestAsync) {
assert apiRequestAsync != null;
this.apiRequestAsync = apiRequestAsync;
return this;
}
public LiveDownloadOperation build() {
return new LiveDownloadOperation(this);
}
public Builder stream(InputStream stream) {
assert stream != null;
this.stream = stream;
return this;
}
public Builder userState(Object userState) {
this.userState = userState;
return this;
}
}
private final ApiRequestAsync<InputStream> apiRequestAsync;
private int contentLength;
private final String method;
private final String path;
private InputStream stream;
private final Object userState;
LiveDownloadOperation(Builder builder) {
this.apiRequestAsync = builder.apiRequestAsync;
this.method = builder.method;
this.path = builder.path;
this.stream = builder.stream;
this.userState = builder.userState;
}
public void cancel() {
final boolean isCancelable = this.apiRequestAsync != null;
if (isCancelable) {
this.apiRequestAsync.cancel(true);
}
}
/**
* @return The type of HTTP method used to make the call.
*/
public String getMethod() {
return this.method;
}
/**
* @return The length of the stream.
*/
public int getContentLength() {
return this.contentLength;
}
/**
* @return The path for the stream object.
*/
public String getPath() {
return this.path;
}
/**
* @return The stream object that contains the downloaded file.
*/
public InputStream getStream() {
return this.stream;
}
/**
* @return The user state.
*/
public Object getUserState() {
return this.userState;
}
void setContentLength(int contentLength) {
assert contentLength >= 0;
this.contentLength = contentLength;
}
void setStream(InputStream stream) {
assert stream != null;
this.stream = stream;
}
}

View File

@ -0,0 +1,38 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
/**
* Represents any functionality related to downloads that works with the Live Connect
* Representational State Transfer (REST) API.
*/
public interface LiveDownloadOperationListener {
/**
* Called when the associated download operation call completes.
* @param operation The {@link LiveDownloadOperation} object.
*/
public void onDownloadCompleted(LiveDownloadOperation operation);
/**
* Called when the associated download operation call fails.
* @param exception The error returned by the REST operation call.
* @param operation The {@link LiveDownloadOperation} object.
*/
public void onDownloadFailed(LiveOperationException exception,
LiveDownloadOperation operation);
/**
* Updates the progression of the download.
* @param totalBytes The total bytes downloaded.
* @param bytesRemaining The bytes remaining to download.
* @param operation The {@link LiveDownloadOperation} object.
*/
public void onDownloadProgress(int totalBytes,
int bytesRemaining,
LiveDownloadOperation operation);
}

View File

@ -0,0 +1,129 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import org.json.JSONObject;
import android.text.TextUtils;
/**
* Represents data returned from the Live Connect Representational State Transfer (REST) API
* services.
*/
public class LiveOperation {
static class Builder {
private ApiRequestAsync<JSONObject> apiRequestAsync;
private final String method;
private final String path;
private JSONObject result;
private Object userState;
public Builder(String method, String path) {
assert !TextUtils.isEmpty(method);
assert !TextUtils.isEmpty(path);
this.method = method;
this.path = path;
}
/**
* Set if the operation to build is an async operation.
*
* @param apiRequestAsync
* @return this Builder
*/
public Builder apiRequestAsync(ApiRequestAsync<JSONObject> apiRequestAsync) {
assert apiRequestAsync != null;
this.apiRequestAsync = apiRequestAsync;
return this;
}
public LiveOperation build() {
return new LiveOperation(this);
}
public Builder result(JSONObject result) {
assert result != null;
this.result = result;
return this;
}
public Builder userState(Object userState) {
this.userState = userState;
return this;
}
}
private final ApiRequestAsync<JSONObject> apiRequestAsync;
private final String method;
private final String path;
private JSONObject result;
private final Object userState;
private LiveOperation(Builder builder) {
this.apiRequestAsync = builder.apiRequestAsync;
this.method = builder.method;
this.path = builder.path;
this.result = builder.result;
this.userState = builder.userState;
}
/** Cancels the pending request. */
public void cancel() {
final boolean isCancelable = this.apiRequestAsync != null;
if (isCancelable) {
this.apiRequestAsync.cancel(true);
}
}
/**
* @return The type of HTTP method used to make the call.
*/
public String getMethod() {
return this.method;
}
/**
* @return The path to which the call was made.
*/
public String getPath() {
return this.path;
}
/**
* @return The raw result of the operation in the requested format.
*/
public String getRawResult() {
JSONObject result = this.getResult();
if (result == null) {
return null;
}
return result.toString();
}
/**
* @return The JSON object that is the result of the requesting operation.
*/
public JSONObject getResult() {
return this.result;
}
/**
* @return The user state that was passed in.
*/
public Object getUserState() {
return this.userState;
}
void setResult(JSONObject result) {
assert result != null;
this.result = result;
}
}

View File

@ -0,0 +1,24 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
/**
* Represents errors that occur when making requests to the Representational State Transfer
* (REST) API.
*/
public class LiveOperationException extends Exception {
private static final long serialVersionUID = 4630383031651156731L;
LiveOperationException(String message) {
super(message);
}
LiveOperationException(String message, Throwable e) {
super(message, e);
}
}

View File

@ -0,0 +1,27 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
/**
* Called when an operation finishes or has an error.
*/
public interface LiveOperationListener {
/**
* Called when the associated Representational State Transfer (REST) API operation call
* completes.
* @param operation The {@link LiveOperation} object.
*/
public void onComplete(LiveOperation operation);
/**
* Called when the associated Representational State Transfer (REST) operation call fails.
* @param exception The error returned by the REST operation call.
* @param operation The {@link LiveOperation} object.
*/
public void onError(LiveOperationException exception, LiveOperation operation);
}

View File

@ -0,0 +1,21 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
/**
* Specifies the status of an auth operation.
*/
public enum LiveStatus {
/** The status is not known. */
UNKNOWN,
/** The session is connected. */
CONNECTED,
/** The user has not consented to the application. */
NOT_CONNECTED;
}

View File

@ -0,0 +1,35 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
/**
* Represents any functionality related to uploads that works with the Live Connect
* Representational State Transfer (REST) API.
*/
public interface LiveUploadOperationListener {
/**
* Called when the associated upload operation call completes.
* @param operation The {@link LiveOperation} object.
*/
public void onUploadCompleted(LiveOperation operation);
/**
* Called when the associated upload operation call fails.
* @param exception The error returned by the REST operation call.
* @param operation The {@link LiveOperation} object.
*/
public void onUploadFailed(LiveOperationException exception, LiveOperation operation);
/**
* Called arbitrarily during the progress of the upload request.
* @param totalBytes The total bytes downloaded.
* @param bytesRemaining The bytes remaining to download.
* @param operation The {@link LiveOperation} object.
*/
public void onUploadProgress(int totalBytes, int bytesRemaining, LiveOperation operation);
}

View File

@ -0,0 +1,55 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import org.apache.http.HttpEntity;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpUriRequest;
import org.json.JSONObject;
/**
* MoveRequest is a subclass of a BodyEnclosingApiRequest and performs a Move request.
*/
class MoveRequest extends EntityEnclosingApiRequest<JSONObject> {
public static final String METHOD = HttpMove.METHOD_NAME;
/**
* Constructs a new MoveRequest and initializes its member variables.
*
* @param session with the access_token
* @param client to make Http requests on
* @param path of the request
* @param entity body of the request
*/
public MoveRequest(LiveConnectSession session,
HttpClient client,
String path,
HttpEntity entity) {
super(session, client, JsonResponseHandler.INSTANCE, path, entity);
}
/** @return the string "MOVE" */
@Override
public String getMethod() {
return METHOD;
}
/**
* Factory method override that constructs a HttpMove and adds a body to it.
*
* @return a HttpMove with the properly body added to it.
*/
@Override
protected HttpUriRequest createHttpRequest() throws LiveOperationException {
final HttpMove request = new HttpMove(this.requestUri.toString());
request.setEntity(this.entity);
return request;
}
}

View File

@ -0,0 +1,214 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
/**
* OAuth is a non-instantiable utility class that contains types and constants
* for the OAuth protocol.
*
* See the <a href="http://tools.ietf.org/html/draft-ietf-oauth-v2-22">OAuth 2.0 spec</a>
* for more information.
*/
final class OAuth {
public enum DisplayType {
ANDROID_PHONE,
ANDROID_TABLET
}
public enum ErrorType {
/**
* Client authentication failed (e.g. unknown client, no
* client authentication included, or unsupported
* authentication method). The authorization server MAY
* return an HTTP 401 (Unauthorized) status code to indicate
* which HTTP authentication schemes are supported. If the
* client attempted to authenticate via the "Authorization"
* request header field, the authorization server MUST
* respond with an HTTP 401 (Unauthorized) status code, and
* include the "WWW-Authenticate" response header field
* matching the authentication scheme used by the client.
*/
INVALID_CLIENT,
/**
* The provided authorization grant (e.g. authorization
* code, resource owner credentials, client credentials) is
* invalid, expired, revoked, does not match the redirection
* URI used in the authorization request, or was issued to
* another client.
*/
INVALID_GRANT,
/**
* The request is missing a required parameter, includes an
* unsupported parameter value, repeats a parameter,
* includes multiple credentials, utilizes more than one
* mechanism for authenticating the client, or is otherwise
* malformed.
*/
INVALID_REQUEST,
/**
* The requested scope is invalid, unknown, malformed, or
* exceeds the scope granted by the resource owner.
*/
INVALID_SCOPE,
/**
* The authenticated client is not authorized to use this
* authorization grant type.
*/
UNAUTHORIZED_CLIENT,
/**
* The authorization grant type is not supported by the
* authorization server.
*/
UNSUPPORTED_GRANT_TYPE;
}
public enum GrantType {
AUTHORIZATION_CODE,
CLIENT_CREDENTIALS,
PASSWORD,
REFRESH_TOKEN;
}
public enum ResponseType {
CODE,
TOKEN;
}
public enum TokenType {
BEARER
}
/**
* Key for the access_token parameter.
*
* See <a href="http://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-5.1">Section 5.1</a>
* of the OAuth 2.0 spec for more information.
*/
public static final String ACCESS_TOKEN = "access_token";
/** The app's authentication token. */
public static final String AUTHENTICATION_TOKEN = "authentication_token";
/** The app's client ID. */
public static final String CLIENT_ID = "client_id";
/** Equivalent to the profile that is described in the OAuth 2.0 protocol spec. */
public static final String CODE = "code";
/**
* The display type to be used for the authorization page. Valid values are
* "popup", "touch", "page", or "none".
*/
public static final String DISPLAY = "display";
/**
* Key for the error parameter.
*
* error can have the following values:
* invalid_request, unauthorized_client, access_denied, unsupported_response_type,
* invalid_scope, server_error, or temporarily_unavailable.
*/
public static final String ERROR = "error";
/**
* Key for the error_description parameter. error_description is described below.
*
* OPTIONAL. A human-readable UTF-8 encoded text providing
* additional information, used to assist the client developer in
* understanding the error that occurred.
*/
public static final String ERROR_DESCRIPTION = "error_description";
/**
* Key for the error_uri parameter. error_uri is described below.
*
* OPTIONAL. A URI identifying a human-readable web page with
* information about the error, used to provide the client
* developer with additional information about the error.
*/
public static final String ERROR_URI = "error_uri";
/**
* Key for the expires_in parameter. expires_in is described below.
*
* OPTIONAL. The lifetime in seconds of the access token. For
* example, the value "3600" denotes that the access token will
* expire in one hour from the time the response was generated.
*/
public static final String EXPIRES_IN = "expires_in";
/**
* Key for the grant_type parameter. grant_type is described below.
*
* grant_type is used in a token request. It can take on the following
* values: authorization_code, password, client_credentials, or refresh_token.
*/
public static final String GRANT_TYPE = "grant_type";
/**
* Optional. A market string that determines how the consent user interface
* (UI) is localized. If the value of this parameter is missing or is not
* valid, a market value is determined by using an internal algorithm.
*/
public static final String LOCALE = "locale";
/**
* Key for the redirect_uri parameter.
*
* See <a href="http://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-3.1.2">Section 3.1.2</a>
* of the OAuth 2.0 spec for more information.
*/
public static final String REDIRECT_URI = "redirect_uri";
/**
* Key used for the refresh_token parameter.
*
* See <a href="http://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-5.1">Section 5.1</a>
* of the OAuth 2.0 spec for more information.
*/
public static final String REFRESH_TOKEN = "refresh_token";
/**
* The type of data to be returned in the response from the authorization
* server. Valid values are "code" or "token".
*/
public static final String RESPONSE_TYPE = "response_type";
/**
* Equivalent to the scope parameter that is described in the OAuth 2.0
* protocol spec.
*/
public static final String SCOPE = "scope";
/** Delimiter for the scopes field response. */
public static final String SCOPE_DELIMITER = " ";
/**
* Equivalent to the state parameter that is described in the OAuth 2.0
* protocol spec.
*/
public static final String STATE = "state";
public static final String THEME = "theme";
/**
* Key used for the token_type parameter.
*
* See <a href="http://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-5.1">Section 5.1</a>
* of the OAuth 2.0 spec for more information.
*/
public static final String TOKEN_TYPE = "token_type";
/** Private to prevent instantiation */
private OAuth() { throw new AssertionError(ErrorMessages.NON_INSTANTIABLE_CLASS); }
}

View File

@ -0,0 +1,173 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import org.json.JSONException;
import org.json.JSONObject;
import com.microsoft.live.OAuth.ErrorType;
/**
* OAuthErrorResponse represents the an Error Response from the OAuth server.
*/
class OAuthErrorResponse implements OAuthResponse {
/**
* Builder is a helper class to create a OAuthErrorResponse.
* An OAuthResponse must contain an error, but an error_description and
* error_uri are optional
*/
public static class Builder {
private final ErrorType error;
private String errorDescription;
private String errorUri;
public Builder(ErrorType error) {
assert error != null;
this.error = error;
}
/**
* @return a new instance of an OAuthErrorResponse containing
* the values called on the builder.
*/
public OAuthErrorResponse build() {
return new OAuthErrorResponse(this);
}
public Builder errorDescription(String errorDescription) {
this.errorDescription = errorDescription;
return this;
}
public Builder errorUri(String errorUri) {
this.errorUri = errorUri;
return this;
}
}
/**
* Static constructor that creates an OAuthErrorResponse from the given OAuth server's
* JSONObject response
* @param response from the OAuth server
* @return A new instance of an OAuthErrorResponse from the given response
* @throws LiveAuthException if there is an JSONException, or the error type cannot be found.
*/
public static OAuthErrorResponse createFromJson(JSONObject response) throws LiveAuthException {
final String errorString;
try {
errorString = response.getString(OAuth.ERROR);
} catch (JSONException e) {
throw new LiveAuthException(ErrorMessages.SERVER_ERROR, e);
}
final ErrorType error;
try {
error = ErrorType.valueOf(errorString.toUpperCase());
} catch (IllegalArgumentException e) {
throw new LiveAuthException(ErrorMessages.SERVER_ERROR, e);
} catch (NullPointerException e) {
throw new LiveAuthException(ErrorMessages.SERVER_ERROR, e);
}
final Builder builder = new Builder(error);
if (response.has(OAuth.ERROR_DESCRIPTION)) {
final String errorDescription;
try {
errorDescription = response.getString(OAuth.ERROR_DESCRIPTION);
} catch (JSONException e) {
throw new LiveAuthException(ErrorMessages.CLIENT_ERROR, e);
}
builder.errorDescription(errorDescription);
}
if (response.has(OAuth.ERROR_URI)) {
final String errorUri;
try {
errorUri = response.getString(OAuth.ERROR_URI);
} catch (JSONException e) {
throw new LiveAuthException(ErrorMessages.CLIENT_ERROR, e);
}
builder.errorUri(errorUri);
}
return builder.build();
}
/**
* @param response to check
* @return true if the given JSONObject is a valid OAuth response
*/
public static boolean validOAuthErrorResponse(JSONObject response) {
return response.has(OAuth.ERROR);
}
/** REQUIRED. */
private final ErrorType error;
/**
* OPTIONAL. A human-readable UTF-8 encoded text providing
* additional information, used to assist the client developer in
* understanding the error that occurred.
*/
private final String errorDescription;
/**
* OPTIONAL. A URI identifying a human-readable web page with
* information about the error, used to provide the client
* developer with additional information about the error.
*/
private final String errorUri;
/**
* OAuthErrorResponse constructor. It is private to enforce
* the use of the Builder.
*
* @param builder to use to construct the object.
*/
private OAuthErrorResponse(Builder builder) {
this.error = builder.error;
this.errorDescription = builder.errorDescription;
this.errorUri = builder.errorUri;
}
@Override
public void accept(OAuthResponseVisitor visitor) {
visitor.visit(this);
}
/**
* error is a required field.
* @return the error
*/
public ErrorType getError() {
return error;
}
/**
* error_description is an optional field
* @return error_description
*/
public String getErrorDescription() {
return errorDescription;
}
/**
* error_uri is an optional field
* @return error_uri
*/
public String getErrorUri() {
return errorUri;
}
@Override
public String toString() {
return String.format("OAuthErrorResponse [error=%s, errorDescription=%s, errorUri=%s]",
error.toString().toLowerCase(), errorDescription, errorUri);
}
}

View File

@ -0,0 +1,26 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
/**
* An observer of an OAuth Request. It will be notified of an Exception or of a Response.
*/
interface OAuthRequestObserver {
/**
* Callback used on an exception.
*
* @param exception
*/
public void onException(LiveAuthException exception);
/**
* Callback used on a response.
*
* @param response
*/
public void onResponse(OAuthResponse response);
}

View File

@ -0,0 +1,25 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
/**
* OAuthRespresent a response from an OAuth server.
* Known implementors are OAuthSuccessfulResponse and OAuthErrorResponse.
* Different OAuthResponses can be determined by using the OAuthResponseVisitor.
*/
interface OAuthResponse {
/**
* Calls visit() on the visitor.
* This method is used to determine which OAuthResponse is being returned
* without using instance of.
*
* @param visitor to visit the given OAuthResponse
*/
public void accept(OAuthResponseVisitor visitor);
}

View File

@ -0,0 +1,27 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
/**
* OAuthResponseVisitor is used to visit various OAuthResponse.
*/
interface OAuthResponseVisitor {
/**
* Called when an OAuthSuccessfulResponse is visited.
*
* @param response being visited
*/
public void visit(OAuthSuccessfulResponse response);
/**
* Called when an OAuthErrorResponse is being visited.
*
* @param response being visited
*/
public void visit(OAuthErrorResponse response);
}

View File

@ -0,0 +1,302 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import java.util.Map;
import org.json.JSONException;
import org.json.JSONObject;
import android.text.TextUtils;
import com.microsoft.live.OAuth.TokenType;
/**
* OAuthSuccessfulResponse represents a successful response form an OAuth server.
*/
class OAuthSuccessfulResponse implements OAuthResponse {
/**
* Builder is a utility class that is used to build a new OAuthSuccessfulResponse.
* It must be constructed with the required fields, and can add on the optional ones.
*/
public static class Builder {
private final String accessToken;
private String authenticationToken;
private int expiresIn = UNINITIALIZED;
private String refreshToken;
private String scope;
private final TokenType tokenType;
public Builder(String accessToken, TokenType tokenType) {
assert accessToken != null;
assert !TextUtils.isEmpty(accessToken);
assert tokenType != null;
this.accessToken = accessToken;
this.tokenType = tokenType;
}
public Builder authenticationToken(String authenticationToken) {
this.authenticationToken = authenticationToken;
return this;
}
/**
* @return a new instance of an OAuthSuccessfulResponse with the given
* parameters passed into the builder.
*/
public OAuthSuccessfulResponse build() {
return new OAuthSuccessfulResponse(this);
}
public Builder expiresIn(int expiresIn) {
this.expiresIn = expiresIn;
return this;
}
public Builder refreshToken(String refreshToken) {
this.refreshToken = refreshToken;
return this;
}
public Builder scope(String scope) {
this.scope = scope;
return this;
}
}
/** Used to declare expiresIn uninitialized */
private static final int UNINITIALIZED = -1;
public static OAuthSuccessfulResponse createFromFragment(
Map<String, String> fragmentParameters) throws LiveAuthException {
String accessToken = fragmentParameters.get(OAuth.ACCESS_TOKEN);
String tokenTypeString = fragmentParameters.get(OAuth.TOKEN_TYPE);
// must have accessToken and tokenTypeString to be a valid OAuthSuccessfulResponse
assert accessToken != null;
assert tokenTypeString != null;
TokenType tokenType;
try {
tokenType = TokenType.valueOf(tokenTypeString.toUpperCase());
} catch (IllegalArgumentException e) {
throw new LiveAuthException(ErrorMessages.SERVER_ERROR, e);
}
OAuthSuccessfulResponse.Builder builder =
new OAuthSuccessfulResponse.Builder(accessToken, tokenType);
String authenticationToken = fragmentParameters.get(OAuth.AUTHENTICATION_TOKEN);
if (authenticationToken != null) {
builder.authenticationToken(authenticationToken);
}
String expiresInString = fragmentParameters.get(OAuth.EXPIRES_IN);
if (expiresInString != null) {
final int expiresIn;
try {
expiresIn = Integer.parseInt(expiresInString);
} catch (final NumberFormatException e) {
throw new LiveAuthException(ErrorMessages.SERVER_ERROR, e);
}
builder.expiresIn(expiresIn);
}
String scope = fragmentParameters.get(OAuth.SCOPE);
if (scope != null) {
builder.scope(scope);
}
return builder.build();
}
/**
* Static constructor used to create a new OAuthSuccessfulResponse from an
* OAuth server's JSON response.
*
* @param response from an OAuth server that is used to create the object.
* @return a new instance of OAuthSuccessfulResponse that is created from the given JSONObject
* @throws LiveAuthException if there is a JSONException or the token_type is unknown.
*/
public static OAuthSuccessfulResponse createFromJson(JSONObject response)
throws LiveAuthException {
assert validOAuthSuccessfulResponse(response);
final String accessToken;
try {
accessToken = response.getString(OAuth.ACCESS_TOKEN);
} catch (final JSONException e) {
throw new LiveAuthException(ErrorMessages.SERVER_ERROR, e);
}
final String tokenTypeString;
try {
tokenTypeString = response.getString(OAuth.TOKEN_TYPE);
} catch (final JSONException e) {
throw new LiveAuthException(ErrorMessages.SERVER_ERROR, e);
}
final TokenType tokenType;
try {
tokenType = TokenType.valueOf(tokenTypeString.toUpperCase());
} catch (final IllegalArgumentException e) {
throw new LiveAuthException(ErrorMessages.SERVER_ERROR, e);
} catch (final NullPointerException e) {
throw new LiveAuthException(ErrorMessages.SERVER_ERROR, e);
}
final Builder builder = new Builder(accessToken, tokenType);
if (response.has(OAuth.AUTHENTICATION_TOKEN)) {
final String authenticationToken;
try {
authenticationToken = response.getString(OAuth.AUTHENTICATION_TOKEN);
} catch (final JSONException e) {
throw new LiveAuthException(ErrorMessages.CLIENT_ERROR, e);
}
builder.authenticationToken(authenticationToken);
}
if (response.has(OAuth.REFRESH_TOKEN)) {
final String refreshToken;
try {
refreshToken = response.getString(OAuth.REFRESH_TOKEN);
} catch (final JSONException e) {
throw new LiveAuthException(ErrorMessages.CLIENT_ERROR, e);
}
builder.refreshToken(refreshToken);
}
if (response.has(OAuth.EXPIRES_IN)) {
final int expiresIn;
try {
expiresIn = response.getInt(OAuth.EXPIRES_IN);
} catch (final JSONException e) {
throw new LiveAuthException(ErrorMessages.CLIENT_ERROR, e);
}
builder.expiresIn(expiresIn);
}
if (response.has(OAuth.SCOPE)) {
final String scope;
try {
scope = response.getString(OAuth.SCOPE);
} catch (final JSONException e) {
throw new LiveAuthException(ErrorMessages.CLIENT_ERROR, e);
}
builder.scope(scope);
}
return builder.build();
}
/**
* @param response
* @return true if the given JSONObject has the required fields to construct an
* OAuthSuccessfulResponse (i.e., has access_token and token_type)
*/
public static boolean validOAuthSuccessfulResponse(JSONObject response) {
return response.has(OAuth.ACCESS_TOKEN) &&
response.has(OAuth.TOKEN_TYPE);
}
/** REQUIRED. The access token issued by the authorization server. */
private final String accessToken;
private final String authenticationToken;
/**
* OPTIONAL. The lifetime in seconds of the access token. For
* example, the value "3600" denotes that the access token will
* expire in one hour from the time the response was generated.
*/
private final int expiresIn;
/**
* OPTIONAL. The refresh token which can be used to obtain new
* access tokens using the same authorization grant.
*/
private final String refreshToken;
/** OPTIONAL. */
private final String scope;
/** REQUIRED. */
private final TokenType tokenType;
/**
* Private constructor to enforce the user of the builder.
* @param builder to use to construct the object from.
*/
private OAuthSuccessfulResponse(Builder builder) {
this.accessToken = builder.accessToken;
this.authenticationToken = builder.authenticationToken;
this.tokenType = builder.tokenType;
this.refreshToken = builder.refreshToken;
this.expiresIn = builder.expiresIn;
this.scope = builder.scope;
}
@Override
public void accept(OAuthResponseVisitor visitor) {
visitor.visit(this);
}
public String getAccessToken() {
return this.accessToken;
}
public String getAuthenticationToken() {
return this.authenticationToken;
}
public int getExpiresIn() {
return this.expiresIn;
}
public String getRefreshToken() {
return this.refreshToken;
}
public String getScope() {
return this.scope;
}
public TokenType getTokenType() {
return this.tokenType;
}
public boolean hasAuthenticationToken() {
return this.authenticationToken != null && !TextUtils.isEmpty(this.authenticationToken);
}
public boolean hasExpiresIn() {
return this.expiresIn != UNINITIALIZED;
}
public boolean hasRefreshToken() {
return this.refreshToken != null && !TextUtils.isEmpty(this.refreshToken);
}
public boolean hasScope() {
return this.scope != null && !TextUtils.isEmpty(this.scope);
}
@Override
public String toString() {
return String.format("OAuthSuccessfulResponse [accessToken=%s, authenticationToken=%s, tokenType=%s, refreshToken=%s, expiresIn=%s, scope=%s]",
this.accessToken,
this.authenticationToken,
this.tokenType,
this.refreshToken,
this.expiresIn,
this.scope);
}
}

View File

@ -0,0 +1,28 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
/**
* An OAuth Request that can be observed, by adding observers that will be notified on any
* exception or response.
*/
interface ObservableOAuthRequest {
/**
* Adds an observer to observe the OAuth request
*
* @param observer to add
*/
public void addObserver(OAuthRequestObserver observer);
/**
* Removes an observer that is observing the OAuth request
*
* @param observer to remove
* @return true if the observer was removed.
*/
public boolean removeObserver(OAuthRequestObserver observer);
}

View File

@ -0,0 +1,47 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
/**
* Enum that specifies what to do during a naming conflict during an upload.
*/
public enum OverwriteOption {
/** Overwrite the existing file. */
Overwrite {
@Override
protected String overwriteQueryParamValue() {
return "true";
}
},
/** Do Not Overwrite the existing file and cancel the upload. */
DoNotOverwrite {
@Override
protected String overwriteQueryParamValue() {
return "false";
}
},
/** Rename the current file to avoid a name conflict. */
Rename {
@Override
protected String overwriteQueryParamValue() {
return "choosenewname";
}
};
/**
* Leaves any existing overwrite query parameter on appends this overwrite
* to the given UriBuilder.
*/
void appendQueryParameterOnTo(UriBuilder uri) {
uri.appendQueryParameter(QueryParameters.OVERWRITE, this.overwriteQueryParamValue());
}
abstract protected String overwriteQueryParamValue();
}

View File

@ -0,0 +1,56 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import org.apache.http.HttpEntity;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.json.JSONObject;
/**
* PostRequest is a subclass of a BodyEnclosingApiRequest and performs a Post request.
*/
class PostRequest extends EntityEnclosingApiRequest<JSONObject> {
public static final String METHOD = HttpPost.METHOD_NAME;
/**
* Constructs a new PostRequest and initializes its member variables.
*
* @param session with the access_token
* @param client to make Http requests on
* @param path of the request
* @param entity body of the request
*/
public PostRequest(LiveConnectSession session,
HttpClient client,
String path,
HttpEntity entity) {
super(session, client, JsonResponseHandler.INSTANCE, path, entity);
}
/** @return the string "POST" */
@Override
public String getMethod() {
return METHOD;
}
/**
* Factory method override that constructs a HttpPost and adds a body to it.
*
* @return a HttpPost with the properly body added to it.
*/
@Override
protected HttpUriRequest createHttpRequest() throws LiveOperationException {
final HttpPost request = new HttpPost(this.requestUri.toString());
request.setEntity(this.entity);
return request;
}
}

View File

@ -0,0 +1,22 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
/**
* Static class that holds constants used by an application's preferences.
*/
final class PreferencesConstants {
public static final String COOKIES_KEY = "cookies";
/** Name of the preference file */
public static final String FILE_NAME = "com.microsoft.live";
public static final String REFRESH_TOKEN_KEY = "refresh_token";
public static final String COOKIE_DELIMITER = ",";
private PreferencesConstants() { throw new AssertionError(); }
}

View File

@ -0,0 +1,56 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import org.apache.http.HttpEntity;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.json.JSONObject;
/**
* PutRequest is a subclass of a BodyEnclosingApiRequest and performs a Put request.
*/
class PutRequest extends EntityEnclosingApiRequest<JSONObject> {
public static final String METHOD = HttpPut.METHOD_NAME;
/**
* Constructs a new PutRequest and initializes its member variables.
*
* @param session with the access_token
* @param client to make Http requests on
* @param path of the request
* @param entity body of the request
*/
public PutRequest(LiveConnectSession session,
HttpClient client,
String path,
HttpEntity entity) {
super(session, client, JsonResponseHandler.INSTANCE, path, entity);
}
/** @return the string "PUT" */
@Override
public String getMethod() {
return METHOD;
}
/**
* Factory method override that constructs a HttpPut and adds a body to it.
*
* @return a HttpPut with the properly body added to it.
*/
@Override
protected HttpUriRequest createHttpRequest() throws LiveOperationException {
final HttpPut request = new HttpPut(this.requestUri.toString());
request.setEntity(this.entity);
return request;
}
}

View File

@ -0,0 +1,27 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
/**
* QueryParameters is a non-instantiable utility class that holds query parameter constants
* used by the API service.
*/
final class QueryParameters {
public static final String PRETTY = "pretty";
public static final String CALLBACK = "callback";
public static final String SUPPRESS_REDIRECTS = "suppress_redirects";
public static final String SUPPRESS_RESPONSE_CODES = "suppress_response_codes";
public static final String METHOD = "method";
public static final String OVERWRITE = "overwrite";
public static final String RETURN_SSL_RESOURCES = "return_ssl_resources";
/** Private to present instantiation. */
private QueryParameters() {
throw new AssertionError(ErrorMessages.NON_INSTANTIABLE_CLASS);
}
}

View File

@ -0,0 +1,55 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import java.util.List;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.message.BasicNameValuePair;
import android.text.TextUtils;
import com.microsoft.live.OAuth.GrantType;
/**
* RefreshAccessTokenRequest performs a refresh access token request. Most of the work
* is done by the parent class, TokenRequest. This class adds in the required body parameters via
* TokenRequest's hook method, constructBody().
*/
class RefreshAccessTokenRequest extends TokenRequest {
/** REQUIRED. Value MUST be set to "refresh_token". */
private final GrantType grantType = GrantType.REFRESH_TOKEN;
/** REQUIRED. The refresh token issued to the client. */
private final String refreshToken;
private final String scope;
public RefreshAccessTokenRequest(HttpClient client,
String clientId,
String refreshToken,
String scope) {
super(client, clientId);
assert refreshToken != null;
assert !TextUtils.isEmpty(refreshToken);
assert scope != null;
assert !TextUtils.isEmpty(scope);
this.refreshToken = refreshToken;
this.scope = scope;
}
@Override
protected void constructBody(List<NameValuePair> body) {
body.add(new BasicNameValuePair(OAuth.REFRESH_TOKEN, this.refreshToken));
body.add(new BasicNameValuePair(OAuth.SCOPE, this.scope));
body.add(new BasicNameValuePair(OAuth.GRANT_TYPE, this.grantType.toString()));
}
}

View File

@ -0,0 +1,73 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import android.app.Activity;
import android.content.res.Configuration;
import android.util.Log;
/**
* The ScreenSize is used to determine the DeviceType.
* Small and Normal ScreenSizes are Phones.
* Large and XLarge are Tablets.
*/
enum ScreenSize {
SMALL {
@Override
public DeviceType getDeviceType() {
return DeviceType.PHONE;
}
},
NORMAL {
@Override
public DeviceType getDeviceType() {
return DeviceType.PHONE;
}
},
LARGE {
@Override
public DeviceType getDeviceType() {
return DeviceType.TABLET;
}
},
XLARGE {
@Override
public DeviceType getDeviceType() {
return DeviceType.TABLET;
}
};
public abstract DeviceType getDeviceType();
/**
* Configuration.SCREENLAYOUT_SIZE_XLARGE was not provided in API level 9.
* However, its value of 4 does show up.
*/
private static final int SCREENLAYOUT_SIZE_XLARGE = 4;
public static ScreenSize determineScreenSize(Activity activity) {
int screenLayout = activity.getResources().getConfiguration().screenLayout;
int screenLayoutMasked = screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK;
switch (screenLayoutMasked) {
case Configuration.SCREENLAYOUT_SIZE_SMALL:
return SMALL;
case Configuration.SCREENLAYOUT_SIZE_NORMAL:
return NORMAL;
case Configuration.SCREENLAYOUT_SIZE_LARGE:
return LARGE;
case SCREENLAYOUT_SIZE_XLARGE:
return XLARGE;
default:
// If we cannot determine the ScreenSize, we'll guess and say it's normal.
Log.d(
"Live SDK ScreenSize",
"Unable to determine ScreenSize. A Normal ScreenSize will be returned.");
return NORMAL;
}
}
}

View File

@ -0,0 +1,125 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;
import org.json.JSONException;
import org.json.JSONObject;
import android.net.Uri;
import android.text.TextUtils;
/**
* Abstract class that represents an OAuth token request.
* Known subclasses include AccessTokenRequest and RefreshAccessTokenRequest
*/
abstract class TokenRequest {
private static final String CONTENT_TYPE =
URLEncodedUtils.CONTENT_TYPE + ";charset=" + HTTP.UTF_8;
protected final HttpClient client;
protected final String clientId;
/**
* Constructs a new TokenRequest instance and initializes its parameters.
*
* @param client the HttpClient to make HTTP requests on
* @param clientId the client_id of the calling application
*/
public TokenRequest(HttpClient client, String clientId) {
assert client != null;
assert clientId != null;
assert !TextUtils.isEmpty(clientId);
this.client = client;
this.clientId = clientId;
}
/**
* Performs the Token Request and returns the OAuth server's response.
*
* @return The OAuthResponse from the server
* @throws LiveAuthException if there is any exception while executing the request
* (e.g., IOException, JSONException)
*/
public OAuthResponse execute() throws LiveAuthException {
final Uri requestUri = Config.INSTANCE.getOAuthTokenUri();
final HttpPost request = new HttpPost(requestUri.toString());
final List<NameValuePair> body = new ArrayList<NameValuePair>();
body.add(new BasicNameValuePair(OAuth.CLIENT_ID, this.clientId));
// constructBody allows subclasses to add to body
this.constructBody(body);
try {
final UrlEncodedFormEntity entity = new UrlEncodedFormEntity(body, HTTP.UTF_8);
entity.setContentType(CONTENT_TYPE);
request.setEntity(entity);
} catch (UnsupportedEncodingException e) {
throw new LiveAuthException(ErrorMessages.CLIENT_ERROR, e);
}
final HttpResponse response;
try {
response = this.client.execute(request);
} catch (ClientProtocolException e) {
throw new LiveAuthException(ErrorMessages.SERVER_ERROR, e);
} catch (IOException e) {
throw new LiveAuthException(ErrorMessages.SERVER_ERROR, e);
}
final HttpEntity entity = response.getEntity();
final String stringResponse;
try {
stringResponse = EntityUtils.toString(entity);
} catch (IOException e) {
throw new LiveAuthException(ErrorMessages.SERVER_ERROR, e);
}
final JSONObject jsonResponse;
try {
jsonResponse = new JSONObject(stringResponse);
} catch (JSONException e) {
throw new LiveAuthException(ErrorMessages.SERVER_ERROR, e);
}
if (OAuthErrorResponse.validOAuthErrorResponse(jsonResponse)) {
return OAuthErrorResponse.createFromJson(jsonResponse);
} else if (OAuthSuccessfulResponse.validOAuthSuccessfulResponse(jsonResponse)) {
return OAuthSuccessfulResponse.createFromJson(jsonResponse);
} else {
throw new LiveAuthException(ErrorMessages.SERVER_ERROR);
}
}
/**
* This method gives a hook in the execute process, and allows subclasses
* to add to the HttpRequest's body.
* NOTE: The content type has already been added
*
* @param body of NameValuePairs to add to
*/
protected abstract void constructBody(List<NameValuePair> body);
}

View File

@ -0,0 +1,79 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import android.os.AsyncTask;
/**
* TokenRequestAsync performs an async token request. It takes in a TokenRequest,
* executes it, checks the OAuthResponse, and then calls the given listener.
*/
class TokenRequestAsync extends AsyncTask<Void, Void, Void> implements ObservableOAuthRequest {
private final DefaultObservableOAuthRequest observerable;
/** Not null if there was an exception */
private LiveAuthException exception;
/** Not null if there was a response */
private OAuthResponse response;
private final TokenRequest request;
/**
* Constructs a new TokenRequestAsync and initializes its member variables
*
* @param request to perform
*/
public TokenRequestAsync(TokenRequest request) {
assert request != null;
this.observerable = new DefaultObservableOAuthRequest();
this.request = request;
}
@Override
public void addObserver(OAuthRequestObserver observer) {
this.observerable.addObserver(observer);
}
@Override
public boolean removeObserver(OAuthRequestObserver observer) {
return this.observerable.removeObserver(observer);
}
@Override
protected Void doInBackground(Void... params) {
try {
this.response = this.request.execute();
} catch (LiveAuthException e) {
this.exception = e;
}
return null;
}
@Override
protected void onPostExecute(Void result) {
super.onPostExecute(result);
if (this.response != null) {
this.observerable.notifyObservers(this.response);
} else if (this.exception != null) {
this.observerable.notifyObservers(this.exception);
} else {
final LiveAuthException exception = new LiveAuthException(ErrorMessages.CLIENT_ERROR);
this.observerable.notifyObservers(exception);
}
}
public void executeSynchronous() {
Void result = doInBackground();
onPostExecute(result);
}
}

View File

@ -0,0 +1,137 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import org.apache.http.HttpEntity;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.json.JSONException;
import org.json.JSONObject;
import android.net.Uri;
import android.text.TextUtils;
class UploadRequest extends EntityEnclosingApiRequest<JSONObject> {
public static final String METHOD = HttpPut.METHOD_NAME;
private static final String FILE_PATH = "file.";
private static final String ERROR_KEY = "error";
private static final String UPLOAD_LOCATION_KEY = "upload_location";
private HttpUriRequest currentRequest;
private final String filename;
/**
* true if the given path refers to a File Object
* (i.e., the path begins with "/file").
*/
private final boolean isFileUpload;
private final OverwriteOption overwrite;
public UploadRequest(LiveConnectSession session,
HttpClient client,
String path,
HttpEntity entity,
String filename,
OverwriteOption overwrite) {
super(session,
client,
JsonResponseHandler.INSTANCE,
path,
entity,
ResponseCodes.SUPPRESS,
Redirects.UNSUPPRESSED);
assert !TextUtils.isEmpty(filename);
this.filename = filename;
this.overwrite = overwrite;
String lowerCasePath = this.pathUri.getPath().toLowerCase();
this.isFileUpload = lowerCasePath.indexOf(FILE_PATH) != -1;
}
@Override
public String getMethod() {
return METHOD;
}
@Override
public JSONObject execute() throws LiveOperationException {
UriBuilder uploadRequestUri;
// if the path was relative, we have to retrieve the upload location, because if we don't,
// we will proxy the upload request, which is a waste of resources.
if (this.pathUri.isRelative()) {
JSONObject response = this.getUploadLocation();
// We could of tried to get the upload location on an invalid path.
// If we did, just return that response.
// If the user passes in a path that does contain an upload location, then
// we need to throw an error.
if (response.has(ERROR_KEY)) {
return response;
} else if (!response.has(UPLOAD_LOCATION_KEY)) {
throw new LiveOperationException(ErrorMessages.MISSING_UPLOAD_LOCATION);
}
// once we have the file object, get the upload location
String uploadLocation;
try {
uploadLocation = response.getString(UPLOAD_LOCATION_KEY);
} catch (JSONException e) {
throw new LiveOperationException(ErrorMessages.SERVER_ERROR, e);
}
uploadRequestUri = UriBuilder.newInstance(Uri.parse(uploadLocation));
// The original path might have query parameters that were sent to the
// the upload location request, and those same query parameters will need
// to be sent to the HttpPut upload request too. Also, the returned upload_location
// *could* have query parameters on it. We want to keep those intact and in front of the
// the client's query parameters.
uploadRequestUri.appendQueryString(this.pathUri.getQuery());
} else {
uploadRequestUri = this.requestUri;
}
if (!this.isFileUpload) {
// if it is not a file upload it is a folder upload and we must
// add the file name to the upload location
// and don't forget to set the overwrite query parameter
uploadRequestUri.appendToPath(this.filename);
this.overwrite.appendQueryParameterOnTo(uploadRequestUri);
}
HttpPut uploadRequest = new HttpPut(uploadRequestUri.toString());
uploadRequest.setEntity(this.entity);
this.currentRequest = uploadRequest;
return super.execute();
}
@Override
protected HttpUriRequest createHttpRequest() throws LiveOperationException {
return this.currentRequest;
}
/**
* Performs an HttpGet on the folder/file object to retrieve the upload_location
*
* @return
* @throws LiveOperationException if there was an error getting the getUploadLocation
*/
private JSONObject getUploadLocation() throws LiveOperationException {
this.currentRequest = new HttpGet(this.requestUri.toString());
return super.execute();
}
}

View File

@ -0,0 +1,277 @@
//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import java.util.Iterator;
import java.util.LinkedList;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
/**
* Class for building URIs. The most useful benefit of this class is its query parameter
* management. It stores all the query parameters in a LinkedList, so parameters can
* be looked up, removed, and added easily.
*/
class UriBuilder {
public static class QueryParameter {
private final String key;
private final String value;
/**
* Constructs a query parameter with no value (e.g., download).
*
* @param key
*/
public QueryParameter(String key) {
assert key != null;
this.key = key;
this.value = null;
}
public QueryParameter(String key, String value) {
assert key != null;
assert value != null;
this.key = key;
this.value = value;
}
public String getKey() {
return this.key;
}
public String getValue() {
return this.value;
}
public boolean hasValue() {
return this.value != null;
}
@Override
public String toString() {
if (this.hasValue()) {
return this.key + "=" + this.value;
}
return this.key;
}
}
private static final String EQUAL = "=";
private static final String AMPERSAND = "&";
private static final char FORWARD_SLASH = '/';
private String scheme;
private String host;
private StringBuilder path;
private final LinkedList<QueryParameter> queryParameters;
/**
* Constructs a new UriBuilder from the given Uri.
*
* @return a new Uri Builder based off the given Uri.
*/
public static UriBuilder newInstance(Uri uri) {
return new UriBuilder().scheme(uri.getScheme())
.host(uri.getHost())
.path(uri.getPath())
.query(uri.getQuery());
}
public UriBuilder() {
this.queryParameters = new LinkedList<QueryParameter>();
}
/**
* Appends a new query parameter to the UriBuilder's query string.
*
* (e.g., appendQueryParameter("k1", "v1") when UriBuilder's query string is
* k2=v2&k3=v3 results in k2=v2&k3=v3&k1=v1).
*
* @param key Key of the new query parameter.
* @param value Value of the new query parameter.
* @return this UriBuilder object. Useful for chaining.
*/
public UriBuilder appendQueryParameter(String key, String value) {
assert key != null;
assert value != null;
this.queryParameters.add(new QueryParameter(key, value));
return this;
}
/**
* Appends the given query string on to the existing UriBuilder's query parameters.
*
* (e.g., UriBuilder's queryString k1=v1&k2=v2 and given queryString k3=v3&k4=v4, results in
* k1=v1&k2=v2&k3=v3&k4=v4).
*
* @param queryString Key-Value pairs separated by & and = (e.g., k1=v1&k2=v2&k3=k3).
* @return this UriBuilder object. Useful for chaining.
*/
public UriBuilder appendQueryString(String queryString) {
if (queryString == null) {
return this;
}
String[] pairs = TextUtils.split(queryString, UriBuilder.AMPERSAND);
for(String pair : pairs) {
String[] splitPair = TextUtils.split(pair, UriBuilder.EQUAL);
if (splitPair.length == 2) {
String key = splitPair[0];
String value = splitPair[1];
this.queryParameters.add(new QueryParameter(key, value));
} else if (splitPair.length == 1){
String key = splitPair[0];
this.queryParameters.add(new QueryParameter(key));
} else {
Log.w("com.microsoft.live.UriBuilder", "Invalid query parameter: " + pair);
}
}
return this;
}
/**
* Appends the given path to the UriBuilder's current path.
*
* @param path The path to append onto this UriBuilder's path.
* @return this UriBuilder object. Useful for chaining.
*/
public UriBuilder appendToPath(String path) {
assert path != null;
if (this.path == null) {
this.path = new StringBuilder(path);
} else {
boolean endsWithSlash = TextUtils.isEmpty(this.path) ? false :
this.path.charAt(this.path.length() - 1) == UriBuilder.FORWARD_SLASH;
boolean pathIsEmpty = TextUtils.isEmpty(path);
boolean beginsWithSlash =
pathIsEmpty ? false : path.charAt(0) == UriBuilder.FORWARD_SLASH;
if (endsWithSlash && beginsWithSlash) {
if (path.length() > 1) {
this.path.append(path.substring(1));
}
} else if (!endsWithSlash && !beginsWithSlash) {
if (!pathIsEmpty) {
this.path.append(UriBuilder.FORWARD_SLASH).append(path);
}
} else {
this.path.append(path);
}
}
return this;
}
/**
* Builds the Uri by converting into a android.net.Uri object.
*
* @return a new android.net.Uri defined by what was given to the builder.
*/
public Uri build() {
return new Uri.Builder().scheme(this.scheme)
.authority(this.host)
.path(this.path == null ? "" : this.path.toString())
.encodedQuery(TextUtils.join("&", this.queryParameters))
.build();
}
/**
* Sets the host part of the Uri.
*
* @return this UriBuilder object. Useful for chaining.
*/
public UriBuilder host(String host) {
assert host != null;
this.host = host;
return this;
}
/**
* Sets the path and removes any previously existing path.
*
* @param path The path to set on this UriBuilder.
* @return this UriBuilder object. Useful for chaining.
*/
public UriBuilder path(String path) {
assert path != null;
this.path = new StringBuilder(path);
return this;
}
/**
* Takes a query string and puts it in the Uri Builder's query string removing
* any existing query parameters.
*
* @param queryString Key-Value pairs separated by & and = (e.g., k1=v1&k2=v2&k3=k3).
* @return this UriBuilder object. Useful for chaining.
*/
public UriBuilder query(String queryString) {
this.queryParameters.clear();
return this.appendQueryString(queryString);
}
/**
* Removes all query parameters from the UriBuilder that has the given key.
*
* (e.g., removeQueryParametersWithKey("k1") when UriBuilder's query string of k1=v1&k2=v2&k1=v3
* results in k2=v2).
*
* @param key Query parameter's key to remove
* @return this UriBuilder object. Useful for chaining.
*/
public UriBuilder removeQueryParametersWithKey(String key) {
// There could be multiple query parameters with this key and
// we want to remove all of them.
Iterator<QueryParameter> it = this.queryParameters.iterator();
while (it.hasNext()) {
QueryParameter qp = it.next();
if (qp.getKey().equals(key)) {
it.remove();
}
}
return this;
}
/**
* Sets the scheme part of the Uri.
*
* @return this UriBuilder object. Useful for chaining.
*/
public UriBuilder scheme(String scheme) {
assert scheme != null;
this.scheme = scheme;
return this;
}
/**
* Returns the URI in string format (e.g., http://foo.com/bar?k1=v2).
*/
@Override
public String toString() {
return this.build().toString();
}
}

View File

@ -11,6 +11,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import com.google.android.gms.auth.UserRecoverableAuthException;
import com.google.api.client.extensions.android.http.AndroidHttp;
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential;
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException;
@ -723,20 +724,28 @@ public class GoogleDriveFileStorage extends JavaFileStorageBase {
public void prepareFileUsage(Context appContext, String path) throws UserInteractionRequiredException, Throwable
{
String accountName;
GDrivePath gdrivePath = null;
if (path.startsWith(getProtocolPrefix()))
try
{
gdrivePath = new GDrivePath();
//don't verify yet, we're not yet initialized:
gdrivePath.setPathWithoutVerify(path);
String accountName;
GDrivePath gdrivePath = null;
if (path.startsWith(getProtocolPrefix()))
{
gdrivePath = new GDrivePath();
//don't verify yet, we're not yet initialized:
gdrivePath.setPathWithoutVerify(path);
accountName = gdrivePath.getAccount();
}
else
accountName = path;
accountName = gdrivePath.getAccount();
initializeAccount(appContext, accountName);
}
catch (UserRecoverableAuthIOException e)
{
throw new UserInteractionRequiredException(e);
}
else
accountName = path;
initializeAccount(appContext, accountName);
}

View File

@ -467,13 +467,18 @@ public class SkyDriveFileStorage extends JavaFileStorageBase {
@Override
public void prepareFileUsage(Context appContext, String path) throws Exception
{
PrepareFileUsageListener listener = new PrepareFileUsageListener();
mAuthClient.initialize(Arrays.asList(SCOPES), listener);
mAuthClient.initializeSynchronous(Arrays.asList(SCOPES), listener, null);
if (listener.exception != null)
throw listener.exception;
if (listener.status == LiveStatus.CONNECTED) {
mConnectClient = new LiveConnectClient(listener.session);
if (mFolderCache.isEmpty())
{
initializeFoldersCache();

View File

@ -1,7 +1,17 @@
package keepass2android.javafilestorage;
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException;
public class UserInteractionRequiredException extends Exception {
public UserInteractionRequiredException(UserRecoverableAuthIOException e) {
super(e);
}
public UserInteractionRequiredException() {
}
/**
*
*/

View File

@ -10,17 +10,28 @@ public class PrepareFileUsageListener implements LiveAuthListener {
public Exception exception;
public LiveStatus status;
volatile boolean done;
public LiveConnectSession session;
public boolean isDone()
{
return done;
}
@Override
public void onAuthError(LiveAuthException _exception,
Object userState) {
exception = _exception;
done = true;
}
@Override
public void onAuthComplete(LiveStatus _status,
LiveConnectSession session, Object userState)
LiveConnectSession _session, Object userState)
{
status = _status;
session = _session;
done = true;
}
}