diff --git a/src/java/.gitignore b/src/java/.gitignore new file mode 100644 index 00000000..02d9721d --- /dev/null +++ b/src/java/.gitignore @@ -0,0 +1,2 @@ +/InputStickAPI - Kopie +/DemoBT diff --git a/src/java/JavaFileStorage/project.properties b/src/java/JavaFileStorage/project.properties index a42052c5..e5bc908c 100644 --- a/src/java/JavaFileStorage/project.properties +++ b/src/java/JavaFileStorage/project.properties @@ -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 diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/AccessTokenRequest.java b/src/java/JavaFileStorage/src/com/microsoft/live/AccessTokenRequest.java new file mode 100644 index 00000000..9d98e4da --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/AccessTokenRequest.java @@ -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 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())); + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/ApiRequest.java b/src/java/JavaFileStorage/src/com/microsoft/live/ApiRequest.java new file mode 100644 index 00000000..7cf6cc92 --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/ApiRequest.java @@ -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 { + + 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 observers; + private final String path; + private final ResponseHandler 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 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 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(); + 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; +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/ApiRequestAsync.java b/src/java/JavaFileStorage/src/com/microsoft/live/ApiRequestAsync.java new file mode 100644 index 00000000..a5b1c35e --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/ApiRequestAsync.java @@ -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 extends AsyncTask + implements UploadProgressListener { + + public interface Observer { + 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 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 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 ApiRequestAsync newInstance(ApiRequest request) { + return new ApiRequestAsync(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 ApiRequestAsync newInstance(EntityEnclosingApiRequest request) { + return new ApiRequestAsync(request); + } + + private final ArrayList> observers; + private final ArrayList progressListeners; + private final ApiRequest request; + + { + this.observers = new ArrayList>(); + this.progressListeners = new ArrayList(); + } + + /** + * 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 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 request) { + assert request != null; + + this.request = request; + } + + public boolean addObserver(Observer 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 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); + } + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/AuthorizationRequest.java b/src/java/JavaFileStorage/src/com/microsoft/live/AuthorizationRequest.java new file mode 100644 index 00000000..8751f654 --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/AuthorizationRequest.java @@ -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 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(); + } + + /** + * 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 { + 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 getFragmentParametersMap(Uri uri) { + String fragment = uri.getFragment(); + String[] keyValuePairs = TextUtils.split(fragment, AMPERSAND); + Map fragementParameters = new HashMap(); + + 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 Section + * 1.3.1 of the OAuth 2.0 spec. + * + * @param fragmentParameters in the uri + */ + private void onAccessTokenResponse(Map 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 Section + * 4.1.2 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 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); + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/Config.java b/src/java/JavaFileStorage/src/com/microsoft/live/Config.java new file mode 100644 index 00000000..7b9536cc --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/Config.java @@ -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; + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/CopyRequest.java b/src/java/JavaFileStorage/src/com/microsoft/live/CopyRequest.java new file mode 100644 index 00000000..7597cff4 --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/CopyRequest.java @@ -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 { + + 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; + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/DefaultObservableOAuthRequest.java b/src/java/JavaFileStorage/src/com/microsoft/live/DefaultObservableOAuthRequest.java new file mode 100644 index 00000000..e302d64d --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/DefaultObservableOAuthRequest.java @@ -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 observers; + + public DefaultObservableOAuthRequest() { + this.observers = new ArrayList(); + } + + @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); + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/DeleteRequest.java b/src/java/JavaFileStorage/src/com/microsoft/live/DeleteRequest.java new file mode 100644 index 00000000..7dd7ab54 --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/DeleteRequest.java @@ -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 { + + 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()); + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/DeviceType.java b/src/java/JavaFileStorage/src/com/microsoft/live/DeviceType.java new file mode 100644 index 00000000..a4e50e06 --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/DeviceType.java @@ -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(); +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/DownloadRequest.java b/src/java/JavaFileStorage/src/com/microsoft/live/DownloadRequest.java new file mode 100644 index 00000000..4d05c741 --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/DownloadRequest.java @@ -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 { + + 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()); + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/EntityEnclosingApiRequest.java b/src/java/JavaFileStorage/src/com/microsoft/live/EntityEnclosingApiRequest.java new file mode 100644 index 00000000..61fb4147 --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/EntityEnclosingApiRequest.java @@ -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 extends ApiRequest { + + /** + * 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 listeners; + + ProgressableEntity(HttpEntity wrapped, List 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 listeners; + long numBytesWritten; + long totalBytes; + + public ProgressableOutputStream(OutputStream outstream, + long totalBytes, + List 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 listeners; + + public EntityEnclosingApiRequest(LiveConnectSession session, + HttpClient client, + ResponseHandler 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 responseHandler, + String path, + HttpEntity entity, + ResponseCodes responseCodes, + Redirects redirects) { + super(session, client, responseHandler, path, responseCodes, redirects); + + assert entity != null; + + this.listeners = new ArrayList(); + 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); + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/ErrorMessages.java b/src/java/JavaFileStorage/src/com/microsoft/live/ErrorMessages.java new file mode 100644 index 00000000..29b163bb --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/ErrorMessages.java @@ -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); } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/GetRequest.java b/src/java/JavaFileStorage/src/com/microsoft/live/GetRequest.java new file mode 100644 index 00000000..cd34857f --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/GetRequest.java @@ -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 { + + 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()); + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/HttpCopy.java b/src/java/JavaFileStorage/src/com/microsoft/live/HttpCopy.java new file mode 100644 index 00000000..9df83ad7 --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/HttpCopy.java @@ -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; + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/HttpMove.java b/src/java/JavaFileStorage/src/com/microsoft/live/HttpMove.java new file mode 100644 index 00000000..89749f74 --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/HttpMove.java @@ -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; + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/InputStreamResponseHandler.java b/src/java/JavaFileStorage/src/com/microsoft/live/InputStreamResponseHandler.java new file mode 100644 index 00000000..3965490b --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/InputStreamResponseHandler.java @@ -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 { + 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(); + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/JsonEntity.java b/src/java/JavaFileStorage/src/com/microsoft/live/JsonEntity.java new file mode 100644 index 00000000..f52ff0d0 --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/JsonEntity.java @@ -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); + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/JsonResponseHandler.java b/src/java/JavaFileStorage/src/com/microsoft/live/JsonResponseHandler.java new file mode 100644 index 00000000..98c1a31a --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/JsonResponseHandler.java @@ -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 { + 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()); + } + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/LiveAuthClient.java b/src/java/JavaFileStorage/src/com/microsoft/live/LiveAuthClient.java new file mode 100644 index 00000000..e917ae76 --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/LiveAuthClient.java @@ -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 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 MSDN Live Connect + * Reference's Scopes and permissions for a list of scopes and explanations. + * @param listener called on either completion or error during the initialize process. + */ + public void initialize(Iterable 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 MSDN Live Connect + * Reference's Scopes and permissions 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 scopes, LiveAuthListener listener, Object userState) { + TokenRequestAsync asyncRequest = getInitializeRequest(scopes, listener, + userState); + if (asyncRequest == null) + { + return; + } + + asyncRequest.execute(); + } + + public void initializeSynchronous(Iterable scopes, LiveAuthListener listener, Object userState) { + TokenRequestAsync asyncRequest = getInitializeRequest(scopes, listener, + userState); + if (asyncRequest == null) + { + return; + } + + asyncRequest.executeSynchronous(); + } + + private TokenRequestAsync getInitializeRequest(Iterable 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(); + 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 MSDN Live Connect + * Reference's Scopes and permissions 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 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 MSDN Live Connect + * Reference's Scopes and permissions 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 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 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 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); + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/LiveAuthException.java b/src/java/JavaFileStorage/src/com/microsoft/live/LiveAuthException.java new file mode 100644 index 00000000..d9cd7e7c --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/LiveAuthException.java @@ -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; + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/LiveAuthListener.java b/src/java/JavaFileStorage/src/com/microsoft/live/LiveAuthListener.java new file mode 100644 index 00000000..e590910e --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/LiveAuthListener.java @@ -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); +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/LiveConnectClient.java b/src/java/JavaFileStorage/src/com/microsoft/live/LiveConnectClient.java new file mode 100644 index 00000000..11110057 --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/LiveConnectClient.java @@ -0,0 +1,1898 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2012 Microsoft Corporation. All rights reserved. +// +// Description: See the class level JavaDoc comments. +//------------------------------------------------------------------------------ + +package com.microsoft.live; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; + +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.HttpVersion; +import org.apache.http.client.HttpClient; +import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.conn.params.ConnManagerParams; +import org.apache.http.conn.scheme.PlainSocketFactory; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.entity.InputStreamEntity; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; +import org.apache.http.params.HttpProtocolParams; +import org.apache.http.protocol.HTTP; +import org.json.JSONException; +import org.json.JSONObject; + +import android.os.AsyncTask; +import android.text.TextUtils; + +/** + * {@code LiveConnectClient} is a class that is responsible for making requests over to the + * Live Connect REST API. In order to perform requests, a {@link LiveConnectSession} is required. + * A {@link LiveConnectSession} can be created from a {@link LiveAuthClient}. + * + * {@code LiveConnectClient} provides methods to perform both synchronous and asynchronous calls + * on the Live Connect REST API. A synchronous method's corresponding asynchronous method is + * suffixed with "Async" (e.g., the synchronous method, get, has a corresponding asynchronous + * method called, getAsync). Asynchronous methods require a call back listener that will be called + * back on the main/UI thread on completion, error, or progress. + */ +public class LiveConnectClient { + + /** Gets the ContentLength when a request finishes and sets it in the given operation. */ + private static class ContentLengthObserver implements ApiRequest.Observer { + private final LiveDownloadOperation operation; + + public ContentLengthObserver(LiveDownloadOperation operation) { + assert operation != null; + + this.operation = operation; + } + + @Override + public void onComplete(HttpResponse response) { + Header header = response.getFirstHeader(HTTP.CONTENT_LEN); + + // Sometimes this header is not included in the response. + if (header == null) { + return; + } + + int contentLength = Integer.valueOf(header.getValue()); + + this.operation.setContentLength(contentLength); + } + } + + /** + * Listens to an {@link ApiRequestAsync} for onComplete and onError events and calls the proper + * method on the given {@link LiveDownloadOperationListener} on a given event. + */ + private static class DownloadObserver implements ApiRequestAsync.Observer { + private final LiveDownloadOperationListener listener; + private final LiveDownloadOperation operation; + + public DownloadObserver(LiveDownloadOperation operation, + LiveDownloadOperationListener listener) { + assert operation != null; + assert listener != null; + + this.operation = operation; + this.listener = listener; + } + + @Override + public void onComplete(InputStream result) { + this.operation.setStream(result); + this.listener.onDownloadCompleted(this.operation); + } + + @Override + public void onError(LiveOperationException e) { + this.listener.onDownloadFailed(e, this.operation); + } + } + + /** + * Listens to an {@link ApiRequestAsync} for onComplete and onError events and calls the proper + * method on the given {@link LiveDownloadOperationListener} on a given event. When the download + * is complete this writes the results to a file, and publishes progress updates. + */ + private static class FileDownloadObserver extends AsyncTask + implements ApiRequestAsync.Observer { + private class OnErrorRunnable implements Runnable { + private final LiveOperationException exception; + + public OnErrorRunnable(LiveOperationException exception) { + this.exception = exception; + } + + @Override + public void run() { + listener.onDownloadFailed(exception, operation); + } + } + + private final File file; + private final LiveDownloadOperationListener listener; + private final LiveDownloadOperation operation; + + public FileDownloadObserver(LiveDownloadOperation operation, + LiveDownloadOperationListener listener, + File file) { + assert operation != null; + assert listener != null; + assert file != null; + + this.operation = operation; + this.listener = listener; + this.file = file; + } + + @Override + protected Runnable doInBackground(InputStream... params) { + InputStream is = params[0]; + + byte[] buffer = new byte[BUFFER_SIZE]; + + OutputStream out; + try { + out = new BufferedOutputStream(new FileOutputStream(file)); + } catch (FileNotFoundException e) { + LiveOperationException exception = + new LiveOperationException(ErrorMessages.CLIENT_ERROR, e); + return new OnErrorRunnable(exception); + } + + try { + int totalBytes = operation.getContentLength(); + int bytesRemaining = totalBytes; + + int bytesRead; + while ((bytesRead = is.read(buffer)) != -1) { + out.write(buffer, 0, bytesRead); + + bytesRemaining -= bytesRead; + publishProgress(totalBytes, bytesRemaining); + } + } catch (IOException e) { + LiveOperationException exception = + new LiveOperationException(ErrorMessages.CLIENT_ERROR, e); + return new OnErrorRunnable(exception); + } finally { + closeSilently(out); + closeSilently(is); + } + + return new Runnable() { + @Override + public void run() { + listener.onDownloadCompleted(operation); + } + }; + } + + @Override + protected void onPostExecute(Runnable result) { + result.run(); + } + + @Override + protected void onProgressUpdate(Integer... values) { + int totalBytes = values[0]; + int bytesRemaining = values[1]; + + assert totalBytes >= 0; + assert bytesRemaining >= 0; + assert totalBytes >= bytesRemaining; + + listener.onDownloadProgress(totalBytes, bytesRemaining, operation); + } + + @Override + public void onComplete(InputStream result) { + this.execute(result); + } + + @Override + public void onError(LiveOperationException e) { + this.listener.onDownloadFailed(e, this.operation); + } + } + + /** + * Listens to an {@link ApiRequestAsync} for onComplete and onError events and calls the proper + * method on the given {@link LiveOperationListener} on a given event. + */ + private static class OperationObserver implements ApiRequestAsync.Observer { + + private final LiveOperationListener listener; + private final LiveOperation operation; + + public OperationObserver(LiveOperation operation, + LiveOperationListener listener) { + assert operation != null; + assert listener != null; + + this.operation = operation; + this.listener = listener; + } + + @Override + public void onComplete(JSONObject result) { + this.operation.setResult(result); + this.listener.onComplete(this.operation); + } + + @Override + public void onError(LiveOperationException e) { + this.listener.onError(e, this.operation); + } + } + + /** non-instantiable class that contains static constants for parameter names. */ + private static final class ParamNames { + public static final String ACCESS_TOKEN = "session.getAccessToken()"; + public static final String BODY = "body"; + public static final String DESTINATION = "destination"; + public static final String FILE = "file"; + public static final String FILENAME = "filename"; + public static final String OVERWRITE = "overwrite"; + public static final String PATH = "path"; + public static final String SESSION = "session"; + + private ParamNames() { throw new AssertionError(ErrorMessages.NON_INSTANTIABLE_CLASS); } + } + + private enum SessionState { + LOGGED_IN { + @Override + public void check() { + // nothing. valid state. + } + }, + LOGGED_OUT { + @Override + public void check() { + throw new IllegalStateException(ErrorMessages.LOGGED_OUT); + } + }; + + public abstract void check(); + } + + /** + * Listens to an {@link ApiRequestAsync} for onComplete and onError events, and listens to an + * {@link EntityEnclosingApiRequest} for onProgress events and calls the + * proper {@link LiveUploadOperationListener} on such events. + */ + private static class UploadRequestListener implements ApiRequestAsync.Observer, + ApiRequestAsync.ProgressObserver { + + private final LiveUploadOperationListener listener; + private final LiveOperation operation; + + public UploadRequestListener(LiveOperation operation, + LiveUploadOperationListener listener) { + assert operation != null; + assert listener != null; + + this.operation = operation; + this.listener = listener; + } + + @Override + public void onComplete(JSONObject result) { + this.operation.setResult(result); + this.listener.onUploadCompleted(this.operation); + } + + @Override + public void onError(LiveOperationException e) { + assert e != null; + + this.listener.onUploadFailed(e, this.operation); + } + + @Override + public void onProgress(Long... values) { + long totalBytes = values[0].longValue(); + long numBytesWritten = values[1].longValue(); + + assert totalBytes >= 0L; + assert numBytesWritten >= 0L; + assert numBytesWritten <= totalBytes; + + long bytesRemaining = totalBytes - numBytesWritten; + this.listener.onUploadProgress((int)totalBytes, (int)bytesRemaining, this.operation); + } + } + + private static int BUFFER_SIZE = 1 << 10; + private static int CONNECT_TIMEOUT_IN_MS = 30 * 1000; + + /** The key used for HTTP MOVE and HTTP COPY requests. */ + private static final String DESTINATION_KEY = "destination"; + + private static volatile HttpClient HTTP_CLIENT; + private static Object HTTP_CLIENT_LOCK = new Object(); + + /** + * A LiveDownloadOperationListener 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 LiveDownloadOperationListener NULL_DOWNLOAD_OPERATION_LISTENER; + + /** + * A LiveOperationListener 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 LiveOperationListener NULL_OPERATION_LISTENER; + + /** + * A LiveUploadOperationListener 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 LiveUploadOperationListener NULL_UPLOAD_OPERATION_LISTENER; + + private static int SOCKET_TIMEOUT_IN_MS = 30 * 1000; + + static { + NULL_DOWNLOAD_OPERATION_LISTENER = new LiveDownloadOperationListener() { + @Override + public void onDownloadCompleted(LiveDownloadOperation operation) { + assert operation != null; + } + + @Override + public void onDownloadFailed(LiveOperationException exception, + LiveDownloadOperation operation) { + assert exception != null; + assert operation != null; + } + + @Override + public void onDownloadProgress(int totalBytes, + int bytesRemaining, + LiveDownloadOperation operation) { + assert totalBytes >= 0; + assert bytesRemaining >= 0; + assert totalBytes >= bytesRemaining; + assert operation != null; + } + }; + + NULL_OPERATION_LISTENER = new LiveOperationListener() { + @Override + public void onComplete(LiveOperation operation) { + assert operation != null; + } + + @Override + public void onError(LiveOperationException exception, LiveOperation operation) { + assert exception != null; + assert operation != null; + } + }; + + NULL_UPLOAD_OPERATION_LISTENER = new LiveUploadOperationListener() { + @Override + public void onUploadCompleted(LiveOperation operation) { + assert operation != null; + } + + @Override + public void onUploadFailed(LiveOperationException exception, + LiveOperation operation) { + assert exception != null; + assert operation != null; + } + + @Override + public void onUploadProgress(int totalBytes, + int bytesRemaining, + LiveOperation operation) { + assert totalBytes >= 0; + assert bytesRemaining >= 0; + assert totalBytes >= bytesRemaining; + assert operation != null; + } + }; + } + + /** + * Checks to see if the given path is a valid uri. + * + * @param path to check. + * @return the valid URI object. + */ + private static URI assertIsUri(String path) { + try { + return new URI(path); + } catch (URISyntaxException e) { + String message = String.format(ErrorMessages.INVALID_URI, ParamNames.PATH); + throw new IllegalArgumentException(message); + } + } + + /** + * Checks to see if the path is null, empty, or a valid uri. + * + * This method will be used for Download and Upload requests. + * This method will NOT be used for Copy, Delete, Get, Move, Post and Put requests. + * + * @param path object_id to check. + * @throws IllegalArgumentException if the path is empty or an invalid uri. + * @throws NullPointerException if the path is null. + */ + private static void assertValidPath(String path) { + LiveConnectUtils.assertNotNullOrEmpty(path, ParamNames.PATH); + assertIsUri(path); + } + + private static void closeSilently(Closeable c) { + try { + c.close(); + } catch (Exception e) { + // Silently...ssshh + } + } + + /** + * Checks to see if the path is null, empty, or is an absolute uri and throws + * the proper exception if it is. + * + * This method will be used for Copy, Delete, Get, Move, Post, and Put requests. + * This method will NOT be used for Download and Upload requests. + * + * @param path object_id to check. + * @throws IllegalArgumentException if the path is empty or an absolute uri. + * @throws NullPointerException if the path is null. + */ + private static void assertValidRelativePath(String path) { + LiveConnectUtils.assertNotNullOrEmpty(path, ParamNames.PATH); + + if (path.toLowerCase().startsWith("http") || path.toLowerCase().startsWith("https")) { + String message = String.format(ErrorMessages.ABSOLUTE_PARAMETER, ParamNames.PATH); + throw new IllegalArgumentException(message); + } + } + + /** + * Creates a new JSONObject body that has one key-value pair. + * @param key + * @param value + * @return a new JSONObject body with one key-value pair. + */ + private static JSONObject createJsonBody(String key, String value) { + Map tempBody = new HashMap(); + tempBody.put(key, value); + return new JSONObject(tempBody); + } + + private static HttpClient getHttpClient() { + // The LiveConnectClients can share one HttpClient with a ThreadSafeConnManager. + if (HTTP_CLIENT == null) { + synchronized (HTTP_CLIENT_LOCK) { + if (HTTP_CLIENT == null) { + HttpParams params = new BasicHttpParams(); + HttpConnectionParams.setConnectionTimeout(params, CONNECT_TIMEOUT_IN_MS); + HttpConnectionParams.setSoTimeout(params, SOCKET_TIMEOUT_IN_MS); + + ConnManagerParams.setMaxTotalConnections(params, 100); + HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1); + + SchemeRegistry schemeRegistry = new SchemeRegistry(); + schemeRegistry.register(new Scheme("http", + PlainSocketFactory.getSocketFactory(), + 80)); + schemeRegistry.register(new Scheme("https", + SSLSocketFactory.getSocketFactory(), + 443)); + + // Create an HttpClient with the ThreadSafeClientConnManager. + // This connection manager must be used if more than one thread will + // be using the HttpClient, which is a common scenario. + ClientConnectionManager cm = + new ThreadSafeClientConnManager(params, schemeRegistry); + HTTP_CLIENT = new DefaultHttpClient(cm, params); + } + } + } + + return HTTP_CLIENT; + } + + /** + * Constructs a new LiveOperation and calls the listener's onError method. + * + * @param e + * @param listener + * @param userState arbitrary object that is used to determine the caller of the method. + * @return a new LiveOperation + */ + private static LiveOperation handleException(String method, + String path, + LiveOperationException e, + LiveOperationListener listener, + Object userState) { + LiveOperation operation = + new LiveOperation.Builder(method, path).userState(userState).build(); + OperationObserver requestListener = + new OperationObserver(operation, listener); + + requestListener.onError(e); + return operation; + } + + /** + * Constructs a new LiveOperation and calls the listener's onUploadFailed method. + * + * @param e + * @param listener + * @param userState arbitrary object that is used to determine the caller of the method. + * @return a new LiveOperation + */ + private static LiveOperation handleException(String method, + String path, + LiveOperationException e, + LiveUploadOperationListener listener, + Object userState) { + LiveOperation operation = + new LiveOperation.Builder(method, path).userState(userState).build(); + UploadRequestListener requestListener = new UploadRequestListener(operation, listener); + + requestListener.onError(e); + return operation; + } + + /** + * Converts an InputStream to a {@code byte[]}. + * + * @param is to convert to a {@code byte[]}. + * @return a new {@code byte[]} from the InputStream. + * @throws IOException if there was an error reading or closing the InputStream. + */ + private static byte[] toByteArray(InputStream is) throws IOException { + ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); + OutputStream out = new BufferedOutputStream(byteOut); + is = new BufferedInputStream(is); + byte[] buffer = new byte[BUFFER_SIZE]; + + try { + int bytesRead; + while ((bytesRead = is.read(buffer)) != -1) { + out.write(buffer, 0, bytesRead); + } + } finally { + // we want to perform silent close operations + closeSilently(is); + closeSilently(out); + } + + return byteOut.toByteArray(); + } + + /** Change this to mock the HTTP responses. */ + private HttpClient httpClient; + + private final LiveConnectSession session; + private SessionState sessionState; + + /** + * Constructs a new {@code LiveConnectClient} instance and initializes it. + * + * @param session that will be used to authenticate calls over to the Live Connect REST API. + * @throws NullPointerException if session is null or if session.getAccessToken() is null. + * @throws IllegalArgumentException if session.getAccessToken() is empty. + */ + public LiveConnectClient(LiveConnectSession session) { + LiveConnectUtils.assertNotNull(session, ParamNames.SESSION); + + String accessToken = session.getAccessToken(); + LiveConnectUtils.assertNotNullOrEmpty(accessToken, ParamNames.ACCESS_TOKEN); + + this.session = session; + this.sessionState = SessionState.LOGGED_IN; + + // set a listener for the accessToken. If it is set to null, then the session was logged + // out. + this.session.addPropertyChangeListener("accessToken", new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent event) { + String newValue = (String)event.getNewValue(); + + if (TextUtils.isEmpty(newValue)) { + LiveConnectClient.this.sessionState = SessionState.LOGGED_OUT; + } else { + LiveConnectClient.this.sessionState = SessionState.LOGGED_IN; + } + } + }); + + this.httpClient = getHttpClient(); + } + + /** + * Performs a synchronous HTTP COPY on the Live Connect REST API. + * + * A COPY duplicates a resource. + * + * @param path object_id of the resource to copy. + * @param destination the folder_id where the resource will be copied to. + * @return The LiveOperation that contains the JSON result. + * @throws LiveOperationException if there is an error during the execution of the request. + * @throws IllegalArgumentException if the path or destination is empty or if the path is an + * absolute uri. + * @throws NullPointerException if either the path or destination parameters are null. + */ + public LiveOperation copy(String path, String destination) throws LiveOperationException { + assertValidRelativePath(path); + LiveConnectUtils.assertNotNullOrEmpty(destination, ParamNames.DESTINATION); + + CopyRequest request = this.createCopyRequest(path, destination); + return execute(request); + } + + /** + * Performs an asynchronous HTTP COPY on the Live Connect REST API. + * + * A COPY duplicates a resource. + * + * {@link LiveOperationListener#onComplete(LiveOperation)} will be called on success. + * Otherwise, {@link LiveOperationListener#onError(LiveOperationException, LiveOperation)} will + * be called. Both of these methods will be called on the main/UI thread. + * + * @param path object_id of the resource to copy. + * @param destination the folder_id where the resource will be copied to. + * @param listener called on either completion or error during the copy request. + * @return the LiveOperation associated with the request. + * @throws IllegalArgumentException if the path or destination is empty or if the path is an + * absolute uri. + * @throws NullPointerException if either the path or destination parameters are null. + */ + public LiveOperation copyAsync(String path, + String destination, + LiveOperationListener listener) { + return this.copyAsync(path, destination, listener, null); + } + + /** + * Performs an asynchronous HTTP COPY on the Live Connect REST API. + * + * A COPY duplicates a resource. + * + * {@link LiveOperationListener#onComplete(LiveOperation)} will be called on success. + * Otherwise, {@link LiveOperationListener#onError(LiveOperationException, LiveOperation)} will + * be called. Both of these methods will be called on the main/UI thread. + * + * @param path object_id of the resource to copy. + * @param destination the folder_id where the resource will be copied to + * @param listener called on either completion or error during the copy request. + * @param userState arbitrary object that is used to determine the caller of the method. + * @return the LiveOperation associated with the request. + * @throws IllegalArgumentException if the path or destination is empty or if the path is an + * absolute uri. + * @throws NullPointerException if either the path or destination parameters are null. + */ + public LiveOperation copyAsync(String path, + String destination, + LiveOperationListener listener, + Object userState) { + assertValidRelativePath(path); + LiveConnectUtils.assertNotNullOrEmpty(destination, ParamNames.DESTINATION); + if (listener == null) { + listener = NULL_OPERATION_LISTENER; + } + + CopyRequest request; + try { + request = this.createCopyRequest(path, destination); + } catch (LiveOperationException e) { + return handleException(CopyRequest.METHOD, path, e, listener, userState); + } + + return executeAsync(request, listener, userState); + } + + /** + * Performs a synchronous HTTP DELETE on the Live Connect REST API. + * + * HTTP DELETE deletes a resource. + * + * @param path object_id of the resource to delete. + * @return The LiveOperation that contains the delete response + * @throws LiveOperationException if there is an error during the execution of the request. + * @throws IllegalArgumentException if the path is empty or an absolute uri. + * @throws NullPointerException if the path is null. + */ + public LiveOperation delete(String path) throws LiveOperationException { + assertValidRelativePath(path); + + DeleteRequest request = new DeleteRequest(this.session, this.httpClient, path); + + return execute(request); + } + + /** + * Performs an asynchronous HTTP DELETE on the Live Connect REST API. + * + * HTTP DELETE deletes a resource. + * + * {@link LiveOperationListener#onComplete(LiveOperation)} will be called on success. + * Otherwise, {@link LiveOperationListener#onError(LiveOperationException, LiveOperation)} will + * be called. Both of these methods will be called on the main/UI thread. + * + * @param path object_id of the resource to delete. + * @param listener called on either completion or error during the delete request. + * @return the LiveOperation associated with the request. + * @throws IllegalArgumentException if the path is empty or an absolute uri. + * @throws NullPointerException if the path is null. + */ + public LiveOperation deleteAsync(String path, LiveOperationListener listener) { + return this.deleteAsync(path, listener, null); + } + + /** + * Performs an asynchronous HTTP DELETE on the Live Connect REST API. + * + * HTTP DELETE deletes a resource. + * + * {@link LiveOperationListener#onComplete(LiveOperation)} will be called on success. + * Otherwise, {@link LiveOperationListener#onError(LiveOperationException, LiveOperation)} will + * be called. Both of these methods will be called on the main/UI thread. + * + * @param path object_id of the resource to delete. + * @param listener called on either completion or error during the delete request. + * @param userState arbitrary object that is used to determine the caller of the method. + * @return the LiveOperation associated with the request. + * @throws IllegalArgumentException if the path is empty or an absolute uri. + * @throws NullPointerException if the path is null. + */ + public LiveOperation deleteAsync(String path, + LiveOperationListener listener, + Object userState) { + assertValidRelativePath(path); + if (listener == null) { + listener = NULL_OPERATION_LISTENER; + } + + DeleteRequest request = new DeleteRequest(this.session, this.httpClient, path); + + + return executeAsync(request, listener, userState); + } + + /** + * Downloads a resource by performing a synchronous HTTP GET on the Live Connect REST API that + * returns the response as an {@link InputStream}. + * + * @param path object_id of the resource to download. + * @throws LiveOperationException if there is an error during the execution of the request. + * @throws IllegalArgumentException if the path is empty or an invalid uri. + * @throws NullPointerException if the path is null. + */ + public LiveDownloadOperation download(String path) throws LiveOperationException { + assertValidPath(path); + + DownloadRequest request = new DownloadRequest(this.session, this.httpClient, path); + + LiveDownloadOperation operation = + new LiveDownloadOperation.Builder(request.getMethod(), request.getPath()).build(); + + request.addObserver(new ContentLengthObserver(operation)); + + InputStream stream = request.execute(); + operation.setStream(stream); + + return operation; + } + + /** + * Downloads a resource by performing an asynchronous HTTP GET on the Live Connect REST API that + * returns the response as an {@link InputStream}. + * + * {@link LiveDownloadOperationListener#onDownloadCompleted(LiveDownloadOperation)} will be + * called on success. + * On any download progress + * {@link LiveDownloadOperationListener#onDownloadProgress(int, int, LiveDownloadOperation)} + * will be called. + * Otherwise on error, + * {@link LiveDownloadOperationListener#onDownloadFailed(LiveOperationException, + * LiveDownloadOperation)} will + * be called. All of these methods will be called on the main/UI thread. + * + * @param path object_id of the resource to download. + * @param listener called on either completion or error during the download request. + * @return the LiveOperation associated with the request. + * @throws IllegalArgumentException if the path is empty or an invalid uri. + * @throws NullPointerException if the path is null. + */ + public LiveDownloadOperation downloadAsync(String path, + LiveDownloadOperationListener listener) { + return this.downloadAsync(path, listener, null); + } + + /** + * Downloads a resource by performing an asynchronous HTTP GET on the Live Connect REST API that + * returns the response as an {@link InputStream}. + * + * {@link LiveDownloadOperationListener#onDownloadCompleted(LiveDownloadOperation)} will be + * called on success. + * On any download progress + * {@link LiveDownloadOperationListener#onDownloadProgress(int, int, LiveDownloadOperation)} + * will be called. + * Otherwise on error, + * {@link LiveDownloadOperationListener#onDownloadFailed(LiveOperationException, + * LiveDownloadOperation)} will + * be called. All of these methods will be called on the main/UI thread. + * + * @param path object_id of the resource to download. + * @param listener called on either completion or error during the download request. + * @param userState arbitrary object that is used to determine the caller of the method. + * @return the LiveOperation associated with the request. + * @throws IllegalArgumentException if the path is empty or an invalid uri. + * @throws NullPointerException if the path is null. + */ + public LiveDownloadOperation downloadAsync(String path, + LiveDownloadOperationListener listener, + Object userState) { + assertValidPath(path); + if (listener == null) { + listener = NULL_DOWNLOAD_OPERATION_LISTENER; + } + + DownloadRequest request = new DownloadRequest(this.session, this.httpClient, path); + return executeAsync(request, listener, userState); + } + + public LiveDownloadOperation downloadAsync(String path, + File file, + LiveDownloadOperationListener listener) { + return this.downloadAsync(path, file, listener, null); + } + + public LiveDownloadOperation downloadAsync(String path, + File file, + LiveDownloadOperationListener listener, + Object userState) { + assertValidPath(path); + if (listener == null) { + listener = NULL_DOWNLOAD_OPERATION_LISTENER; + } + + DownloadRequest request = new DownloadRequest(this.session, this.httpClient, path); + ApiRequestAsync asyncRequest = ApiRequestAsync.newInstance(request); + + LiveDownloadOperation operation = + new LiveDownloadOperation.Builder(request.getMethod(), request.getPath()) + .userState(userState) + .apiRequestAsync(asyncRequest) + .build(); + + request.addObserver(new ContentLengthObserver(operation)); + asyncRequest.addObserver(new FileDownloadObserver(operation, listener, file)); + + asyncRequest.execute(); + + return operation; + } + + /** + * Performs a synchronous HTTP GET on the Live Connect REST API. + * + * HTTP GET retrieves the representation of a resource. + * + * @param path object_id of the resource to retrieve. + * @return The LiveOperation that contains the JSON result. + * @throws LiveOperationException if there is an error during the execution of the request. + * @throws IllegalArgumentException if the path is empty or an absolute uri. + * @throws NullPointerException if the path is null. + */ + public LiveOperation get(String path) throws LiveOperationException { + assertValidRelativePath(path); + + GetRequest request = new GetRequest(this.session, this.httpClient, path); + return execute(request); + } + + /** + * Performs an asynchronous HTTP GET on the Live Connect REST API. + * + * {@link LiveOperationListener#onComplete(LiveOperation)} will be called on success. + * Otherwise, {@link LiveOperationListener#onError(LiveOperationException, LiveOperation)} will + * be called. Both of these methods will be called on the main/UI thread. + * + * @param path of the resource to retrieve. + * @param listener called on either completion or error during the get request. + * @return the LiveOperation associated with the request. + * @throws IllegalArgumentException if the path is empty or an absolute uri. + * @throws NullPointerException if the path is null. + */ + public LiveOperation getAsync(String path, LiveOperationListener listener) { + return this.getAsync(path, listener, null); + } + + /** + * Performs an asynchronous HTTP GET on the Live Connect REST API. + * + * {@link LiveOperationListener#onComplete(LiveOperation)} will be called on success. + * Otherwise, {@link LiveOperationListener#onError(LiveOperationException, LiveOperation)} will + * be called. Both of these methods will be called on the main/UI thread. + * + * @param path object_id of the resource to retrieve. + * @param listener called on either completion or error during the get request. + * @param userState arbitrary object that is used to determine the caller of the method. + * @return the LiveOperation associated with the request. + * @throws IllegalArgumentException if the path is empty or an absolute uri. + * @throws NullPointerException if the path is null. + */ + public LiveOperation getAsync(String path, LiveOperationListener listener, Object userState) { + assertValidRelativePath(path); + if (listener == null) { + listener = NULL_OPERATION_LISTENER; + } + + GetRequest request = new GetRequest(this.session, this.httpClient, path); + return executeAsync(request, listener, userState); + } + + /** @return the {@link LiveConnectSession} instance used by this {@code LiveConnectClient}. */ + public LiveConnectSession getSession() { + return this.session; + } + + /** + * Performs a synchronous HTTP MOVE on the Live Connect REST API. + * + * A MOVE moves the location of a resource. + * + * @param path object_id of the resource to move. + * @param destination the folder_id to where the resource will be moved to. + * @return The LiveOperation that contains the JSON result. + * @throws LiveOperationException if there is an error during the execution of the request. + * @throws IllegalArgumentException if the path or destination is empty or if the path is an + * absolute uri. + * @throws NullPointerException if either the path or destination parameters are null. + */ + public LiveOperation move(String path, String destination) throws LiveOperationException { + assertValidRelativePath(path); + LiveConnectUtils.assertNotNullOrEmpty(destination, ParamNames.DESTINATION); + + MoveRequest request = this.createMoveRequest(path, destination); + return execute(request); + } + + /** + * Performs an asynchronous HTTP MOVE on the Live Connect REST API. + * + * A MOVE moves the location of a resource. + * + * {@link LiveOperationListener#onComplete(LiveOperation)} will be called on success. + * Otherwise, {@link LiveOperationListener#onError(LiveOperationException, LiveOperation)} will + * be called. Both of these methods will be called on the main/UI thread. + * + * @param path object_id of the resource to move. + * @param destination the folder_id to where the resource will be moved to. + * @param listener called on either completion or error during the copy request. + * @return the LiveOperation associated with the request. + * @throws IllegalArgumentException if the path or destination is empty or if the path is an + * absolute uri. + * @throws NullPointerException if either the path or destination parameters are null. + */ + public LiveOperation moveAsync(String path, + String destination, + LiveOperationListener listener) { + return this.moveAsync(path, destination, listener, null); + } + + /** + * Performs an asynchronous HTTP MOVE on the Live Connect REST API. + * + * A MOVE moves the location of a resource. + * + * {@link LiveOperationListener#onComplete(LiveOperation)} will be called on success. + * Otherwise, {@link LiveOperationListener#onError(LiveOperationException, LiveOperation)} will + * be called. Both of these methods will be called on the main/UI thread. + * + * @param path object_id of the resource to move. + * @param destination the folder_id to where the resource will be moved to. + * @param listener called on either completion or error during the copy request. + * @param userState arbitrary object that is used to determine the caller of the method. + * @return the LiveOperation associated with the request. + * @throws IllegalArgumentException if the path or destination is empty or if the path is an + * absolute uri. + * @throws NullPointerException if either the path or destination parameters are null. + */ + public LiveOperation moveAsync(String path, + String destination, + LiveOperationListener listener, + Object userState) { + assertValidRelativePath(path); + LiveConnectUtils.assertNotNullOrEmpty(destination, ParamNames.DESTINATION); + if (listener == null) { + listener = NULL_OPERATION_LISTENER; + } + + MoveRequest request; + try { + request = this.createMoveRequest(path, destination); + } catch (LiveOperationException e) { + return handleException(MoveRequest.METHOD, path, e, listener, userState); + } + + return executeAsync(request, listener, userState); + } + + /** + * Performs a synchronous HTTP POST on the Live Connect REST API. + * + * A POST adds a new resource to a collection. + * + * @param path object_id of the post request. + * @param body body of the post request. + * @return a LiveOperation that contains the JSON result. + * @throws LiveOperationException if there is an error during the execution of the request. + * @throws IllegalArgumentException if the path is empty or is an absolute uri. + * @throws NullPointerException if either the path or body parameters are null. + */ + public LiveOperation post(String path, JSONObject body) throws LiveOperationException { + assertValidRelativePath(path); + LiveConnectUtils.assertNotNull(body, ParamNames.BODY); + + PostRequest request = createPostRequest(path, body); + return execute(request); + } + + /** + * Performs a synchronous HTTP POST on the Live Connect REST API. + * + * A POST adds a new resource to a collection. + * + * @param path object_id of the post request. + * @param body body of the post request. + * @return a LiveOperation that contains the JSON result. + * @throws LiveOperationException if there is an error during the execution of the request. + * @throws IllegalArgumentException if the path is empty or is an absolute uri. + * @throws NullPointerException if either the path or body parameters are null. + */ + public LiveOperation post(String path, String body) throws LiveOperationException { + LiveConnectUtils.assertNotNullOrEmpty(body, ParamNames.BODY); + + JSONObject jsonBody; + try { + jsonBody = new JSONObject(body.toString()); + } catch (JSONException e) { + throw new LiveOperationException(ErrorMessages.CLIENT_ERROR, e); + } + + return this.post(path, jsonBody); + } + + /** + * Performs an asynchronous HTTP POST on the Live Connect REST API. + * + * A POST adds a new resource to a collection. + * + * {@link LiveOperationListener#onComplete(LiveOperation)} will be called on success. + * Otherwise, {@link LiveOperationListener#onError(LiveOperationException, LiveOperation)} will + * be called. Both of these methods will be called on the main/UI thread. + * + * @param path object_id of the post request. + * @param body body of the post request. + * @param listener called on either completion or error during the copy request. + * @return the LiveOperation associated with the request. + * @throws IllegalArgumentException if the path is empty or is an absolute uri. + * @throws NullPointerException if either the path or body parameters are null. + */ + public LiveOperation postAsync(String path, JSONObject body, LiveOperationListener listener) { + return this.postAsync(path, body, listener, null); + } + + /** + * Performs an asynchronous HTTP POST on the Live Connect REST API. + * + * A POST adds a new resource to a collection. + * + * {@link LiveOperationListener#onComplete(LiveOperation)} will be called on success. + * Otherwise, {@link LiveOperationListener#onError(LiveOperationException, LiveOperation)} will + * be called. Both of these methods will be called on the main/UI thread. + * + * @param path object_id of the post request. + * @param body body of the post request. + * @param listener called on either completion or error during the copy request. + * @param userState arbitrary object that is used to determine the caller of the method. + * @return the LiveOperation associated with the request. + * @throws IllegalArgumentException if the path is empty or is an absolute uri. + * @throws NullPointerException if either the path or body parameters are null. + */ + public LiveOperation postAsync(String path, + JSONObject body, + LiveOperationListener listener, + Object userState) { + assertValidRelativePath(path); + LiveConnectUtils.assertNotNull(body, ParamNames.BODY); + if (listener == null) { + listener = NULL_OPERATION_LISTENER; + } + + PostRequest request; + try { + request = createPostRequest(path, body); + } catch (LiveOperationException e) { + return handleException(PostRequest.METHOD, path, e, listener, userState); + } + + return executeAsync(request, listener, userState); + } + + /** + * Performs an asynchronous HTTP POST on the Live Connect REST API. + * + * A POST adds a new resource to a collection. + * + * {@link LiveOperationListener#onComplete(LiveOperation)} will be called on success. + * Otherwise, {@link LiveOperationListener#onError(LiveOperationException, LiveOperation)} will + * be called. Both of these methods will be called on the main/UI thread. + * + * @param path object_id of the post request. + * @param body body of the post request. + * @param listener called on either completion or error during the copy request. + * @return the LiveOperation associated with the request. + * @throws IllegalArgumentException if the path is empty or is an absolute uri. + * @throws NullPointerException if either the path or body parameters are null. + */ + public LiveOperation postAsync(String path, String body, LiveOperationListener listener) { + return this.postAsync(path, body, listener, null); + } + + /** + * Performs an asynchronous HTTP POST on the Live Connect REST API. + * + * A POST adds a new resource to a collection. + * + * {@link LiveOperationListener#onComplete(LiveOperation)} will be called on success. + * Otherwise, {@link LiveOperationListener#onError(LiveOperationException, LiveOperation)} will + * be called. Both of these methods will be called on the main/UI thread. + * + * @param path object_id of the post request. + * @param body body of the post request. + * @param listener called on either completion or error during the copy request. + * @param userState arbitrary object that is used to determine the caller of the method. + * @return the LiveOperation associated with the request. + * @throws IllegalArgumentException if the path is empty or is an absolute uri. + * @throws NullPointerException if either the path or body parameters are null. + */ + public LiveOperation postAsync(String path, + String body, + LiveOperationListener listener, + Object userState) { + LiveConnectUtils.assertNotNullOrEmpty(body, ParamNames.BODY); + + JSONObject jsonBody; + try { + jsonBody = new JSONObject(body.toString()); + } catch (JSONException e) { + return handleException(PostRequest.METHOD, + path, + new LiveOperationException(ErrorMessages.CLIENT_ERROR, e), + listener, + userState); + } + + return this.postAsync(path, jsonBody, listener, userState); + } + + /** + * Performs a synchronous HTTP PUT on the Live Connect REST API. + * + * A PUT updates a resource or if it does not exist, it creates a one. + * + * @param path object_id of the put request. + * @param body body of the put request. + * @return a LiveOperation that contains the JSON result. + * @throws LiveOperationException if there is an error during the execution of the request. + * @throws IllegalArgumentException if the path is empty or is an absolute uri. + * @throws NullPointerException if either the path or body parameters are null. + */ + public LiveOperation put(String path, JSONObject body) throws LiveOperationException { + assertValidRelativePath(path); + LiveConnectUtils.assertNotNull(body, ParamNames.BODY); + + PutRequest request = createPutRequest(path, body); + return execute(request); + } + + /** + * Performs a synchronous HTTP PUT on the Live Connect REST API. + * + * A PUT updates a resource or if it does not exist, it creates a one. + * + * @param path object_id of the put request. + * @param body body of the put request. + * @return a LiveOperation that contains the JSON result. + * @throws LiveOperationException if there is an error during the execution of the request. + * @throws IllegalArgumentException if the path is empty or is an absolute uri. + * @throws NullPointerException if either the path or body parameters are null. + */ + public LiveOperation put(String path, String body) throws LiveOperationException { + LiveConnectUtils.assertNotNullOrEmpty(body, ParamNames.BODY); + + JSONObject jsonBody; + try { + jsonBody = new JSONObject(body.toString()); + } catch (JSONException e) { + throw new LiveOperationException(ErrorMessages.CLIENT_ERROR, e); + } + + return this.put(path, jsonBody); + } + + /** + * Performs an asynchronous HTTP PUT on the Live Connect REST API. + * + * A PUT updates a resource or if it does not exist, it creates a one. + * + * {@link LiveOperationListener#onComplete(LiveOperation)} will be called on success. + * Otherwise, {@link LiveOperationListener#onError(LiveOperationException, LiveOperation)} will + * be called. Both of these methods will be called on the main/UI thread. + * + * @param path object_id of the put request. + * @param body body of the put request. + * @param listener called on either completion or error during the put request. + * @return the LiveOperation associated with the request. + * @throws IllegalArgumentException if the path is empty or is an absolute uri. + * @throws NullPointerException if either the path or body parameters are null. + */ + public LiveOperation putAsync(String path, JSONObject body, LiveOperationListener listener) { + return this.putAsync(path, body, listener, null); + } + + /** + * Performs an asynchronous HTTP PUT on the Live Connect REST API. + * + * A PUT updates a resource or if it does not exist, it creates a one. + * + * {@link LiveOperationListener#onComplete(LiveOperation)} will be called on success. + * Otherwise, {@link LiveOperationListener#onError(LiveOperationException, LiveOperation)} will + * be called. Both of these methods will be called on the main/UI thread. + * + * @param path of the put request. + * @param body of the put request. + * @param listener called on either completion or error during the put request. + * @param userState arbitrary object that is used to determine the caller of the method. + * @return the LiveOperation associated with the request. + * @throws IllegalArgumentException if the path is empty or is an absolute uri. + * @throws NullPointerException if either the path or body parameters are null. + */ + public LiveOperation putAsync(String path, + JSONObject body, + LiveOperationListener listener, + Object userState) { + assertValidRelativePath(path); + LiveConnectUtils.assertNotNull(body, ParamNames.BODY); + if (listener == null) { + listener = NULL_OPERATION_LISTENER; + } + + PutRequest request; + try { + request = createPutRequest(path, body); + } catch (LiveOperationException e) { + return handleException(PutRequest.METHOD, path, e, listener, userState); + } + + return executeAsync(request, listener, userState); + } + + /** + * Performs an asynchronous HTTP PUT on the Live Connect REST API. + * + * A PUT updates a resource or if it does not exist, it creates a one. + * + * {@link LiveOperationListener#onComplete(LiveOperation)} will be called on success. + * Otherwise, {@link LiveOperationListener#onError(LiveOperationException, LiveOperation)} will + * be called. Both of these methods will be called on the main/UI thread. + * + * @param path object_id of the put request. + * @param body body of the put request. + * @param listener called on either completion or error during the put request. + * @return the LiveOperation associated with the request. + * @throws IllegalArgumentException if the path is empty or is an absolute uri. + * @throws NullPointerException if either the path or body parameters are null. + */ + public LiveOperation putAsync(String path, String body, LiveOperationListener listener) { + return this.putAsync(path, body, listener, null); + } + + /** + * Performs an asynchronous HTTP PUT on the Live Connect REST API. + * + * A PUT updates a resource or if it does not exist, it creates a one. + * + * {@link LiveOperationListener#onComplete(LiveOperation)} will be called on success. + * Otherwise, {@link LiveOperationListener#onError(LiveOperationException, LiveOperation)} will + * be called. Both of these methods will be called on the main/UI thread. + * + * @param path object_id of the put request. + * @param body body of the put request. + * @param listener called on either completion or error during the put request. + * @param userState arbitrary object that is used to determine the caller of the method. + * @return the LiveOperation associated with the request. + * @throws IllegalArgumentException if the path is empty or is an absolute uri. + * @throws NullPointerException if either the path or body parameters are null. + */ + public LiveOperation putAsync(String path, + String body, + LiveOperationListener listener, + Object userState) { + LiveConnectUtils.assertNotNullOrEmpty(body, ParamNames.BODY); + JSONObject jsonBody; + try { + jsonBody = new JSONObject(body.toString()); + } catch (JSONException e) { + return handleException(PutRequest.METHOD, + path, + new LiveOperationException(ErrorMessages.CLIENT_ERROR, e), + listener, + userState); + } + + return this.putAsync(path, jsonBody, listener, userState); + } + + /** + * Uploads a resource by performing a synchronous HTTP PUT on the Live Connect REST API that + * returns the response as an {@link InputStream}. + * + * If a file with the same name exists the upload will fail. + * + * @param path location to upload to. + * @param filename name of the new resource. + * @param file contents of the upload. + * @return a LiveOperation that contains the JSON result. + * @throws LiveOperationException if there is an error during the execution of the request. + */ + public LiveOperation upload(String path, + String filename, + InputStream file) throws LiveOperationException { + return this.upload(path, filename, file, OverwriteOption.DoNotOverwrite); + } + + /** + * Uploads a resource by performing a synchronous HTTP PUT on the Live Connect REST API that + * returns the response as an {@link InputStream}. + * + * @param path location to upload to. + * @param filename name of the new resource. + * @param file contents of the upload. + * @param overwrite specifies what to do when a file with the same name exists. + * @return a LiveOperation that contains the JSON result. + * @throws LiveOperationException if there is an error during the execution of the request. + */ + public LiveOperation upload(String path, + String filename, + InputStream file, + OverwriteOption overwrite) throws LiveOperationException { + assertValidPath(path); + LiveConnectUtils.assertNotNullOrEmpty(filename, ParamNames.FILENAME); + LiveConnectUtils.assertNotNull(file, ParamNames.FILE); + LiveConnectUtils.assertNotNull(overwrite, ParamNames.OVERWRITE); + + // Currently, the API Service does not support chunked uploads, + // so we must know the length of the InputStream, before we send it. + // Load the stream into memory to get the length. + byte[] bytes; + try { + bytes = LiveConnectClient.toByteArray(file); + } catch (IOException e) { + throw new LiveOperationException(ErrorMessages.CLIENT_ERROR, e); + } + + UploadRequest request = createUploadRequest(path, + filename, + new ByteArrayInputStream(bytes), + bytes.length, + overwrite); + return execute(request); + } + + /** + * Uploads a resource by performing a synchronous HTTP PUT on the Live Connect REST API that + * returns the response as an {@link InputStream}. + * + * If a file with the same name exists the upload will fail. + * + * @param path location to upload to. + * @param filename name of the new resource. + * @param file contents of the upload. + * @return a LiveOperation that contains the JSON result. + * @throws LiveOperationException if there is an error during the execution of the request. + */ + public LiveOperation upload(String path, + String filename, + File file) throws LiveOperationException { + return this.upload(path, filename, file, OverwriteOption.DoNotOverwrite); + } + + /** + * Uploads a resource by performing a synchronous HTTP PUT on the Live Connect REST API that + * returns the response as an {@link InputStream}. + * + * @param path location to upload to. + * @param filename name of the new resource. + * @param file contents of the upload. + * @param overwrite specifies what to do when a file with the same name exists. + * @return a LiveOperation that contains the JSON result. + * @throws LiveOperationException if there is an error during the execution of the request. + */ + public LiveOperation upload(String path, + String filename, + File file, + OverwriteOption overwrite) throws LiveOperationException { + assertValidPath(path); + LiveConnectUtils.assertNotNullOrEmpty(filename, ParamNames.FILENAME); + LiveConnectUtils.assertNotNull(file, ParamNames.FILE); + LiveConnectUtils.assertNotNull(overwrite, ParamNames.OVERWRITE); + + InputStream is = null; + try { + is = new FileInputStream(file); + } catch (FileNotFoundException e) { + throw new LiveOperationException(ErrorMessages.CLIENT_ERROR, e); + } + + UploadRequest request; + request = createUploadRequest(path, + filename, + is, + file.length(), + overwrite); + return execute(request); + } + + /** + * Uploads a resource by performing an asynchronous HTTP PUT on the Live Connect REST API that + * returns the response as an {@link InputStream}. + * + * {@link LiveUploadOperationListener#onUploadCompleted(LiveOperation)} will be called on + * success. + * {@link LiveUploadOperationListener#onUploadProgress(int, int, LiveOperation) will be called + * on upload progress. Both of these methods will be called on the main/UI thread. + * Otherwise, + * {@link LiveUploadOperationListener#onUploadFailed(LiveOperationException, LiveOperation)} + * will be called. This method will NOT be called on the main/UI thread. + * + * @param path location to upload to. + * @param filename name of the new resource. + * @param file contents of the upload. + * @param overwrite specifies what to do when a file with the same name exists. + * @param listener called on completion, on progress, or on an error of the upload request. + * @param userState arbitrary object that is used to determine the caller of the method. + * @return the LiveOperation associated with the request. + */ + public LiveOperation uploadAsync(String path, + String filename, + InputStream file, + OverwriteOption overwrite, + LiveUploadOperationListener listener, + Object userState) { + assertValidPath(path); + LiveConnectUtils.assertNotNullOrEmpty(filename, ParamNames.FILENAME); + LiveConnectUtils.assertNotNull(file, ParamNames.FILE); + LiveConnectUtils.assertNotNull(overwrite, ParamNames.OVERWRITE); + if (listener == null) { + listener = NULL_UPLOAD_OPERATION_LISTENER; + } + + // Currently, the API Service does not support chunked uploads, + // so we must know the length of the InputStream, before we send it. + // Load the stream into memory to get the length. + byte[] bytes; + try { + bytes = LiveConnectClient.toByteArray(file); + } catch (IOException e) { + LiveOperationException exception = + new LiveOperationException(ErrorMessages.CLIENT_ERROR, e); + return handleException(UploadRequest.METHOD, path, exception, listener, userState); + } + + UploadRequest request; + try { + request = createUploadRequest(path, + filename, + new ByteArrayInputStream(bytes), + bytes.length, + overwrite); + } catch (LiveOperationException e) { + return handleException(UploadRequest.METHOD, path, e, listener, userState); + } + + ApiRequestAsync asyncRequest = ApiRequestAsync.newInstance(request); + + LiveOperation operation = new LiveOperation.Builder(request.getMethod(), request.getPath()) + .userState(userState) + .apiRequestAsync(asyncRequest) + .build(); + + UploadRequestListener operationListener = new UploadRequestListener(operation, listener); + + asyncRequest.addObserver(operationListener); + asyncRequest.addProgressObserver(operationListener); + asyncRequest.execute(); + + return operation; + } + + /** + * Uploads a resource by performing an asynchronous HTTP PUT on the Live Connect REST API that + * returns the response as an {@link InputStream}. + * + * {@link LiveUploadOperationListener#onUploadCompleted(LiveOperation)} will be called on + * success. + * {@link LiveUploadOperationListener#onUploadProgress(int, int, LiveOperation) will be called + * on upload progress. Both of these methods will be called on the main/UI thread. + * Otherwise, + * {@link LiveUploadOperationListener#onUploadFailed(LiveOperationException, LiveOperation)} + * will be called. This method will NOT be called on the main/UI thread. + * + * If a file with the same name exists the upload will fail. + * + * @param path location to upload to. + * @param filename name of the new resource. + * @param input contents of the upload. + * @param listener called on completion, on progress, or on an error of the upload request. + * @return the LiveOperation associated with the request. + */ + public LiveOperation uploadAsync(String path, + String filename, + InputStream input, + LiveUploadOperationListener listener) { + return this.uploadAsync(path, filename, input, listener, null); + } + + /** + * Uploads a resource by performing an asynchronous HTTP PUT on the Live Connect REST API that + * returns the response as an {@link InputStream}. + * + * {@link LiveUploadOperationListener#onUploadCompleted(LiveOperation)} will be called on + * success. + * {@link LiveUploadOperationListener#onUploadProgress(int, int, LiveOperation) will be called + * on upload progress. Both of these methods will be called on the main/UI thread. + * Otherwise, + * {@link LiveUploadOperationListener#onUploadFailed(LiveOperationException, LiveOperation)} + * will be called. This method will NOT be called on the main/UI thread. + * + * If a file with the same name exists the upload will fail. + * + * @param path location to upload to. + * @param filename name of the new resource. + * @param file contents of the upload. + * @param listener called on completion, on progress, or on an error of the upload request. + * @param userState arbitrary object that is used to determine the caller of the method. + * @return the LiveOperation associated with the request. + */ + public LiveOperation uploadAsync(String path, + String filename, + InputStream input, + LiveUploadOperationListener listener, + Object userState) { + return this.uploadAsync( + path, + filename, + input, + OverwriteOption.DoNotOverwrite, + listener, + userState); + } + + /** + * Uploads a resource by performing an asynchronous HTTP PUT on the Live Connect REST API that + * returns the response as an {@link InputStream}. + * + * {@link LiveUploadOperationListener#onUploadCompleted(LiveOperation)} will be called on + * success. + * {@link LiveUploadOperationListener#onUploadProgress(int, int, LiveOperation) will be called + * on upload progress. Both of these methods will be called on the main/UI thread. + * Otherwise, + * {@link LiveUploadOperationListener#onUploadFailed(LiveOperationException, LiveOperation)} + * will be called. This method will NOT be called on the main/UI thread. + * + * If a file with the same name exists the upload will fail. + * + * @param path location to upload to. + * @param filename name of the new resource. + * @param file contents of the upload. + * @param listener called on completion, on progress, or on an error of the upload request. + * @return the LiveOperation associated with the request. + */ + public LiveOperation uploadAsync(String path, + String filename, + File file, + LiveUploadOperationListener listener) { + return this.uploadAsync(path, filename, file, listener, null); + } + + /** + * Uploads a resource by performing an asynchronous HTTP PUT on the Live Connect REST API that + * returns the response as an {@link InputStream}. + * + * {@link LiveUploadOperationListener#onUploadCompleted(LiveOperation)} will be called on + * success. + * {@link LiveUploadOperationListener#onUploadProgress(int, int, LiveOperation) will be called + * on upload progress. Both of these methods will be called on the main/UI thread. + * Otherwise, + * {@link LiveUploadOperationListener#onUploadFailed(LiveOperationException, LiveOperation)} + * will be called. This method will NOT be called on the main/UI thread. + * + * If a file with the same name exists the upload will fail. + * + * @param path location to upload to. + * @param filename name of the new resource. + * @param file contents of the upload. + * @param listener called on completion, on progress, or on an error of the upload request. + * @param userState arbitrary object that is used to determine the caller of the method. + * @return the LiveOperation associated with the request. + */ + public LiveOperation uploadAsync(String path, + String filename, + File file, + LiveUploadOperationListener listener, + Object userState) { + return this.uploadAsync( + path, + filename, + file, + OverwriteOption.DoNotOverwrite, + listener, + userState); + } + + /** + * Uploads a resource by performing an asynchronous HTTP PUT on the Live Connect REST API that + * returns the response as an {@link InputStream}. + * + * {@link LiveUploadOperationListener#onUploadCompleted(LiveOperation)} will be called on + * success. + * {@link LiveUploadOperationListener#onUploadProgress(int, int, LiveOperation) will be called + * on upload progress. Both of these methods will be called on the main/UI thread. + * Otherwise, + * {@link LiveUploadOperationListener#onUploadFailed(LiveOperationException, LiveOperation)} + * will be called. This method will NOT be called on the main/UI thread. + * + * @param path location to upload to. + * @param filename name of the new resource. + * @param file contents of the upload. + * @param overwrite specifies what to do when a file with the same name exists. + * @param listener called on completion, on progress, or on an error of the upload request. + * @param userState arbitrary object that is used to determine the caller of the method. + * @return the LiveOperation associated with the request. + */ + public LiveOperation uploadAsync(String path, + String filename, + File file, + OverwriteOption overwrite, + LiveUploadOperationListener listener, + Object userState) { + assertValidPath(path); + LiveConnectUtils.assertNotNullOrEmpty(filename, ParamNames.FILENAME); + LiveConnectUtils.assertNotNull(file, ParamNames.FILE); + LiveConnectUtils.assertNotNull(overwrite, ParamNames.OVERWRITE); + if (listener == null) { + listener = NULL_UPLOAD_OPERATION_LISTENER; + } + + UploadRequest request; + try { + request = createUploadRequest(path, + filename, + new FileInputStream(file), + file.length(), + overwrite); + } catch (LiveOperationException e) { + return handleException(UploadRequest.METHOD, path, e, listener, userState); + } catch (FileNotFoundException e) { + LiveOperationException exception = + new LiveOperationException(ErrorMessages.CLIENT_ERROR, e); + return handleException(UploadRequest.METHOD, path, exception, listener, userState); + } + + ApiRequestAsync asyncRequest = ApiRequestAsync.newInstance(request); + + LiveOperation operation = new LiveOperation.Builder(request.getMethod(), request.getPath()) + .userState(userState) + .apiRequestAsync(asyncRequest) + .build(); + + UploadRequestListener operationListener = new UploadRequestListener(operation, listener); + + asyncRequest.addObserver(operationListener); + asyncRequest.addProgressObserver(operationListener); + asyncRequest.execute(); + + return operation; + } + + /** + * Sets the HttpClient that is used in requests. + * + * This is here to be able to mock the server for testing purposes. + * + * @param client + */ + void setHttpClient(HttpClient client) { + assert client != null; + this.httpClient = client; + } + + /** + * Creates a {@link CopyRequest} and its json body. + * @param path location of the request. + * @param destination value for the json body. + * @return a new {@link CopyRequest}. + * @throws LiveOperationException if there is an error creating the request. + */ + private CopyRequest createCopyRequest(String path, + String destination) throws LiveOperationException { + assert !TextUtils.isEmpty(path); + assert !TextUtils.isEmpty(destination); + + JSONObject body = LiveConnectClient.createJsonBody(DESTINATION_KEY, destination); + HttpEntity entity = createJsonEntity(body); + return new CopyRequest(this.session, this.httpClient, path, entity); + } + + private JsonEntity createJsonEntity(JSONObject body) throws LiveOperationException { + assert body != null; + + try { + return new JsonEntity(body); + } catch (UnsupportedEncodingException e) { + throw new LiveOperationException(ErrorMessages.CLIENT_ERROR, e); + } + } + + private MoveRequest createMoveRequest(String path, + String destination) throws LiveOperationException { + assert !TextUtils.isEmpty(path); + assert !TextUtils.isEmpty(destination); + + JSONObject body = LiveConnectClient.createJsonBody(DESTINATION_KEY, destination); + HttpEntity entity = createJsonEntity(body); + return new MoveRequest(this.session, this.httpClient, path, entity); + } + + private PostRequest createPostRequest(String path, + JSONObject body) throws LiveOperationException { + assert !TextUtils.isEmpty(path); + assert body != null; + + HttpEntity entity = createJsonEntity(body); + return new PostRequest(this.session, this.httpClient, path, entity); + } + + private PutRequest createPutRequest(String path, + JSONObject body) throws LiveOperationException { + assert !TextUtils.isEmpty(path); + assert body != null; + + HttpEntity entity = createJsonEntity(body); + return new PutRequest(this.session, this.httpClient, path, entity); + } + + private UploadRequest createUploadRequest(String path, + String filename, + InputStream is, + long length, + OverwriteOption overwrite) throws LiveOperationException { + assert !TextUtils.isEmpty(path); + assert !TextUtils.isEmpty(filename); + assert is != null; + + InputStreamEntity entity = new InputStreamEntity(is, length); + + return new UploadRequest(this.session, this.httpClient, path, entity, filename, overwrite); + } + + /** + * Creates a new LiveOperation and executes it synchronously. + * + * @param request + * @param listener + * @param userState arbitrary object that is used to determine the caller of the method. + * @return a new LiveOperation. + */ + private LiveOperation execute(ApiRequest request) throws LiveOperationException { + this.sessionState.check(); + + JSONObject result = request.execute(); + + LiveOperation.Builder builder = + new LiveOperation.Builder(request.getMethod(), request.getPath()).result(result); + + return builder.build(); + } + + /** + * Creates a new LiveDownloadOperation and executes it asynchronously. + * + * @param request + * @param listener + * @param userState arbitrary object that is used to determine the caller of the method. + * @return a new LiveDownloadOperation. + */ + private LiveDownloadOperation executeAsync(ApiRequest request, + LiveDownloadOperationListener listener, + Object userState) { + this.sessionState.check(); + + ApiRequestAsync asyncRequest = ApiRequestAsync.newInstance(request); + + LiveDownloadOperation operation = + new LiveDownloadOperation.Builder(request.getMethod(), request.getPath()) + .userState(userState) + .apiRequestAsync(asyncRequest) + .build(); + + + request.addObserver(new ContentLengthObserver(operation)); + asyncRequest.addObserver(new DownloadObserver(operation, listener)); + asyncRequest.execute(); + + return operation; + } + + /** + * Creates a new LiveOperation and executes it asynchronously. + * + * @param request + * @param listener + * @param userState arbitrary object that is used to determine the caller of the method. + * @return a new LiveOperation. + */ + private LiveOperation executeAsync(ApiRequest request, + LiveOperationListener listener, + Object userState) { + this.sessionState.check(); + + ApiRequestAsync asyncRequest = ApiRequestAsync.newInstance(request); + + LiveOperation operation = new LiveOperation.Builder(request.getMethod(), request.getPath()) + .userState(userState) + .apiRequestAsync(asyncRequest) + .build(); + + asyncRequest.addObserver(new OperationObserver(operation, listener)); + asyncRequest.execute(); + + return operation; + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/LiveConnectSession.java b/src/java/JavaFileStorage/src/com/microsoft/live/LiveConnectSession.java new file mode 100644 index 00000000..c14fff7b --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/LiveConnectSession.java @@ -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 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 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 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 scopes) { + final Iterable oldValue = this.scopes; + + // Defensive copy + this.scopes = new HashSet(); + 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); + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/LiveConnectUtils.java b/src/java/JavaFileStorage/src/com/microsoft/live/LiveConnectUtils.java new file mode 100644 index 00000000..dc06e013 --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/LiveConnectUtils.java @@ -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); } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/LiveDownloadOperation.java b/src/java/JavaFileStorage/src/com/microsoft/live/LiveDownloadOperation.java new file mode 100644 index 00000000..db8ce356 --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/LiveDownloadOperation.java @@ -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 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 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 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; + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/LiveDownloadOperationListener.java b/src/java/JavaFileStorage/src/com/microsoft/live/LiveDownloadOperationListener.java new file mode 100644 index 00000000..2d0348ef --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/LiveDownloadOperationListener.java @@ -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); +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/LiveOperation.java b/src/java/JavaFileStorage/src/com/microsoft/live/LiveOperation.java new file mode 100644 index 00000000..6cc07484 --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/LiveOperation.java @@ -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 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 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 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; + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/LiveOperationException.java b/src/java/JavaFileStorage/src/com/microsoft/live/LiveOperationException.java new file mode 100644 index 00000000..db4904eb --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/LiveOperationException.java @@ -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); + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/LiveOperationListener.java b/src/java/JavaFileStorage/src/com/microsoft/live/LiveOperationListener.java new file mode 100644 index 00000000..e3d6741e --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/LiveOperationListener.java @@ -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); +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/LiveStatus.java b/src/java/JavaFileStorage/src/com/microsoft/live/LiveStatus.java new file mode 100644 index 00000000..ef4cad33 --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/LiveStatus.java @@ -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; +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/LiveUploadOperationListener.java b/src/java/JavaFileStorage/src/com/microsoft/live/LiveUploadOperationListener.java new file mode 100644 index 00000000..7da4f357 --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/LiveUploadOperationListener.java @@ -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); +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/MoveRequest.java b/src/java/JavaFileStorage/src/com/microsoft/live/MoveRequest.java new file mode 100644 index 00000000..a5e5908d --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/MoveRequest.java @@ -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 { + + 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; + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/OAuth.java b/src/java/JavaFileStorage/src/com/microsoft/live/OAuth.java new file mode 100644 index 00000000..da6bc452 --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/OAuth.java @@ -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 OAuth 2.0 spec + * 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 Section 5.1 + * 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 Section 3.1.2 + * 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 Section 5.1 + * 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 Section 5.1 + * 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); } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/OAuthErrorResponse.java b/src/java/JavaFileStorage/src/com/microsoft/live/OAuthErrorResponse.java new file mode 100644 index 00000000..e2f96af3 --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/OAuthErrorResponse.java @@ -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); + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/OAuthRequestObserver.java b/src/java/JavaFileStorage/src/com/microsoft/live/OAuthRequestObserver.java new file mode 100644 index 00000000..7c5e4f01 --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/OAuthRequestObserver.java @@ -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); +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/OAuthResponse.java b/src/java/JavaFileStorage/src/com/microsoft/live/OAuthResponse.java new file mode 100644 index 00000000..32ee510a --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/OAuthResponse.java @@ -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); + +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/OAuthResponseVisitor.java b/src/java/JavaFileStorage/src/com/microsoft/live/OAuthResponseVisitor.java new file mode 100644 index 00000000..8a1b03dd --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/OAuthResponseVisitor.java @@ -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); +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/OAuthSuccessfulResponse.java b/src/java/JavaFileStorage/src/com/microsoft/live/OAuthSuccessfulResponse.java new file mode 100644 index 00000000..5f203a5d --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/OAuthSuccessfulResponse.java @@ -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 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); + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/ObservableOAuthRequest.java b/src/java/JavaFileStorage/src/com/microsoft/live/ObservableOAuthRequest.java new file mode 100644 index 00000000..a4422408 --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/ObservableOAuthRequest.java @@ -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); +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/OverwriteOption.java b/src/java/JavaFileStorage/src/com/microsoft/live/OverwriteOption.java new file mode 100644 index 00000000..d261ecd1 --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/OverwriteOption.java @@ -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(); +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/PostRequest.java b/src/java/JavaFileStorage/src/com/microsoft/live/PostRequest.java new file mode 100644 index 00000000..3a995750 --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/PostRequest.java @@ -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 { + + 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; + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/PreferencesConstants.java b/src/java/JavaFileStorage/src/com/microsoft/live/PreferencesConstants.java new file mode 100644 index 00000000..78a2b536 --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/PreferencesConstants.java @@ -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(); } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/PutRequest.java b/src/java/JavaFileStorage/src/com/microsoft/live/PutRequest.java new file mode 100644 index 00000000..32de9257 --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/PutRequest.java @@ -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 { + + 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; + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/QueryParameters.java b/src/java/JavaFileStorage/src/com/microsoft/live/QueryParameters.java new file mode 100644 index 00000000..6200bfce --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/QueryParameters.java @@ -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); + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/RefreshAccessTokenRequest.java b/src/java/JavaFileStorage/src/com/microsoft/live/RefreshAccessTokenRequest.java new file mode 100644 index 00000000..c836d247 --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/RefreshAccessTokenRequest.java @@ -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 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())); + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/ScreenSize.java b/src/java/JavaFileStorage/src/com/microsoft/live/ScreenSize.java new file mode 100644 index 00000000..ec67f36b --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/ScreenSize.java @@ -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; + } + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/TokenRequest.java b/src/java/JavaFileStorage/src/com/microsoft/live/TokenRequest.java new file mode 100644 index 00000000..1b0d56f0 --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/TokenRequest.java @@ -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 body = new ArrayList(); + 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 body); +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/TokenRequestAsync.java b/src/java/JavaFileStorage/src/com/microsoft/live/TokenRequestAsync.java new file mode 100644 index 00000000..f6db6c44 --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/TokenRequestAsync.java @@ -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 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); + + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/UploadRequest.java b/src/java/JavaFileStorage/src/com/microsoft/live/UploadRequest.java new file mode 100644 index 00000000..489c2e43 --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/UploadRequest.java @@ -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 { + + 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(); + } +} diff --git a/src/java/JavaFileStorage/src/com/microsoft/live/UriBuilder.java b/src/java/JavaFileStorage/src/com/microsoft/live/UriBuilder.java new file mode 100644 index 00000000..9dc499ca --- /dev/null +++ b/src/java/JavaFileStorage/src/com/microsoft/live/UriBuilder.java @@ -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 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(); + } + + /** + * 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 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(); + } +} diff --git a/src/java/JavaFileStorage/src/keepass2android/javafilestorage/GoogleDriveFileStorage.java b/src/java/JavaFileStorage/src/keepass2android/javafilestorage/GoogleDriveFileStorage.java index 21f4bebf..25b5018c 100644 --- a/src/java/JavaFileStorage/src/keepass2android/javafilestorage/GoogleDriveFileStorage.java +++ b/src/java/JavaFileStorage/src/keepass2android/javafilestorage/GoogleDriveFileStorage.java @@ -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); } diff --git a/src/java/JavaFileStorage/src/keepass2android/javafilestorage/SkyDriveFileStorage.java b/src/java/JavaFileStorage/src/keepass2android/javafilestorage/SkyDriveFileStorage.java index c2a70e4b..22f284d3 100644 --- a/src/java/JavaFileStorage/src/keepass2android/javafilestorage/SkyDriveFileStorage.java +++ b/src/java/JavaFileStorage/src/keepass2android/javafilestorage/SkyDriveFileStorage.java @@ -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(); diff --git a/src/java/JavaFileStorage/src/keepass2android/javafilestorage/UserInteractionRequiredException.java b/src/java/JavaFileStorage/src/keepass2android/javafilestorage/UserInteractionRequiredException.java index 2158f98d..c30d6dbd 100644 --- a/src/java/JavaFileStorage/src/keepass2android/javafilestorage/UserInteractionRequiredException.java +++ b/src/java/JavaFileStorage/src/keepass2android/javafilestorage/UserInteractionRequiredException.java @@ -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() { + + } + /** * */ diff --git a/src/java/JavaFileStorage/src/keepass2android/javafilestorage/skydrive/PrepareFileUsageListener.java b/src/java/JavaFileStorage/src/keepass2android/javafilestorage/skydrive/PrepareFileUsageListener.java index f16577d1..baf7442f 100644 --- a/src/java/JavaFileStorage/src/keepass2android/javafilestorage/skydrive/PrepareFileUsageListener.java +++ b/src/java/JavaFileStorage/src/keepass2android/javafilestorage/skydrive/PrepareFileUsageListener.java @@ -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; } }