implemented first version of WebDavStorage based on OkHttp

implemented first version of DropboxV2Storage (based on Api v2)
started to move to OneDrive SDK. duplicated MsaAuthenticator for making custom changes.
This commit is contained in:
Philipp Crocoll 2016-11-22 04:30:29 +01:00
parent 113d693f7a
commit a63663c30e
87 changed files with 2569 additions and 10267 deletions

View File

@ -8,7 +8,7 @@
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="true">
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.gradle" />

View File

@ -12,8 +12,9 @@
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
<option name="ASSEMBLE_TASK_NAME" value="assembleDebug" />
<option name="COMPILE_JAVA_TASK_NAME" value="compileDebugSources" />
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleDebugAndroidTest" />
<option name="COMPILE_JAVA_TEST_TASK_NAME" value="compileDebugAndroidTestSources" />
<afterSyncTasks>
<task>generateDebugSources</task>
</afterSyncTasks>
<option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
@ -23,9 +24,9 @@
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="false">
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/build/intermediates/classes/debug" />
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/debug" />
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/test/debug" />
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/debug" isTestSource="false" generated="true" />
@ -47,6 +48,13 @@
<sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/jni" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" />
@ -61,31 +69,30 @@
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/jni" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" />
<sourceFolder url="file://$MODULE_DIR$/src/test/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/jni" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/rs" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundles" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/coverage-instrumented-classes" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dependency-cache" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex-cache" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/jacoco" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/javaResources" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/libs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental-safeguard" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/lint" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/ndk" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/pre-dexed" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/proguard" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/proguard-rules" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/shaders" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/transforms" />
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
</content>
<orderEntry type="jdk" jdkName="Android API 23 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" exported="" name="httpmime-4.0.3" level="project" />
<orderEntry type="library" exported="" scope="TEST" name="commons-codec-1.3" level="project" />
<orderEntry type="library" exported="" name="jsr305-1.3.9" level="project" />
<orderEntry type="library" exported="" name="1_jsr305-1.3.9" level="project" />
<orderEntry type="library" exported="" name="google-http-client-1.20.0" level="project" />
@ -96,15 +103,19 @@
<orderEntry type="library" exported="" name="google-http-client-1.16.0-rc" level="project" />
<orderEntry type="library" exported="" name="google-oauth-client-1.16.0-rc" level="project" />
<orderEntry type="library" exported="" name="google-api-client-android-1.16.0-rc" level="project" />
<orderEntry type="library" exported="" scope="TEST" name="httpclient-4.0.1" level="project" />
<orderEntry type="library" exported="" scope="TEST" name="mockable-android-23" level="project" />
<orderEntry type="library" exported="" name="jackson-core-asl-1.9.11" level="project" />
<orderEntry type="library" exported="" name="google-http-client-jackson2-1.16.0-rc" level="project" />
<orderEntry type="library" exported="" scope="TEST" name="httpcore-4.0.1" level="project" />
<orderEntry type="library" exported="" name="1_httpcore-4.0.1" level="project" />
<orderEntry type="library" exported="" name="httpclient-4.0.3" level="project" />
<orderEntry type="library" exported="" name="httpcore-4.0.1" level="project" />
<orderEntry type="library" exported="" name="json_simple-1.1" level="project" />
<orderEntry type="library" exported="" name="google-http-client-android-1.16.0-rc" level="project" />
<orderEntry type="library" exported="" name="gson-2.1" level="project" />
<orderEntry type="library" exported="" name="google-http-client-gson-1.20.0" level="project" />
<orderEntry type="library" exported="" name="gson-2.1" level="project" />
<orderEntry type="library" exported="" name="google-http-client-jackson-1.16.0-rc" level="project" />
<orderEntry type="library" exported="" name="commons-logging-1.1.1" level="project" />
<orderEntry type="library" exported="" scope="TEST" name="commons-logging-1.1.1" level="project" />
<orderEntry type="library" exported="" name="1_commons-logging-1.1.1" level="project" />
</component>
</module>

View File

@ -2,9 +2,9 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion 23
buildToolsVersion '23.0.0'
buildToolsVersion '23.0.2'
defaultConfig {
minSdkVersion 14
minSdkVersion 15
targetSdkVersion 23
}
buildTypes {
@ -18,23 +18,61 @@ android {
}
dependencies {
compile 'com.google.http-client:google-http-client-gson:1.20.0'
compile 'com.google.code.gson:gson:2.1'
compile files('libs/commons-logging-1.1.1.jar')
compile files('libs/dropbox-android-sdk-1.6.2.jar')
compile files('libs/google-api-client-1.16.0-rc.jar')
compile files('libs/google-api-client-android-1.16.0-rc.jar')
compile files('libs/google-api-services-drive-v2-rev102-1.16.0-rc.jar')
compile files('libs/google-http-client-1.16.0-rc.jar')
compile files('libs/google-http-client-android-1.16.0-rc.jar')
compile files('libs/google-http-client-jackson-1.16.0-rc.jar')
compile files('libs/google-http-client-jackson2-1.16.0-rc.jar')
compile files('libs/google-oauth-client-1.16.0-rc.jar')
compile files('libs/httpclient-4.0.3.jar')
compile files('libs/httpcore-4.0.1.jar')
compile files('libs/httpmime-4.0.3.jar')
compile files('libs/jackson-core-2.1.3.jar')
compile files('libs/jackson-core-asl-1.9.11.jar')
compile files('libs/json_simple-1.1.jar')
compile files('libs/jsr305-1.3.9.jar')
/*
//compile files('libs/google-api-services-drive-v2-rev102-1.16.0-rc')
compile 'com.google.android.gms:play-services:6.5.+'
compile 'com.google.api-client:google-api-client-xml:1.18.0-rc'
compile 'com.google.http-client:google-http-client-gson:1.18.0-rc'
compile 'com.google.api-client:google-api-client-android:1.18.0-rc'
compile 'com.google.apis:google-api-services-drive:v2-rev155-1.19.0'
*/
compile 'com.squareup.okhttp3:okhttp:3.4.1'
compile 'com.burgstaller:okhttp-digest:1.7'
// compile files('libs/dropbox-android-sdk-1.6.2.jar')
compile 'com.google.android.gms:play-services:4.0.30'
compile('com.google.api-client:google-api-client-xml:1.17.0-rc') {
exclude group: 'com.google.android.google-play-services'
}
compile 'com.google.http-client:google-http-client-gson:1.17.0-rc'
compile('com.google.api-client:google-api-client-android:1.17.0-rc') {
exclude group: 'com.google.android.google-play-services'
}
compile 'com.google.apis:google-api-services-drive:v2-rev105-1.17.0-rc'
//compile 'com.dropbox.core:dropbox-core-sdk:2.0.1'
//compile group: 'com.dropbox.core', name: 'dropbox-core-sdk', version: '0-SNAPSHOT', changing: true
compile 'com.dropbox.core:dropbox-core-sdk:2.1.1'
//onedrive:
compile ('com.onedrive.sdk:onedrive-sdk-android:1.2+') {
transitive = false
}
// Include the gson dependency
compile ('com.google.code.gson:gson:2.3.1')
compile ('com.microsoft.services.msa:msa-auth:0.8.+')
compile ('com.microsoft.aad:adal:1.1.+')
/* compile 'com.google.http-client:google-http-client-gson:1.20.0'
compile 'com.google.code.gson:gson:2.1'
compile files('libs/commons-logging-1.1.1.jar')
compile files('libs/dropbox-android-sdk-1.6.2.jar')
compile files('libs/google-api-client-1.16.0-rc.jar')
compile files('libs/google-api-client-android-1.16.0-rc.jar')
compile files('libs/google-http-client-1.16.0-rc.jar')
compile files('libs/google-http-client-android-1.16.0-rc.jar')
compile files('libs/google-http-client-jackson-1.16.0-rc.jar')
compile files('libs/google-http-client-jackson2-1.16.0-rc.jar')
compile files('libs/google-oauth-client-1.16.0-rc.jar')
compile files('libs/httpclient-4.0.3.jar')
compile files('libs/httpcore-4.0.1.jar')
compile files('libs/httpmime-4.0.3.jar')
compile files('libs/jackson-core-2.1.3.jar')
compile files('libs/jackson-core-asl-1.9.11.jar')
compile files('libs/json_simple-1.1.jar')
compile files('libs/jsr305-1.3.9.jar')*/
}

View File

@ -18,3 +18,17 @@
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
-verbose
-dontobfuscate
-keep class keepass2android.javafilestorage.** {*; }
-keep class keepass2android.javafilestorage.JavaFileStorage {*; }
-keep class keepass2android.javafilestorage.JavaFileStorage$* {*; }
-keep interface keepass2android.javafilestorage.JavaFileStorage$* {*; }
-keep class keepass2android.javafilestorage.JavaFileStorage$FileEntry {*; }
-keepclassmembers class keepass2android.javafilestorage.JavaFileStorage {*; }
-keep interface keepass2android.javafilestorage.** {*; }
-keep interface keepass2android.javafilestorage.JavaFileStorage$FileStorageSetupActivity {*; }

View File

@ -1,23 +1,15 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="keepass2android.javafilestorage"
android:versionCode="1"
android:versionName="1.0" >
android:versionName="1.0">
<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="14" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
</application>
<uses-permission android:name="android.permission.GET_ACCOUNTS"/>
</manifest>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,33 +0,0 @@
//------------------------------------------------------------------------------
// 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);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,28 +0,0 @@
package keepass2android.javafilestorage;
import com.dropbox.client2.session.Session.AccessType;
import android.content.Context;
public class DropboxAppFolderFileStorage extends DropboxFileStorage {
public DropboxAppFolderFileStorage(Context ctx, String _appKey,
String _appSecret) {
super(ctx, _appKey, _appSecret, false, AccessType.APP_FOLDER);
}
public DropboxAppFolderFileStorage(Context ctx, String _appKey, String _appSecret, boolean clearKeysOnStart)
{
super(ctx, _appKey, _appSecret, clearKeysOnStart, AccessType.APP_FOLDER);
}
@Override
public String getProtocolId() {
return "dropboxKP2A";
}
}

View File

@ -1,529 +0,0 @@
package keepass2android.javafilestorage;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import com.dropbox.client2.DropboxAPI;
import com.dropbox.client2.android.AndroidAuthSession;
import com.dropbox.client2.android.AuthActivity;
import com.dropbox.client2.exception.DropboxException;
import com.dropbox.client2.exception.DropboxServerException;
import com.dropbox.client2.exception.DropboxUnlinkedException;
import com.dropbox.client2.session.AccessTokenPair;
import com.dropbox.client2.session.AppKeyPair;
import com.dropbox.client2.session.TokenPair;
import com.dropbox.client2.session.Session.AccessType;
public class DropboxFileStorage extends JavaFileStorageBase {
final static private String TAG = "KP2AJ";
final static private String ACCOUNT_PREFS_NAME = "prefs";
final static private String ACCESS_KEY_NAME = "ACCESS_KEY";
final static private String ACCESS_SECRET_NAME = "ACCESS_SECRET";
DropboxAPI<AndroidAuthSession> mApi;
private boolean mLoggedIn = false;
private Context mContext;
protected AccessType mAccessType = AccessType.DROPBOX;
private String appKey;
private String appSecret;
public DropboxFileStorage(Context ctx, String _appKey, String _appSecret)
{
initialize(ctx, _appKey, _appSecret, false, mAccessType);
}
public DropboxFileStorage(Context ctx, String _appKey, String _appSecret, boolean clearKeysOnStart)
{
initialize(ctx, _appKey, _appSecret, clearKeysOnStart, mAccessType);
}
public DropboxFileStorage(Context ctx, String _appKey, String _appSecret, boolean clearKeysOnStart, AccessType accessType)
{
initialize(ctx, _appKey, _appSecret, clearKeysOnStart, accessType);
}
private void initialize(Context ctx, String _appKey, String _appSecret,
boolean clearKeysOnStart, AccessType accessType) {
appKey = _appKey;
appSecret = _appSecret;
mContext = ctx;
if (clearKeysOnStart)
clearKeys();
this.mAccessType = accessType;
// We create a new AuthSession so that we can use the Dropbox API.
AndroidAuthSession session = buildSession();
mApi = new DropboxAPI<AndroidAuthSession>(session);
checkAppKeySetup();
}
public boolean tryConnect(Activity activity)
{
if (!mLoggedIn)
mApi.getSession().startAuthentication(activity);
return mLoggedIn;
}
private void setLoggedIn(boolean b) {
mLoggedIn = b;
}
private boolean checkAppKeySetup() {
// Check if the app has set up its manifest properly.
Intent testIntent = new Intent(Intent.ACTION_VIEW);
String scheme = "db-" + appKey;
String uri = scheme + "://" + AuthActivity.AUTH_VERSION + "/test";
testIntent.setData(Uri.parse(uri));
PackageManager pm = mContext.getPackageManager();
if (0 == pm.queryIntentActivities(testIntent, 0).size()) {
showToast("URL scheme in your app's " +
"manifest is not set up correctly. You should have a " +
"com.dropbox.client2.android.AuthActivity with the " +
"scheme: " + scheme);
return false;
}
return true;
}
public boolean isConnected()
{
return mLoggedIn;
}
public boolean checkForFileChangeFast(String path, String previousFileVersion) throws Exception
{
if ((previousFileVersion == null) || (previousFileVersion.equals("")))
return false;
try {
path = removeProtocol(path);
com.dropbox.client2.DropboxAPI.Entry entry = mApi.metadata(path, 1, null, false, null);
return entry.hash != previousFileVersion;
} catch (DropboxException e) {
throw convertException(e);
}
}
public String getCurrentFileVersionFast(String path)
{
try {
path = removeProtocol(path);
com.dropbox.client2.DropboxAPI.Entry entry = mApi.metadata(path, 1, null, false, null);
return entry.rev;
} catch (DropboxException e) {
Log.d(TAG, e.toString());
return "";
}
}
public InputStream openFileForRead(String path) throws Exception
{
try {
path = removeProtocol(path);
return mApi.getFileStream(path, null);
} catch (DropboxException e) {
//System.out.println("Something went wrong: " + e);
throw convertException(e);
}
}
public void uploadFile(String path, byte[] data, boolean writeTransactional) throws Exception
{
ByteArrayInputStream bis = new ByteArrayInputStream(data);
try {
path = removeProtocol(path);
//TODO: it would be nice to be able to use the parent version with putFile()
mApi.putFileOverwrite(path, bis, data.length, null);
} catch (DropboxException e) {
throw convertException(e);
}
}
private Exception convertException(DropboxException e) {
Log.d(TAG, "Exception of type " +e.getClass().getName()+":" + e.getMessage());
if (DropboxUnlinkedException.class.isAssignableFrom(e.getClass()) )
{
Log.d(TAG, "LoggedIn=false (due to unlink exception)");
setLoggedIn(false);
clearKeys();
return new UserInteractionRequiredException("Unlinked from Dropbox! User must re-link.", e);
}
//test for special error FileNotFound which must be reported with FileNotFoundException
if (DropboxServerException.class.isAssignableFrom(e.getClass()) )
{
DropboxServerException serverEx = (DropboxServerException)e;
if (serverEx.error == 404)
return new FileNotFoundException(e.toString());
}
return e;
}
private void showToast(String msg) {
Toast error = Toast.makeText(mContext, msg, Toast.LENGTH_LONG);
error.show();
}
/**
* Keep the access keys returned from Trusted Authenticator in a local
* store, rather than storing user name & password, and re-authenticating each
* time (which is not to be done, ever).
*
* @return Array of [access_key, access_secret], or null if none stored
*/
private String[] getKeys() {
SharedPreferences prefs = mContext.getSharedPreferences(ACCOUNT_PREFS_NAME, 0);
String key = prefs.getString(ACCESS_KEY_NAME, null);
String secret = prefs.getString(ACCESS_SECRET_NAME, null);
if (key != null && secret != null) {
String[] ret = new String[2];
ret[0] = key;
ret[1] = secret;
return ret;
} else {
return null;
}
}
/**
* Keeping the access keys returned from Trusted Authenticator in a local
* store, rather than storing user name & password, and re-authenticating each
* time (which is not to be done, ever).
*/
private void storeKeys(String key, String secret) {
Log.d(TAG, "Storing Dropbox accessToken");
// Save the access key for later
SharedPreferences prefs = mContext.getSharedPreferences(ACCOUNT_PREFS_NAME, 0);
Editor edit = prefs.edit();
edit.putString(ACCESS_KEY_NAME, key);
edit.putString(ACCESS_SECRET_NAME, secret);
edit.commit();
}
private void clearKeys() {
SharedPreferences prefs = mContext.getSharedPreferences(ACCOUNT_PREFS_NAME, 0);
Editor edit = prefs.edit();
edit.clear();
edit.commit();
}
private AndroidAuthSession buildSession() {
AppKeyPair appKeyPair = new AppKeyPair(appKey, appSecret);
AndroidAuthSession session;
String[] stored = getKeys();
if (stored != null) {
AccessTokenPair accessToken = new AccessTokenPair(stored[0], stored[1]);
session = new AndroidAuthSession(appKeyPair, mAccessType, accessToken);
setLoggedIn(true);
Log.d(TAG, "Creating Dropbox Session with accessToken");
} else {
session = new AndroidAuthSession(appKeyPair, mAccessType);
setLoggedIn(false);
Log.d(TAG, "Creating Dropbox Session without accessToken");
}
return session;
}
@Override
public String createFolder(String parentPath, String newDirName) throws Exception {
try
{
String path = parentPath;
if (!path.endsWith("/"))
path = path + "/";
path = path + newDirName;
String pathWithoutProtocol = removeProtocol(path);
mApi.createFolder(pathWithoutProtocol);
return path;
}
catch (DropboxException e) {
throw convertException(e);
}
}
@Override
public String createFilePath(String parentPath, String newFileName) throws Exception {
String path = parentPath;
if (!path.endsWith("/"))
path = path + "/";
path = path + newFileName;
return path;
}
@Override
public List<FileEntry> listFiles(String parentPath) throws Exception {
try
{
parentPath = removeProtocol(parentPath);
com.dropbox.client2.DropboxAPI.Entry dirEntry = mApi.metadata(parentPath, 0, null, true, null);
if (dirEntry.isDeleted)
throw new FileNotFoundException("Directory "+parentPath+" is deleted!");
List<FileEntry> result = new ArrayList<FileEntry>();
for (com.dropbox.client2.DropboxAPI.Entry e: dirEntry.contents)
{
if (e.isDeleted)
continue;
FileEntry fileEntry = convertToFileEntry(e);
result.add(fileEntry);
}
return result;
} catch (DropboxException e) {
throw convertException(e);
}
}
private FileEntry convertToFileEntry(com.dropbox.client2.DropboxAPI.Entry e) {
//Log.d("JFS","e="+e);
FileEntry fileEntry = new FileEntry();
fileEntry.canRead = true;
fileEntry.canWrite = true;
fileEntry.isDirectory = e.isDir;
fileEntry.sizeInBytes = e.bytes;
fileEntry.path = getProtocolId()+"://"+ e.path;
fileEntry.displayName = e.path.substring(e.path.lastIndexOf("/")+1);
//Log.d("JFS","fileEntry="+fileEntry);
Date lastModifiedDate = null;
if (e.modified != null)
lastModifiedDate = com.dropbox.client2.RESTUtility.parseDate(e.modified);
if (lastModifiedDate != null)
fileEntry.lastModifiedTime = lastModifiedDate.getTime();
else
fileEntry.lastModifiedTime = -1;
//Log.d("JFS","Ok. Dir="+fileEntry.isDirectory);
return fileEntry;
}
@Override
public void delete(String path) throws Exception {
try
{
path = removeProtocol(path);
mApi.delete(path);
} catch (DropboxException e) {
throw convertException(e);
}
}
@Override
public FileEntry getFileEntry(String filename) throws Exception {
try
{
filename = removeProtocol(filename);
Log.d("KP2AJ", "getFileEntry(), mApi = "+mApi+" filename="+filename);
com.dropbox.client2.DropboxAPI.Entry dbEntry = mApi.metadata(filename, 0, null, false, null);
Log.d("JFS", "dbEntry = "+dbEntry);
if (dbEntry.isDeleted)
throw new FileNotFoundException(filename+" is deleted!");
return convertToFileEntry(dbEntry);
} catch (DropboxException e) {
throw convertException(e);
}
}
@Override
public void startSelectFile(FileStorageSetupInitiatorActivity activity, boolean isForSave,
int requestCode)
{
String path = getProtocolId()+":///";
Log.d("KP2AJ", "startSelectFile "+path+", connected: "+path);
/*if (isConnected())
{
Intent intent = new Intent();
intent.putExtra(EXTRA_IS_FOR_SAVE, isForSave);
intent.putExtra(EXTRA_PATH, path);
activity.onImmediateResult(requestCode, RESULT_FILECHOOSER_PREPARED, intent);
}
else*/
{
activity.startSelectFileProcess(path, isForSave, requestCode);
}
}
@Override
public String getProtocolId() {
return "dropbox";
}
public boolean requiresSetup(String path)
{
return !isConnected();
}
@Override
public void prepareFileUsage(FileStorageSetupInitiatorActivity activity, String path, int requestCode, boolean alwaysReturnSuccess) {
if (isConnected())
{
Intent intent = new Intent();
intent.putExtra(EXTRA_PATH, path);
activity.onImmediateResult(requestCode, RESULT_FILEUSAGE_PREPARED, intent);
}
else
{
activity.startFileUsageProcess(path, requestCode, alwaysReturnSuccess);
}
}
@Override
public void prepareFileUsage(Context appContext, String path) throws UserInteractionRequiredException {
if (!isConnected())
{
throw new UserInteractionRequiredException();
}
}
@Override
public void onCreate(FileStorageSetupActivity activity, Bundle savedInstanceState) {
Log.d("KP2AJ", "OnCreate");
}
@Override
public void onResume(FileStorageSetupActivity activity) {
if (activity.getProcessName().equals(PROCESS_NAME_SELECTFILE))
activity.getState().putString(EXTRA_PATH, activity.getPath());
Log.d("KP2AJ", "OnResume (3). LoggedIn="+mLoggedIn);
/*if (mLoggedIn)
{
finishActivityWithSuccess(activity);
return;
}*/
AndroidAuthSession session = mApi.getSession();
JavaFileStorage.FileStorageSetupActivity storageSetupAct = (JavaFileStorage.FileStorageSetupActivity)activity;
if (storageSetupAct.getState().containsKey("hasStartedAuth"))
{
Log.d("KP2AJ", "auth started");
// The next part must be inserted in the onResume() method of the
// activity from which session.startAuthentication() was called, so
// that Dropbox authentication completes properly.
if (session.authenticationSuccessful()) {
Log.d("KP2AJ", "auth successful");
try {
// Mandatory call to complete the auth
session.finishAuthentication();
Log.d("KP2AJ", "finished auth ");
// Store it locally in our app for later use
TokenPair tokens = session.getAccessTokenPair();
storeKeys(tokens.key, tokens.secret);
setLoggedIn(true);
Log.d("KP2AJ", "success");
finishActivityWithSuccess(activity);
return;
} catch (Exception e) {
Log.d("KP2AJ", "finish with error: " + e.toString());
finishWithError(activity, e);
return;
}
}
Log.i(TAG, "authenticating not succesful");
Intent data = new Intent();
data.putExtra(EXTRA_ERROR_MESSAGE, "authenticating not succesful");
((Activity)activity).setResult(Activity.RESULT_CANCELED, data);
((Activity)activity).finish();
}
else
{
Log.d("KP2AJ", "Starting auth");
mApi.getSession().startAuthentication(((Activity)activity));
storageSetupAct.getState().putBoolean("hasStartedAuth", true);
}
}
@Override
public void onStart(FileStorageSetupActivity activity) {
}
@Override
public void onActivityResult(FileStorageSetupActivity activity, int requestCode, int resultCode, Intent data) {
//nothing to do here
}
String removeProtocol(String path)
{
if (path == null)
return null;
return path.substring(getProtocolId().length()+3);
}
@Override
public String getDisplayName(String path) {
return path;
}
@Override
public String getFilename(String path) throws Exception {
return path.substring(path.lastIndexOf("/")+1);
}
}

View File

@ -0,0 +1,586 @@
package keepass2android.javafilestorage;
import com.dropbox.core.DbxAppInfo;
import com.dropbox.core.DbxException;
import com.dropbox.core.DbxOAuth1AccessToken;
import com.dropbox.core.DbxOAuth1Upgrader;
import com.dropbox.core.DbxRequestConfig;
import com.dropbox.core.InvalidAccessTokenException;
import com.dropbox.core.android.Auth;
import com.dropbox.core.v2.DbxClientV2;
import com.dropbox.core.http.OkHttp3Requestor;
import com.dropbox.core.v2.files.DeleteErrorException;
import com.dropbox.core.v2.files.DeletedMetadata;
import com.dropbox.core.v2.files.DownloadErrorException;
import com.dropbox.core.v2.files.FileMetadata;
import com.dropbox.core.v2.files.FolderMetadata;
import com.dropbox.core.v2.files.GetMetadataErrorException;
import com.dropbox.core.v2.files.ListFolderErrorException;
import com.dropbox.core.v2.files.ListFolderResult;
import com.dropbox.core.v2.files.Metadata;
import com.dropbox.core.v2.files.WriteMode;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
/**
* Created by Philipp on 18.11.2016.
*/
public class DropboxV2Storage extends JavaFileStorageBase
{
private DbxAppInfo appInfo;
public void bla()
{
}
DbxRequestConfig requestConfig = DbxRequestConfig.newBuilder("kp2a")
.withHttpRequestor(OkHttp3Requestor.INSTANCE)
.build();
final static private String TAG = "KP2AJ";
final static private String ACCOUNT_PREFS_NAME = "prefs";
final static private String ACCESS_KEY_V1_NAME = "ACCESS_KEY";
final static private String ACCESS_SECRET_V1_NAME = "ACCESS_SECRET";
final static private String ACCESS_TOKEN_NAME = "ACCESS_TOKEN_V2";
private boolean mLoggedIn = false;
private Context mContext;
public FileEntry getRootFileEntry() {
FileEntry rootEntry = new FileEntry();
rootEntry.displayName = "";
rootEntry.isDirectory = true;
rootEntry.lastModifiedTime = -1;
rootEntry.canRead = rootEntry.canWrite = true;
rootEntry.path = getProtocolId()+":///";
rootEntry.sizeInBytes = -1;
return rootEntry;
}
public enum AccessType { Full, AppFolder};
protected AccessType mAccessType = AccessType.Full;
DbxClientV2 dbxClient;
public DropboxV2Storage(Context ctx, String _appKey, String _appSecret)
{
initialize(ctx, _appKey, _appSecret, false, mAccessType);
}
public DropboxV2Storage(Context ctx, String _appKey, String _appSecret, boolean clearKeysOnStart)
{
initialize(ctx, _appKey, _appSecret, clearKeysOnStart, mAccessType);
}
public DropboxV2Storage(Context ctx, String _appKey, String _appSecret, boolean clearKeysOnStart, AccessType accessType)
{
initialize(ctx, _appKey, _appSecret, clearKeysOnStart, accessType);
}
private void initialize(Context ctx, String _appKey, String _appSecret,
boolean clearKeysOnStart, AccessType accessType) {
appInfo = new DbxAppInfo(_appKey,_appSecret);
mContext = ctx;
if (clearKeysOnStart)
clearKeys();
this.mAccessType = accessType;
buildSession();
}
public boolean tryConnect(Activity activity)
{
if (!mLoggedIn)
Auth.startOAuth2Authentication(activity, appInfo.getKey());
return mLoggedIn;
}
private void setLoggedIn(boolean b) {
mLoggedIn = b;
}
public boolean isConnected()
{
return mLoggedIn;
}
public boolean checkForFileChangeFast(String path, String previousFileVersion) throws Exception
{
if ((previousFileVersion == null) || (previousFileVersion.equals("")))
return false;
path = removeProtocol(path);
try {
Metadata entry = dbxClient.files().getMetadata(path);
return !String.valueOf(entry.hashCode()) .equals(previousFileVersion);
} catch (DbxException e) {
throw convertException(e);
}
}
public String getCurrentFileVersionFast(String path)
{
try {
path = removeProtocol(path);
Metadata entry = dbxClient.files().getMetadata(path);
return String.valueOf(entry.hashCode());
} catch (DbxException e) {
Log.d(TAG, e.toString());
return "";
}
}
public InputStream openFileForRead(String path) throws Exception
{
try {
path = removeProtocol(path);
return dbxClient.files().download(path).getInputStream();
} catch (DbxException e) {
//System.out.println("Something went wrong: " + e);
throw convertException(e);
}
}
public void uploadFile(String path, byte[] data, boolean writeTransactional) throws Exception
{
ByteArrayInputStream bis = new ByteArrayInputStream(data);
try {
path = removeProtocol(path);
dbxClient.files().uploadBuilder(path).withMode(WriteMode.OVERWRITE).uploadAndFinish(bis);
} catch (DbxException e) {
throw convertException(e);
}
}
private Exception convertException(DbxException e) {
Log.d(TAG, "Exception of type " +e.getClass().getName()+":" + e.getMessage());
if (InvalidAccessTokenException.class.isAssignableFrom(e.getClass()) ) {
Log.d(TAG, "LoggedIn=false (due to InvalidAccessTokenException)");
setLoggedIn(false);
clearKeys();
return new UserInteractionRequiredException("Unlinked from Dropbox! User must re-link.", e);
}
if (ListFolderErrorException.class.isAssignableFrom(e.getClass()) ) {
ListFolderErrorException listFolderErrorException = (ListFolderErrorException)e;
if (listFolderErrorException.errorValue.getPathValue().isNotFound())
return new FileNotFoundException(e.toString());
}
if (DownloadErrorException.class.isAssignableFrom(e.getClass()) ) {
DownloadErrorException downloadErrorException = (DownloadErrorException)e;
if (downloadErrorException.errorValue.getPathValue().isNotFound())
return new FileNotFoundException(e.toString());
}
if (GetMetadataErrorException.class.isAssignableFrom(e.getClass()) ) {
GetMetadataErrorException getMetadataErrorException = (GetMetadataErrorException)e;
if (getMetadataErrorException.errorValue.getPathValue().isNotFound())
return new FileNotFoundException(e.toString());
}
if (DeleteErrorException.class.isAssignableFrom(e.getClass()) ) {
DeleteErrorException deleteErrorException = (DeleteErrorException)e;
if (deleteErrorException.errorValue.getPathLookupValue().isNotFound())
return new FileNotFoundException(e.toString());
}
return e;
}
private void showToast(String msg) {
Toast error = Toast.makeText(mContext, msg, Toast.LENGTH_LONG);
error.show();
}
/**
* Keep the access keys returned from Trusted Authenticator in a local
* store, rather than storing user name & password, and re-authenticating each
* time (which is not to be done, ever).
*
* @return Array of [access_key, access_secret], or null if none stored
*/
private String[] getKeysV1() {
SharedPreferences prefs = mContext.getSharedPreferences(ACCOUNT_PREFS_NAME, 0);
String key = prefs.getString(ACCESS_KEY_V1_NAME, null);
String secret = prefs.getString(ACCESS_SECRET_V1_NAME, null);
if (key != null && secret != null) {
String[] ret = new String[2];
ret[0] = key;
ret[1] = secret;
return ret;
} else {
return null;
}
}
private String getKeyV2() {
SharedPreferences prefs = mContext.getSharedPreferences(ACCOUNT_PREFS_NAME, 0);
return prefs.getString(ACCESS_TOKEN_NAME, null);
}
private void storeKey(String v2token) {
Log.d(TAG, "Storing Dropbox accessToken");
// Save the access key for later
SharedPreferences prefs = mContext.getSharedPreferences(ACCOUNT_PREFS_NAME, 0);
Editor edit = prefs.edit();
edit.putString(ACCESS_TOKEN_NAME, v2token);
edit.commit();
}
private void clearKeys() {
SharedPreferences prefs = mContext.getSharedPreferences(ACCOUNT_PREFS_NAME, 0);
Editor edit = prefs.edit();
edit.clear();
edit.commit();
}
private void buildSession() {
String v2Token = getKeyV2();
if (v2Token == null) {
String[] storedV1Keys = getKeysV1();
if (storedV1Keys != null) {
DbxOAuth1AccessToken v1Token = new DbxOAuth1AccessToken(storedV1Keys[0], storedV1Keys[1]);
DbxOAuth1Upgrader upgrader = new DbxOAuth1Upgrader(requestConfig, appInfo);
try {
v2Token = upgrader.createOAuth2AccessToken(v1Token);
upgrader.disableOAuth1AccessToken(v1Token);
storeKey(v2Token);
} catch (DbxException e) {
}
}
}
if (v2Token != null)
{
dbxClient = new DbxClientV2(requestConfig, v2Token);
setLoggedIn(true);
Log.d(TAG, "Creating Dropbox Session with accessToken");
} else {
setLoggedIn(false);
Log.d(TAG, "Creating Dropbox Session without accessToken");
}
}
@Override
public String createFolder(String parentPath, String newDirName) throws Exception {
try
{
String path = parentPath;
if (!path.endsWith("/"))
path = path + "/";
path = path + newDirName;
String pathWithoutProtocol = removeProtocol(path);
dbxClient.files().createFolder(pathWithoutProtocol);
return path;
}
catch (DbxException e) {
throw convertException(e);
}
}
@Override
public String createFilePath(String parentPath, String newFileName) throws Exception {
String path = parentPath;
if (!path.endsWith("/"))
path = path + "/";
path = path + newFileName;
return path;
}
@Override
public List<FileEntry> listFiles(String parentPath) throws Exception {
try
{
parentPath = removeProtocol(parentPath);
if (parentPath.equals("/"))
parentPath = ""; //Dropbox is a bit picky here
ListFolderResult dirEntry = dbxClient.files().listFolder(parentPath);
List<FileEntry> result = new ArrayList<FileEntry>();
while (true)
{
for (Metadata e: dirEntry.getEntries())
{
FileEntry fileEntry = convertToFileEntry(e);
result.add(fileEntry);
}
if (!dirEntry.getHasMore()) {
break;
}
dirEntry = dbxClient.files().listFolderContinue(dirEntry.getCursor());
}
return result;
} catch (DbxException e) {
throw convertException(e);
}
}
private FileEntry convertToFileEntry(Metadata e) throws Exception {
//Log.d("JFS","e="+e);
FileEntry fileEntry = new FileEntry();
fileEntry.canRead = true;
fileEntry.canWrite = true;
if (e instanceof FolderMetadata)
{
FolderMetadata fm = (FolderMetadata)e;
fileEntry.isDirectory = true;
fileEntry.sizeInBytes = 0;
fileEntry.lastModifiedTime = 0;
}
else if (e instanceof FileMetadata)
{
FileMetadata fm = (FileMetadata)e;
fileEntry.sizeInBytes = fm.getSize();
fileEntry.isDirectory = false;
fileEntry.lastModifiedTime = fm.getServerModified().getTime();
}
else if (e instanceof DeletedMetadata)
{
throw new FileNotFoundException();
}
else
{
throw new Exception("unexpected metadata " + e.getClass().getName() );
}
fileEntry.path = getProtocolId()+"://"+ e.getPathLower();
fileEntry.displayName = e.getName();
//Log.d("JFS","fileEntry="+fileEntry);
//Log.d("JFS","Ok. Dir="+fileEntry.isDirectory);
return fileEntry;
}
@Override
public void delete(String path) throws Exception {
try
{
path = removeProtocol(path);
dbxClient.files().delete(path);
} catch (DbxException e) {
throw convertException(e);
}
}
@Override
public FileEntry getFileEntry(String filename) throws Exception {
try
{
filename = removeProtocol(filename);
Log.d("KP2AJ", "getFileEntry(), " +filename);
//querying root is not supported
if ((filename.equals("")) || (filename.equals("/")))
return getRootFileEntry();
Metadata dbEntry = dbxClient.files().getMetadata(filename);
return convertToFileEntry(dbEntry);
} catch (DbxException e) {
throw convertException(e);
}
}
@Override
public void startSelectFile(FileStorageSetupInitiatorActivity activity, boolean isForSave,
int requestCode)
{
String path = getProtocolId()+":///";
Log.d("KP2AJ", "startSelectFile "+path+", connected: "+path);
/*if (isConnected())
{
Intent intent = new Intent();
intent.putExtra(EXTRA_IS_FOR_SAVE, isForSave);
intent.putExtra(EXTRA_PATH, path);
activity.onImmediateResult(requestCode, RESULT_FILECHOOSER_PREPARED, intent);
}
else*/
{
activity.startSelectFileProcess(path, isForSave, requestCode);
}
}
@Override
public String getProtocolId() {
return "dropbox";
}
public boolean requiresSetup(String path)
{
return !isConnected();
}
@Override
public void prepareFileUsage(FileStorageSetupInitiatorActivity activity, String path, int requestCode, boolean alwaysReturnSuccess) {
if (isConnected())
{
Intent intent = new Intent();
intent.putExtra(EXTRA_PATH, path);
activity.onImmediateResult(requestCode, RESULT_FILEUSAGE_PREPARED, intent);
}
else
{
activity.startFileUsageProcess(path, requestCode, alwaysReturnSuccess);
}
}
@Override
public void prepareFileUsage(Context appContext, String path) throws UserInteractionRequiredException {
if (!isConnected())
{
throw new UserInteractionRequiredException();
}
}
@Override
public void onCreate(FileStorageSetupActivity activity, Bundle savedInstanceState) {
Log.d("KP2AJ", "OnCreate");
}
@Override
public void onResume(FileStorageSetupActivity activity) {
if (activity.getProcessName().equals(PROCESS_NAME_SELECTFILE))
activity.getState().putString(EXTRA_PATH, activity.getPath());
Log.d("KP2AJ", "OnResume (3). LoggedIn="+mLoggedIn);
/*if (mLoggedIn)
{
finishActivityWithSuccess(activity);
return;
}*/
JavaFileStorage.FileStorageSetupActivity storageSetupAct = (JavaFileStorage.FileStorageSetupActivity)activity;
if (storageSetupAct.getState().containsKey("hasStartedAuth"))
{
Log.d("KP2AJ", "auth started");
String v2Token = Auth.getOAuth2Token();
if (v2Token != null) {
Log.d("KP2AJ", "auth successful");
try {
storeKey(v2Token);
buildSession();
finishActivityWithSuccess(activity);
return;
} catch (Exception e) {
Log.d("KP2AJ", "finish with error: " + e.toString());
finishWithError(activity, e);
return;
}
}
Log.i(TAG, "authenticating not succesful");
Intent data = new Intent();
data.putExtra(EXTRA_ERROR_MESSAGE, "authenticating not succesful");
((Activity)activity).setResult(Activity.RESULT_CANCELED, data);
((Activity)activity).finish();
}
else
{
Log.d("KP2AJ", "Starting auth");
Auth.startOAuth2Authentication((Activity)activity, appInfo.getKey());
storageSetupAct.getState().putBoolean("hasStartedAuth", true);
}
}
@Override
public void onStart(FileStorageSetupActivity activity) {
}
@Override
public void onActivityResult(FileStorageSetupActivity activity, int requestCode, int resultCode, Intent data) {
//nothing to do here
}
String removeProtocol(String path)
{
if (path == null)
return null;
return path.substring(getProtocolId().length()+3);
}
@Override
public String getDisplayName(String path) {
return path;
}
@Override
public String getFilename(String path) throws Exception {
return path.substring(path.lastIndexOf("/")+1);
}
}

View File

@ -9,7 +9,6 @@ import java.sql.Date;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import org.apache.http.protocol.HTTP;
import android.app.Activity;
import android.content.Intent;
@ -18,6 +17,7 @@ import android.util.Log;
public abstract class JavaFileStorageBase implements JavaFileStorage{
protected static final String UTF_8 = "UTF-8";
private static final String ISO_8859_1 = "ISO-8859-1";
private static final String UTF8_PREFIX = ".U8-";
@ -64,7 +64,7 @@ public abstract class JavaFileStorageBase implements JavaFileStorage{
protected String encode(final String unencoded)
throws UnsupportedEncodingException {
return UTF8_PREFIX+java.net.URLEncoder.encode(unencoded, HTTP.UTF_8);
return UTF8_PREFIX+java.net.URLEncoder.encode(unencoded, UTF_8);
}
@ -73,12 +73,14 @@ public abstract class JavaFileStorageBase implements JavaFileStorage{
//the first version of encode/decode used ISO 8859-1 which doesn't work with Cyrillic characters
//this is why we need to check for the prefix, even though all new strings are UTF8 encoded.
if (encodedString.startsWith(UTF8_PREFIX))
return java.net.URLDecoder.decode(encodedString.substring(UTF8_PREFIX.length()), HTTP.UTF_8);
return java.net.URLDecoder.decode(encodedString.substring(UTF8_PREFIX.length()), UTF_8);
else
return java.net.URLDecoder.decode(encodedString, ISO_8859_1);
}
public class InvalidPathException extends Exception
{
/**

View File

@ -1,10 +0,0 @@
package keepass2android.javafilestorage;
import android.content.Context;
public class KitKatFileStorage {
public KitKatFileStorage(Context ctx)
{
// ctx.getContentResolver().
}
}

View File

@ -0,0 +1,320 @@
package keepass2android.javafilestorage;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.util.Log;
import android.widget.Toast;
import com.onedrive.sdk.concurrency.ICallback;
import com.onedrive.sdk.core.ClientException;
import com.onedrive.sdk.core.DefaultClientConfig;
import com.onedrive.sdk.core.IClientConfig;
import com.onedrive.sdk.extensions.IItemCollectionPage;
import com.onedrive.sdk.extensions.IOneDriveClient;
import com.onedrive.sdk.extensions.Item;
import com.onedrive.sdk.extensions.OneDriveClient;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
/**
* Created by Philipp on 20.11.2016.
*/
public class OneDriveStorage extends JavaFileStorageBase
{
final keepass2android.javafilestorage.onedrive.MyMSAAuthenticator msaAuthenticator = new keepass2android.javafilestorage.onedrive.MyMSAAuthenticator() {
@Override
public String getClientId() {
return "000000004010C234";
}
@Override
public String[] getScopes() {
return new String[] { "offline_access", "onedrive.readwrite" };
}
};
IOneDriveClient oneDriveClient;
public void bla(final Activity activity) {
android.util.Log.d("KP2A", "0");
android.util.Log.d("KP2A", "1");
android.util.Log.d("KP2A", "2");
oneDriveClient
.getDrive()
.getRoot()
.buildRequest()
.get(new ICallback<Item>() {
@Override
public void success(final Item result) {
final String msg = "Found Root " + result.id;
Toast.makeText(activity, msg, Toast.LENGTH_SHORT)
.show();
}
@Override
public void failure(ClientException ex) {
Toast.makeText(activity, ex.toString(), Toast.LENGTH_SHORT)
.show();
}
});
android.util.Log.d("KP2A", "3");
}
@Override
public boolean requiresSetup(String path) {
return !isConnected();
}
@Override
public void startSelectFile(FileStorageSetupInitiatorActivity activity, boolean isForSave, int requestCode) {
String path = getProtocolId()+":///";
Log.d("KP2AJ", "startSelectFile "+path+", connected: "+path);
if (isConnected())
{
Intent intent = new Intent();
intent.putExtra(EXTRA_IS_FOR_SAVE, isForSave);
intent.putExtra(EXTRA_PATH, path);
activity.onImmediateResult(requestCode, RESULT_FILECHOOSER_PREPARED, intent);
}
else
{
activity.startSelectFileProcess(path, isForSave, requestCode);
}
}
private boolean isConnected() {
return msaAuthenticator.loginSilent() != null;
}
@Override
public void prepareFileUsage(FileStorageSetupInitiatorActivity activity, String path, int requestCode, boolean alwaysReturnSuccess) {
if (isConnected())
{
Intent intent = new Intent();
intent.putExtra(EXTRA_PATH, path);
activity.onImmediateResult(requestCode, RESULT_FILEUSAGE_PREPARED, intent);
}
else
{
activity.startFileUsageProcess(path, requestCode, alwaysReturnSuccess);
}
}
@Override
public String getProtocolId() {
return "onedrive";
}
@Override
public void prepareFileUsage(Context appContext, String path) throws UserInteractionRequiredException {
if (!isConnected())
{
throw new UserInteractionRequiredException();
}
}
@Override
public void onCreate(FileStorageSetupActivity activity, Bundle savedInstanceState) {
Log.d("KP2AJ", "OnCreate");
}
@Override
public void onResume(FileStorageSetupActivity activity) {
if (activity.getProcessName().equals(PROCESS_NAME_SELECTFILE))
activity.getState().putString(EXTRA_PATH, activity.getPath());
JavaFileStorage.FileStorageSetupActivity storageSetupAct = activity;
if (storageSetupAct.getState().containsKey("hasStartedAuth"))
{
Log.d("KP2AJ", "auth started");
if (oneDriveClient != null) {
Log.d("KP2AJ", "auth successful");
try {
finishActivityWithSuccess(activity);
return;
} catch (Exception e) {
Log.d("KP2AJ", "finish with error: " + e.toString());
finishWithError(activity, e);
return;
}
}
Log.i(TAG, "authenticating not succesful");
Intent data = new Intent();
data.putExtra(EXTRA_ERROR_MESSAGE, "authenticating not succesful");
((Activity)activity).setResult(Activity.RESULT_CANCELED, data);
((Activity)activity).finish();
}
else
{
Log.d("KP2AJ", "Starting auth");
final IClientConfig oneDriveConfig = new DefaultClientConfig() { };
oneDriveClient = new OneDriveClient.Builder()
//.fromConfig(oneDriveConfig)
.authenticator(msaAuthenticator)
.executors(oneDriveConfig.getExecutors())
.httpProvider(oneDriveConfig.getHttpProvider())
.serializer(oneDriveConfig.getSerializer())
.loginAndBuildClient((Activity)activity);
storageSetupAct.getState().putBoolean("hasStartedAuth", true);
}
}
String removeProtocol(String path)
{
if (path == null)
return null;
return path.substring(getProtocolId().length()+3);
}
@Override
public String getDisplayName(String path) {
return path;
}
@Override
public String getFilename(String path) throws Exception {
return path.substring(path.lastIndexOf("/")+1);
}
@Override
public boolean checkForFileChangeFast(String path, String previousFileVersion) throws Exception {
return false;
}
@Override
public String getCurrentFileVersionFast(String path) {
return null;
}
@Override
public InputStream openFileForRead(String path) throws Exception {
path = removeProtocol(path);
return oneDriveClient.getDrive()
.getRoot()
.getItemWithPath(path)
.getContent()
.buildRequest()
.get();
}
@Override
public void uploadFile(String path, byte[] data, boolean writeTransactional) throws Exception {
path = removeProtocol(path);
oneDriveClient.getDrive()
.getRoot()
.getItemWithPath(path)
.getContent()
.buildRequest()
.put(data);
}
@Override
public String createFolder(String parentPath, String newDirName) throws Exception {
throw new Exception("not implemented.");
}
@Override
public String createFilePath(String parentPath, String newFileName) throws Exception {
String path = parentPath;
if (!path.endsWith("/"))
path = path + "/";
path = path + newFileName;
return path;
}
@Override
public List<FileEntry> listFiles(String parentPath) throws Exception {
ArrayList<FileEntry> result = new ArrayList<FileEntry>();
parentPath = removeProtocol(parentPath);
IItemCollectionPage itemsPage = oneDriveClient.getDrive()
.getRoot()
.getItemWithPath(parentPath)
.getChildren()
.buildRequest()
.get();
while (true)
{
List<Item> items = itemsPage.getCurrentPage();
if (items.isEmpty())
return result;
itemsPage = itemsPage.getNextPage().buildRequest().get();
for (Item i: items)
{
FileEntry e = getFileEntry(getProtocolId() +"://"+ parentPath + "/" + i.name, i);
result.add(e);
}
}
}
@NonNull
private FileEntry getFileEntry(String path, Item i) {
FileEntry e = new FileEntry();
e.sizeInBytes = i.size;
e.displayName = i.name;
e.canRead = e.canWrite = true;
e.path = path;
e.isDirectory = i.folder != null;
return e;
}
@Override
public FileEntry getFileEntry(String filename) throws Exception {
filename = removeProtocol(filename);
Item item = oneDriveClient.getDrive()
.getRoot()
.getItemWithPath(filename)
.buildRequest()
.get();
return getFileEntry(filename, item);
}
@Override
public void delete(String path) throws Exception {
path = removeProtocol(path);
oneDriveClient.getDrive()
.getRoot()
.getItemWithPath(path)
.buildRequest()
.delete();
}
@Override
public void onStart(FileStorageSetupActivity activity) {
}
@Override
public void onActivityResult(FileStorageSetupActivity activity, int requestCode, int resultCode, Intent data) {
}
}

View File

@ -8,8 +8,6 @@ import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import org.apache.http.protocol.HTTP;
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.ChannelSftp.LsEntry;
@ -307,13 +305,13 @@ public class SftpStorage extends JavaFileStorageBase {
@Override
protected String decode(String encodedString)
throws UnsupportedEncodingException {
return java.net.URLDecoder.decode(encodedString, HTTP.UTF_8);
return java.net.URLDecoder.decode(encodedString, UTF_8);
}
@Override
protected String encode(final String unencoded)
throws UnsupportedEncodingException {
return java.net.URLEncoder.encode(unencoded, HTTP.UTF_8);
return java.net.URLEncoder.encode(unencoded, UTF_8);
}
ChannelSftp init(String filename) throws JSchException, UnsupportedEncodingException {
@ -321,7 +319,6 @@ public class SftpStorage extends JavaFileStorageBase {
ConnectionInfo ci = splitStringToConnectionInfo(filename);
Session session = jsch.getSession(ci.username, ci.host, ci.port);
UserInfo ui = new SftpUserInfo(ci.password);
session.setUserInfo(ui);

View File

@ -1,844 +0,0 @@
package keepass2android.javafilestorage;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import keepass2android.javafilestorage.skydrive.PrepareFileUsageListener;
import keepass2android.javafilestorage.skydrive.SkyDriveException;
import keepass2android.javafilestorage.skydrive.SkyDriveFile;
import keepass2android.javafilestorage.skydrive.SkyDriveFolder;
import keepass2android.javafilestorage.skydrive.SkyDriveObject;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import com.microsoft.live.LiveAuthClient;
import com.microsoft.live.LiveAuthException;
import com.microsoft.live.LiveAuthListener;
import com.microsoft.live.LiveConnectClient;
import com.microsoft.live.LiveConnectSession;
import com.microsoft.live.LiveDownloadOperation;
import com.microsoft.live.LiveOperation;
import com.microsoft.live.LiveOperationException;
import com.microsoft.live.LiveStatus;
import com.microsoft.live.OverwriteOption;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
public class SkyDriveFileStorage extends JavaFileStorageBase {
private LiveAuthClient mAuthClient;
private LiveConnectClient mConnectClient;
private String mRootFolderId;
private HashMap<String /* id */, SkyDriveObject> mFolderCache = new HashMap<String, SkyDriveObject>();
public static final String[] SCOPES = { "wl.signin", "wl.skydrive_update", "wl.offline_access"};
// see http://stackoverflow.com/questions/17997688/howto-to-parse-skydrive-api-date-in-java
SimpleDateFormat SKYDRIVE_DATEFORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.ENGLISH);
private Context mAppContext;
private String mClientId;
public final class JsonKeys {
public static final String CODE = "code";
public static final String DATA = "data";
public static final String DESCRIPTION = "description";
public static final String ERROR = "error";
public static final String EMAIL_HASHES = "email_hashes";
public static final String FIRST_NAME = "first_name";
public static final String GENDER = "gender";
public static final String ID = "id";
public static final String IS_FAVORITE = "is_favorite";
public static final String IS_FRIEND = "is_friend";
public static final String LAST_NAME = "last_name";
public static final String LOCALE = "locale";
public static final String LINK = "link";
public static final String MESSAGE = "message";
public static final String NAME = "name";
public static final String UPDATED_TIME = "updated_time";
public static final String USER_ID = "user_id";
public static final String PERMISSIONS = "permissions";
public static final String IS_DEFAULT = "is_default";
public static final String FROM = "from";
public static final String SUBSCRIPTION_LOCATION = "subscription_location";
public static final String CREATED_TIME = "created_time";
public static final String LOCATION = "location";
public static final String TYPE = "type";
public static final String PARENT_ID = "parent_id";
public static final String SOURCE = "source";
private JsonKeys() {
throw new AssertionError();
}
}
class SkyDrivePath {
String mPath;
public SkyDrivePath() {
}
public SkyDrivePath(String path) throws UnsupportedEncodingException,
FileNotFoundException, InvalidPathException,
LiveOperationException, SkyDriveException {
setPath(path);
}
public SkyDrivePath(String parentPath, JSONObject fileToAppend)
throws UnsupportedEncodingException, FileNotFoundException,
IOException, InvalidPathException, JSONException,
LiveOperationException, SkyDriveException {
setPath(parentPath);
if ((!mPath.endsWith("/")) && (!mPath.equals("")))
mPath = mPath + "/";
mPath += encode(fileToAppend.getString("name")) + NAME_ID_SEP
+ encode(fileToAppend.getString("id"));
}
public void setPath(String path) throws UnsupportedEncodingException,
InvalidPathException, FileNotFoundException,
LiveOperationException, SkyDriveException {
setPathWithoutVerify(path);
verifyWithRetry();
}
private void verifyWithRetry() throws FileNotFoundException,
LiveOperationException, SkyDriveException,
UnsupportedEncodingException {
try {
verify();
} catch (FileNotFoundException e) {
initializeFoldersCache();
verify();
}
}
public void setPathWithoutVerify(String path)
throws UnsupportedEncodingException, InvalidPathException {
mPath = path.substring(getProtocolPrefix().length());
logDebug( " mPath=" + mPath);
}
// make sure the path exists
private void verify() throws FileNotFoundException,
UnsupportedEncodingException {
if (mPath.equals(""))
return;
String[] parts = mPath.split("/");
String parentId = mRootFolderId;
for (int i = 0; i < parts.length; i++) {
String part = parts[i];
logDebug( "parsing part " + part);
int indexOfSeparator = part.lastIndexOf(NAME_ID_SEP);
if (indexOfSeparator < 0)
throw new FileNotFoundException("invalid path " + mPath);
String id = decode(part.substring(indexOfSeparator
+ NAME_ID_SEP.length()));
String name = decode(part.substring(0, indexOfSeparator));
logDebug( " name=" + name);
SkyDriveObject thisFolder = mFolderCache.get(id);
if (thisFolder == null) {
logDebug( "adding to cache");
thisFolder = tryAddToCache(id);
// check if it's still null
if (thisFolder == null)
throw new FileNotFoundException("couldn't find id "
+ id + " being part of " + mPath
+ " in SkyDrive ");
}
if (thisFolder.getParentId().equals(parentId) == false)
throw new FileNotFoundException("couldn't find parent id "
+ parentId + " as parent of "
+ thisFolder.getName() + " in " + mPath
+ " in SkyDrive");
if (thisFolder.getName().equals(name) == false)
throw new FileNotFoundException("Name of " + id
+ " changed from " + name + " to "
+ thisFolder.getName() + " in " + mPath
+ " in SkyDrive ");
parentId = id;
}
}
public String getDisplayName() {
// skydrive://
String displayName = getProtocolPrefix();
if (mPath.equals(""))
return displayName;
String[] parts = mPath.split("/");
for (int i = 0; i < parts.length; i++) {
String part = parts[i];
logDebug("parsing part " + part);
int indexOfSeparator = part.lastIndexOf(NAME_ID_SEP);
if (indexOfSeparator < 0) {
// seems invalid, but we're very generous here
displayName += "/" + part;
continue;
}
String name = part.substring(0, indexOfSeparator);
try {
name = decode(name);
} catch (UnsupportedEncodingException e) {
// ignore
}
displayName += "/" + name;
}
return displayName;
}
public String getSkyDriveId() throws InvalidPathException,
UnsupportedEncodingException {
String pathWithoutTrailingSlash = mPath;
if (pathWithoutTrailingSlash.endsWith("/"))
pathWithoutTrailingSlash = pathWithoutTrailingSlash.substring(
0, pathWithoutTrailingSlash.length() - 1);
if (pathWithoutTrailingSlash.equals("")) {
return mRootFolderId;
}
String lastPart = pathWithoutTrailingSlash
.substring(pathWithoutTrailingSlash
.lastIndexOf(NAME_ID_SEP) + NAME_ID_SEP.length());
if (lastPart.contains("/"))
throw new InvalidPathException(
"error extracting SkyDriveId from " + mPath);
return decode(lastPart);
}
public String getFullPath() throws UnsupportedEncodingException {
return getProtocolPrefix() + mPath;
}
public SkyDrivePath getParentPath() throws UnsupportedEncodingException, FileNotFoundException, InvalidPathException, LiveOperationException, SkyDriveException {
String pathWithoutTrailingSlash = mPath;
if (pathWithoutTrailingSlash.endsWith("/"))
pathWithoutTrailingSlash = pathWithoutTrailingSlash.substring(
0, pathWithoutTrailingSlash.length() - 1);
if (pathWithoutTrailingSlash.equals(""))
{
return null;
}
int indexOfLastSlash = pathWithoutTrailingSlash.lastIndexOf("/");
if (indexOfLastSlash == -1)
{
return new SkyDrivePath(getProtocolPrefix());
}
String parentPath = pathWithoutTrailingSlash.substring(0, indexOfLastSlash);
return new SkyDrivePath(getProtocolPrefix()+parentPath);
}
public String getFilename() throws InvalidPathException {
String pathWithoutTrailingSlash = mPath;
if (pathWithoutTrailingSlash.endsWith("/"))
pathWithoutTrailingSlash = pathWithoutTrailingSlash.substring(
0, pathWithoutTrailingSlash.length() - 1);
String[] parts = mPath.split("/");
String lastPart = parts[parts.length-1];
int indexOfSeparator = lastPart.lastIndexOf(NAME_ID_SEP);
if (indexOfSeparator < 0) {
throw new InvalidPathException("cannot extract filename from " + mPath);
}
String name = lastPart.substring(0, indexOfSeparator);
try {
name = decode(name);
} catch (UnsupportedEncodingException e) {
// ignore
}
return name;
}
};
public SkyDriveFileStorage(String clientId, Context appContext) {
logDebug("Constructing SkyDriveFileStorage");
mAuthClient = new LiveAuthClient(appContext, clientId);
mAppContext = appContext;
mClientId = clientId;
}
void login(final FileStorageSetupActivity activity) {
logDebug("skydrive login");
mAuthClient.login((Activity) activity, Arrays.asList(SCOPES),
new LiveAuthListener() {
@Override
public void onAuthComplete(LiveStatus status,
LiveConnectSession session, Object userState) {
if (status == LiveStatus.CONNECTED) {
initialize(activity, session);
} else {
finishWithError(activity, new Exception(
"Error connecting to SkdDrive. Status is "
+ status));
}
}
@Override
public void onAuthError(LiveAuthException exception,
Object userState) {
finishWithError(activity, exception);
}
});
}
private void initialize(final FileStorageSetupActivity setupAct,
LiveConnectSession session) {
logDebug("skydrive initialize");
mConnectClient = new LiveConnectClient(session);
final Activity activity = (Activity)setupAct;
AsyncTask<Object, Void, AsyncTaskResult<String> > task = new AsyncTask<Object, Void, AsyncTaskResult<String>>()
{
@Override
protected AsyncTaskResult<String> doInBackground(Object... arg0) {
try {
initializeFoldersCache();
if (setupAct.getProcessName().equals(PROCESS_NAME_SELECTFILE))
setupAct.getState().putString(EXTRA_PATH, getProtocolPrefix());
return new AsyncTaskResult<String>("ok");
} catch ( Exception anyError) {
return new AsyncTaskResult<String>(anyError);
}
}
@Override
protected void onPostExecute(AsyncTaskResult<String> result) {
Exception error = result.getError();
if (error != null ) {
finishWithError(setupAct, error);
} else if ( isCancelled()) {
activity.setResult(Activity.RESULT_CANCELED);
activity.finish();
} else {
//all right!
finishActivityWithSuccess(setupAct);
}
}
};
task.execute(new Object[]{});
}
private void initializeFoldersCache() throws LiveOperationException,
SkyDriveException, FileNotFoundException {
logDebug("skydrive initializeFoldersCache");
//use alias for now (overwritten later):
mRootFolderId = "me/skydrive";
LiveOperation operation = mConnectClient.get(mRootFolderId + "/files");
JSONObject result = operation.getResult();
checkResult(result);
mFolderCache.clear();
JSONArray data = result.optJSONArray(JsonKeys.DATA);
for (int i = 0; i < data.length(); i++) {
SkyDriveObject skyDriveObj = SkyDriveObject.create(data
.optJSONObject(i));
if (skyDriveObj == null)
continue; // ignored type
logDebug( "adding "+skyDriveObj.getName()+" to cache with id " + skyDriveObj.getId()+" in "+skyDriveObj.getParentId());
mFolderCache.put(skyDriveObj.getId(), skyDriveObj);
mRootFolderId = skyDriveObj.getParentId();
}
//check if we received anything. If not: query the root folder directly
if (data.length() == 0)
{
operation = mConnectClient.get(mRootFolderId);
result = operation.getResult();
checkResult(result);
mRootFolderId = SkyDriveObject.create(result).getId();
}
}
private void checkResult(JSONObject result) throws SkyDriveException, FileNotFoundException {
if (result.has(JsonKeys.ERROR)) {
JSONObject error = result.optJSONObject(JsonKeys.ERROR);
String message = error.optString(JsonKeys.MESSAGE);
String code = error.optString(JsonKeys.CODE);
logDebug( "Code: "+code);
if ("resource_not_found".equals(code))
throw new FileNotFoundException(message);
else
throw new SkyDriveException(message, code);
}
}
private SkyDriveObject tryAddToCache(String skyDriveId) {
try {
SkyDriveObject obj = getSkyDriveObject(skyDriveId);
if (obj != null) {
mFolderCache.put(obj.getId(), obj);
}
return obj;
} catch (Exception e) {
return null;
}
}
private SkyDriveObject getSkyDriveObject(SkyDrivePath skyDrivePath)
throws LiveOperationException, InvalidPathException,
UnsupportedEncodingException, SkyDriveException, FileNotFoundException {
String skyDriveID = skyDrivePath.getSkyDriveId();
return getSkyDriveObject(skyDriveID);
}
private SkyDriveObject getSkyDriveObject(String skyDriveID)
throws LiveOperationException, SkyDriveException,
FileNotFoundException {
LiveOperation operation = mConnectClient.get(skyDriveID);
JSONObject result = operation.getResult();
checkResult(result);
SkyDriveObject obj = SkyDriveObject.create(result);
return obj;
}
@Override
public boolean requiresSetup(String path) {
// always go through the setup process:
return true;
}
@Override
public void startSelectFile(FileStorageSetupInitiatorActivity activity,
boolean isForSave, int requestCode) {
((JavaFileStorage.FileStorageSetupInitiatorActivity) (activity))
.startSelectFileProcess(getProtocolId() + "://", isForSave,
requestCode);
}
@Override
public void prepareFileUsage(FileStorageSetupInitiatorActivity activity,
String path, int requestCode, boolean alwaysReturnSuccess) {
//tell the activity which requests the file usage that it must launch the FileStorageSetupActivity
// which will then go through the onCreate/onStart/onResume process which is used by our FileStorage
((JavaFileStorage.FileStorageSetupInitiatorActivity) (activity))
.startFileUsageProcess(path, requestCode, alwaysReturnSuccess);
}
@Override
public void prepareFileUsage(Context appContext, String path) throws Exception
{
PrepareFileUsageListener listener = new PrepareFileUsageListener();
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();
}
} else {
if (listener.status == LiveStatus.NOT_CONNECTED)
logDebug( "not connected");
else if (listener.status == LiveStatus.UNKNOWN)
logDebug( "unknown");
else
logDebug( "unexpected status " + listener.status);
throw new UserInteractionRequiredException();
}
}
@Override
public String getProtocolId() {
return "skydrive";
}
@Override
public String getDisplayName(String path) {
SkyDrivePath skydrivePath = new SkyDrivePath();
try {
skydrivePath.setPathWithoutVerify(path);
}
catch (Exception e)
{
e.printStackTrace();
return path;
}
return skydrivePath.getDisplayName();
}
@Override
public boolean checkForFileChangeFast(String path,
String previousFileVersion) throws Exception {
String currentVersion = getCurrentFileVersionFast(path);
if (currentVersion == null)
return false;
return currentVersion.equals(previousFileVersion) == false;
}
@Override
public String getCurrentFileVersionFast(String path) {
try
{
SkyDrivePath drivePath = new SkyDrivePath(path);
SkyDriveObject obj = getSkyDriveObject(drivePath);
if (obj == null)
return null;
return obj.getUpdatedTime();
}
catch (Exception e)
{
logDebug("Error getting file version.");
Log.w(TAG,"Error getting file version:");
e.printStackTrace();
return null;
}
}
@Override
public InputStream openFileForRead(String path) throws Exception {
try
{
logDebug("openFileForRead: " + path);
LiveDownloadOperation op = mConnectClient.download(new SkyDrivePath(path).getSkyDriveId()+"/content");
logDebug("openFileForRead ok" + path);
return op.getStream();
}
catch (Exception e)
{
throw convertException(e);
}
}
@Override
public void uploadFile(String path, byte[] data, boolean writeTransactional)
throws Exception {
try
{
logDebug("uploadFile to "+path);
SkyDrivePath driveTargetPath = new SkyDrivePath(path);
SkyDrivePath driveUploadPath = driveTargetPath;
SkyDrivePath driveTempPath = null;
ByteArrayInputStream bis = new ByteArrayInputStream(data);
//if writeTransactional, upload the file to a temp destination.
//this is a somewhat ugly way because it requires two uploads, but renaming/copying doesn't work
//nicely in SkyDrive, and SkyDrive doesn't provide file histories by itself, so we need to make sure
//no file gets corrupt if upload is canceled.
if (writeTransactional)
{
LiveOperation uploadOp = uploadFile(driveUploadPath.getParentPath(), driveUploadPath.getFilename()+".tmp", bis);
driveTempPath = new SkyDrivePath(driveUploadPath.getParentPath().getFullPath(), uploadOp.getResult());
//recreate ByteArrayInputStream for use in uploadFile below
bis = new ByteArrayInputStream(data);
}
//upload the file
uploadFile(driveUploadPath.getParentPath(), driveUploadPath.getFilename(), bis);
if (writeTransactional)
{
//delete old file
mConnectClient.delete(driveTempPath.getSkyDriveId());
// don't check result. If delete fails -> not a big deal
}
}
catch (Exception e)
{
throw convertException(e);
}
}
private LiveOperation uploadFile(SkyDrivePath parentPath, String filename, ByteArrayInputStream bis)
throws LiveOperationException, InvalidPathException,
UnsupportedEncodingException, FileNotFoundException,
SkyDriveException {
LiveOperation op = mConnectClient.upload(parentPath.getSkyDriveId(), filename, bis, OverwriteOption.Overwrite);
checkResult(op.getResult());
return op;
}
@Override
public String createFolder(String parentPath, String newDirName)
throws Exception {
try {
SkyDrivePath skyDriveParentPath = new SkyDrivePath(parentPath);
String parentId = skyDriveParentPath.getSkyDriveId();
JSONObject newFolder = new JSONObject();
newFolder.put("name", newDirName);
newFolder.put("description", "folder");
LiveOperation operation = mConnectClient.post(
parentId, newFolder);
JSONObject result = operation.getResult();
checkResult(result);
return new SkyDrivePath(parentPath, result).getFullPath();
} catch (Exception e) {
throw convertException(e);
}
}
private Exception convertException(Exception e) throws Exception {
e.printStackTrace();
logDebug(e.toString());
Log.w(TAG, e);
throw e;
}
@Override
public String createFilePath(String parentPath, String newFileName)
throws Exception {
try {
SkyDrivePath skyDriveParentPath = new SkyDrivePath(parentPath);
LiveOperation op = uploadFile(skyDriveParentPath, newFileName, new ByteArrayInputStream(new byte[0]));
checkResult(op.getResult());
return new SkyDrivePath(parentPath, op.getResult()).getFullPath();
} catch (Exception e) {
throw convertException(e);
}
}
@Override
public List<FileEntry> listFiles(String parentPath) throws Exception {
try
{
SkyDrivePath parentDrivePath = new SkyDrivePath(parentPath);
LiveOperation operation = mConnectClient.get(parentDrivePath.getSkyDriveId() + "/files");
JSONObject result = operation.getResult();
checkResult(result);
JSONArray data = result.optJSONArray(JsonKeys.DATA);
List<FileEntry> resultList = new ArrayList<FileEntry>(data.length());
for (int i = 0; i < data.length(); i++) {
SkyDriveObject skyDriveObj = SkyDriveObject.create(data
.optJSONObject(i));
if (skyDriveObj == null)
continue; // ignored type
logDebug( "listing "+skyDriveObj.getName()+" with id " + skyDriveObj.getId()+" in "+skyDriveObj.getParentId());
resultList.add(convertToFileEntry(parentDrivePath, skyDriveObj));
}
return resultList;
}
catch (Exception e)
{
throw convertException(e);
}
}
private FileEntry convertToFileEntry(SkyDrivePath parentPath, SkyDriveObject skyDriveObj) throws UnsupportedEncodingException, FileNotFoundException, IOException, InvalidPathException, JSONException, LiveOperationException, SkyDriveException {
FileEntry res = new FileEntry();
res.canRead = true;
res.canWrite = true;
res.displayName = skyDriveObj.getName();
res.isDirectory = SkyDriveFolder.class.isAssignableFrom(skyDriveObj.getClass());
try
{
res.lastModifiedTime = SKYDRIVE_DATEFORMATTER.parse(skyDriveObj.getUpdatedTime()).getTime();
}
catch (Exception e)
{
Log.w(TAG, "Cannot parse time " + skyDriveObj.getUpdatedTime());
logDebug("Cannot parse time! " + e.toString());
res.lastModifiedTime = -1;
}
if (parentPath == null) //this is the case if we're listing the parent path itself
res.path = getProtocolPrefix();
else
res.path = new SkyDrivePath(parentPath.getFullPath(), skyDriveObj.toJson()).getFullPath();
logDebug( "path: "+res.path);
if (SkyDriveFile.class.isAssignableFrom(skyDriveObj.getClass()))
{
res.sizeInBytes = ((SkyDriveFile)skyDriveObj).getSize();
}
return res;
}
@Override
public FileEntry getFileEntry(String filename) throws Exception {
try
{
SkyDrivePath drivePath = new SkyDrivePath(filename);
logDebug( "getFileEntry for "+ filename +" = "+drivePath.getFullPath());
logDebug( " parent is "+drivePath.getParentPath());
return convertToFileEntry(drivePath.getParentPath(),getSkyDriveObject(drivePath));
}
catch (Exception e)
{
throw convertException(e);
}
}
@Override
public void delete(String path) throws Exception {
try
{
SkyDrivePath drivePath = new SkyDrivePath(path);
LiveOperation op = mConnectClient.delete(drivePath.getSkyDriveId());
checkResult(op.getResult());
}
catch (Exception e)
{
throw convertException(e);
}
}
@Override
public void onCreate(FileStorageSetupActivity activity,
Bundle savedInstanceState) {
}
@Override
public void onResume(FileStorageSetupActivity activity) {
}
@Override
public void onStart(final FileStorageSetupActivity activity) {
try
{
logDebug("skydrive onStart");
initialize(activity);
}
catch (Exception e)
{
finishWithError(activity, e);
}
}
private void initialize(final FileStorageSetupActivity activity) {
mAuthClient.initialize(Arrays.asList(SCOPES), new LiveAuthListener() {
@Override
public void onAuthError(LiveAuthException exception,
Object userState) {
finishWithError(( activity), exception);
}
@Override
public void onAuthComplete(LiveStatus status,
LiveConnectSession session, Object userState) {
if (status == LiveStatus.CONNECTED) {
logDebug("connected!");
initialize(activity, session);
} else {
if (status == LiveStatus.NOT_CONNECTED)
logDebug( "not connected");
else if (status == LiveStatus.UNKNOWN)
logDebug( "unknown");
else
logDebug( "unexpected status " + status);
try
{
login(activity);
}
catch (IllegalStateException e)
{
//this may happen if an un-cancelled login progress is already in progress.
//however, the activity might have been destroyed, so try again with another auth client next time
logDebug("IllegalStateException: Recreating AuthClient");
mAuthClient = new LiveAuthClient(mAppContext, mClientId);
finishWithError(activity, e);
}
catch (Exception e)
{
finishWithError(activity, e);
}
}
}
});
}
@Override
public void onActivityResult(FileStorageSetupActivity activity,
int requestCode, int resultCode, Intent data) {
}
@Override
public String getFilename(String path) throws Exception {
SkyDrivePath p = new SkyDrivePath();
p.setPathWithoutVerify(path);
return p.getFilename();
}
}

View File

@ -0,0 +1,435 @@
package keepass2android.javafilestorage;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import com.burgstaller.okhttp.AuthenticationCacheInterceptor;
import com.burgstaller.okhttp.CachingAuthenticatorDecorator;
import com.burgstaller.okhttp.DispatchingAuthenticator;
import com.burgstaller.okhttp.basic.BasicAuthenticator;
import com.burgstaller.okhttp.digest.CachingAuthenticator;
import com.burgstaller.okhttp.digest.DigestAuthenticator;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
import keepass2android.javafilestorage.webdav.PropfindXmlParser;
import keepass2android.javafilestorage.webdav.WebDavUtil;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class WebDavStorage extends JavaFileStorageBase {
public String buildFullPath(String url, String username, String password) throws UnsupportedEncodingException {
String scheme = url.substring(0, url.indexOf("://"));
url = url.substring(scheme.length() + 3);
return scheme + "://" + encode(username)+":"+encode(password)+"@"+url;
}
static class ConnectionInfo {
String URL;
String username;
String password;
}
private ConnectionInfo splitStringToConnectionInfo(String filename)
throws UnsupportedEncodingException {
ConnectionInfo ci = new ConnectionInfo();
String scheme = filename.substring(0, filename.indexOf("://"));
filename = filename.substring(scheme.length() + 3);
String userPwd = filename.substring(0, filename.indexOf('@'));
ci.username = decode(userPwd.substring(0, userPwd.indexOf(":")));
ci.password = decode(userPwd.substring(userPwd.indexOf(":") + 1));
ci.URL = scheme + "://" +filename.substring(filename.indexOf('@') + 1);
return ci;
}
private static final String HTTP_PROTOCOL_ID = "http";
private static final String HTTPS_PROTOCOL_ID = "https";
@Override
public boolean checkForFileChangeFast(String path,
String previousFileVersion) throws Exception {
String currentVersion = getCurrentFileVersionFast(path);
if (currentVersion == null)
return false;
return currentVersion.equals(previousFileVersion) == false;
}
@Override
public String getCurrentFileVersionFast(String path) {
return null; // no simple way to get the version "fast"
}
@Override
public InputStream openFileForRead(String path) throws Exception {
try {
ConnectionInfo ci = splitStringToConnectionInfo(path);
Request request = new Request.Builder()
.url(new URL(ci.URL))
.method("GET", null)
.build();
Response response = getClient(ci).newCall(request).execute();
checkStatus(response);
return response.body().byteStream();
} catch (Exception e) {
throw convertException(e);
}
}
private OkHttpClient getClient(ConnectionInfo ci) {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
final Map<String, CachingAuthenticator> authCache = new ConcurrentHashMap<>();
com.burgstaller.okhttp.digest.Credentials credentials = new com.burgstaller.okhttp.digest.Credentials(ci.username, ci.password);
final BasicAuthenticator basicAuthenticator = new BasicAuthenticator(credentials);
final DigestAuthenticator digestAuthenticator = new DigestAuthenticator(credentials);
// note that all auth schemes should be registered as lowercase!
DispatchingAuthenticator authenticator = new DispatchingAuthenticator.Builder()
.with("digest", digestAuthenticator)
.with("basic", basicAuthenticator)
.build();
OkHttpClient client = builder
.authenticator(new CachingAuthenticatorDecorator(authenticator, authCache))
.addInterceptor(new AuthenticationCacheInterceptor(authCache))
.build();
return client;
}
@Override
public void uploadFile(String path, byte[] data, boolean writeTransactional)
throws Exception {
try {
ConnectionInfo ci = splitStringToConnectionInfo(path);
Request request = new Request.Builder()
.url(new URL(ci.URL))
.put(RequestBody.create(MediaType.parse("application/binary"), data))
.build();
//TODO consider writeTransactional
//TODO check for error
Response response = getClient(ci).newCall(request).execute();
checkStatus(response);
} catch (Exception e) {
throw convertException(e);
}
}
@Override
public String createFolder(String parentPath, String newDirName)
throws Exception {
try {
String newFolder = createFilePath(parentPath, newDirName);
ConnectionInfo ci = splitStringToConnectionInfo(newFolder);
Request request = new Request.Builder()
.url(new URL(ci.URL))
.method("MKCOL", null)
.build();
Response response = getClient(ci).newCall(request).execute();
checkStatus(response);
return newFolder;
} catch (Exception e) {
throw convertException(e);
}
}
private String concatPaths(String parentPath, String newDirName) {
String res = parentPath;
if (!res.endsWith("/"))
res += "/";
res += newDirName;
return res;
}
@Override
public String createFilePath(String parentPath, String newFileName)
throws Exception {
if (parentPath.endsWith("/") == false)
parentPath += "/";
return parentPath + newFileName;
}
public List<FileEntry> listFiles(String parentPath, int depth) throws Exception {
ArrayList<FileEntry> result = new ArrayList<>();
try {
if (parentPath.endsWith("/"))
parentPath = parentPath.substring(0,parentPath.length()-1);
ConnectionInfo ci = splitStringToConnectionInfo(parentPath);
String requestBody = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<d:propfind xmlns:d=\"DAV:\">\n" +
" <d:prop><d:displayname/><d:getlastmodified/><d:getcontentlength/></d:prop>\n" +
"</d:propfind>\n";
Log.d("WEBDAV", "starting query for " + ci.URL);
Request request = new Request.Builder()
.url(new URL(ci.URL))
.method("PROPFIND", RequestBody.create(MediaType.parse("application/xml"),requestBody))
.addHeader("Depth",String.valueOf(depth))
.build();
Response response = getClient(ci).newCall(request).execute();
checkStatus(response);
String xml = response.body().string();
PropfindXmlParser parser = new PropfindXmlParser();
List<PropfindXmlParser.Response> responses = parser.parse(new StringReader(xml));
for (PropfindXmlParser.Response r: responses)
{
PropfindXmlParser.Response.PropStat.Prop okprop =r.getOkProp();
if (okprop != null)
{
FileEntry e = new FileEntry();
e.canRead = e.canWrite = true;
Date lastMod = WebDavUtil.parseDate(okprop.LastModified);
if (lastMod != null)
e.lastModifiedTime = lastMod.getTime();
if (okprop.ContentLength != null)
{
try {
e.sizeInBytes = Integer.parseInt(okprop.ContentLength);
} catch (NumberFormatException exc) {
e.sizeInBytes = -1;
}
}
e.isDirectory = r.href.endsWith("/");
e.displayName = okprop.DisplayName;
if (e.displayName == null)
{
e.displayName = getDisplayName(r.href);
}
e.path = r.href;
if (e.path.indexOf("://") == -1)
{
//relative path:
e.path = buildPathFromHref(parentPath, r.href);
}
if ((depth == 1) && e.isDirectory)
{
String path = e.path;
if (!path.endsWith("/"))
path += "/";
String parentPathWithTrailingSlash = parentPath + "/";
//for depth==1 only list children, not directory itself
if (path.equals(parentPathWithTrailingSlash))
continue;
}
result.add(e);
}
}
return result;
} catch (Exception e) {
throw convertException(e);
}
}
private String buildPathFromHref(String parentPath, String href) throws UnsupportedEncodingException {
String scheme = parentPath.substring(0, parentPath.indexOf("://"));
String filename = parentPath.substring(scheme.length() + 3);
String userPwd = filename.substring(0, filename.indexOf('@'));
String username_enc = (userPwd.substring(0, userPwd.indexOf(":")));
String password_enc = (userPwd.substring(userPwd.indexOf(":") + 1));
String host = filename.substring(filename.indexOf('@')+1);
int firstSlashPos = host.indexOf("/");
if (firstSlashPos >= 0)
{
host = host.substring(0,firstSlashPos);
}
if (!href.startsWith("/"))
href = "/" + href;
return scheme + "://" + username_enc + ":" + password_enc + "@" + host + href;
}
@Override
public List<FileEntry> listFiles(String parentPath) throws Exception {
return listFiles(parentPath, 1);
}
private void checkStatus(Response response) throws Exception {
if((response.code() < 200)
|| (response.code() >= 300))
{
if (response.code() == 404)
throw new FileNotFoundException();
throw new Exception("Received unexpected response: " + response.toString());
}
}
private Exception convertException(Exception e) {
return e;
}
@Override
public FileEntry getFileEntry(String filename) throws Exception {
List<FileEntry> list = listFiles(filename,0);
if (list.size() != 1)
throw new FileNotFoundException();
return list.get(0);
}
@Override
public void delete(String path) throws Exception {
try {
ConnectionInfo ci = splitStringToConnectionInfo(path);
Request request = new Request.Builder()
.url(new URL(ci.URL))
.delete()
.build();
Response response = getClient(ci).newCall(request).execute();
checkStatus(response);
} catch (Exception e) {
throw convertException(e);
}
}
@Override
public void startSelectFile(
JavaFileStorage.FileStorageSetupInitiatorActivity activity,
boolean isForSave, int requestCode) {
activity.performManualFileSelect(isForSave, requestCode, getProtocolId());
}
@Override
protected String decode(String encodedString)
throws UnsupportedEncodingException {
return java.net.URLDecoder.decode(encodedString, UTF_8);
}
@Override
protected String encode(final String unencoded)
throws UnsupportedEncodingException {
return java.net.URLEncoder.encode(unencoded, UTF_8);
}
@Override
public void prepareFileUsage(JavaFileStorage.FileStorageSetupInitiatorActivity activity, String path, int requestCode, boolean alwaysReturnSuccess) {
Intent intent = new Intent();
intent.putExtra(EXTRA_PATH, path);
activity.onImmediateResult(requestCode, RESULT_FILEUSAGE_PREPARED, intent);
}
@Override
public String getProtocolId() {
return HTTPS_PROTOCOL_ID;
}
@Override
public void onResume(JavaFileStorage.FileStorageSetupActivity setupAct) {
}
@Override
public boolean requiresSetup(String path) {
return false;
}
@Override
public void onCreate(FileStorageSetupActivity activity,
Bundle savedInstanceState) {
}
@Override
public String getDisplayName(String path) {
if (path.endsWith("/"))
path = path.substring(0, path.length()-1);
int lastIndex = path.lastIndexOf("/");
if (lastIndex >= 0)
return path.substring(lastIndex + 1);
else
return path;
}
@Override
public String getFilename(String path) throws Exception {
if (path.endsWith("/"))
path = path.substring(0, path.length() - 1);
int lastIndex = path.lastIndexOf("/");
if (lastIndex >= 0)
return path.substring(lastIndex + 1);
else
return path;
}
@Override
public void onStart(FileStorageSetupActivity activity) {
}
@Override
public void onActivityResult(FileStorageSetupActivity activity,
int requestCode, int resultCode, Intent data) {
}
@Override
public void prepareFileUsage(Context appContext, String path) {
//nothing to do
}
}

View File

@ -0,0 +1,102 @@
package keepass2android.javafilestorage.onedrive;
/**
* Created by Philipp on 22.11.2016.
*/
import com.microsoft.services.msa.LiveConnectSession;
import com.onedrive.sdk.authentication.AccountType;
import com.onedrive.sdk.authentication.IAccountInfo;
import com.onedrive.sdk.authentication.MSAAccountInfo;
import com.onedrive.sdk.authentication.MSAAuthenticator;
import com.onedrive.sdk.logger.ILogger;
import com.microsoft.services.msa.LiveConnectSession;
import com.onedrive.sdk.logger.ILogger;
/**
* Account information for a MSA based account.
*/
public class MyMSAAccountInfo implements IAccountInfo {
/**
* The service root for the OneDrive personal API.
*/
public static final String ONE_DRIVE_PERSONAL_SERVICE_ROOT = "https://api.onedrive.com/v1.0";
/**
* The authenticator that can refresh this account.
*/
private final MyMSAAuthenticator mAuthenticator;
/**
* The session this account is based off of.
*/
private LiveConnectSession mSession;
/**
* The logger.
*/
private final ILogger mLogger;
/**
* Creates an MSAAccountInfo object.
* @param authenticator The authenticator that this account info was created from.
* @param liveConnectSession The session this account is based off of.
* @param logger The logger.
*/
public MyMSAAccountInfo(final MyMSAAuthenticator authenticator,
final LiveConnectSession liveConnectSession,
final ILogger logger) {
mAuthenticator = authenticator;
mSession = liveConnectSession;
mLogger = logger;
}
/**
* Get the type of the account.
* @return The MicrosoftAccount account type.
*/
@Override
public AccountType getAccountType() {
return AccountType.MicrosoftAccount;
}
/**
* Get the access token for requests against the service root.
* @return The access token for requests against the service root.
*/
@Override
public String getAccessToken() {
return mSession.getAccessToken();
}
/**
* Get the OneDrive service root for this account.
* @return the OneDrive service root for this account.
*/
@Override
public String getServiceRoot() {
return ONE_DRIVE_PERSONAL_SERVICE_ROOT;
}
/**
* Indicates if the account access token is expired and needs to be refreshed.
* @return true if refresh() needs to be called and
* false if the account is still valid.
*/
@Override
public boolean isExpired() {
return mSession.isExpired();
}
/**
* Refreshes the authentication token for this account info.
*/
@Override
public void refresh() {
mLogger.logDebug("Refreshing access token...");
final MyMSAAccountInfo newInfo = (MyMSAAccountInfo)mAuthenticator.loginSilent();
mSession = newInfo.mSession;
}
}

View File

@ -0,0 +1,437 @@
package keepass2android.javafilestorage.onedrive;
/**
* Created by Philipp on 22.11.2016.
*/
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import com.microsoft.onedrivesdk.BuildConfig;
import com.microsoft.services.msa.LiveAuthClient;
import com.microsoft.services.msa.LiveAuthException;
import com.microsoft.services.msa.LiveAuthListener;
import com.microsoft.services.msa.LiveConnectSession;
import com.microsoft.services.msa.LiveStatus;
import com.onedrive.sdk.authentication.ClientAuthenticatorException;
import com.onedrive.sdk.authentication.IAccountInfo;
import com.onedrive.sdk.authentication.IAuthenticator;
import com.onedrive.sdk.authentication.MSAAccountInfo;
import com.onedrive.sdk.concurrency.ICallback;
import com.onedrive.sdk.core.ClientException;
import com.onedrive.sdk.concurrency.SimpleWaiter;
import com.onedrive.sdk.concurrency.IExecutors;
import com.onedrive.sdk.core.OneDriveErrorCodes;
import com.onedrive.sdk.http.IHttpProvider;
import com.onedrive.sdk.logger.ILogger;
import java.security.InvalidParameterException;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicReference;
/**
* Wrapper around the MSA authentication library.
* https://github.com/MSOpenTech/msa-auth-for-android
*/
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
public abstract class MyMSAAuthenticator implements IAuthenticator {
/**
* The sign in cancellation message.
*/
private static final String SIGN_IN_CANCELLED_MESSAGE = "The user cancelled the login operation.";
/**
* The preferences for this authenticator.
*/
private static final String MSA_AUTHENTICATOR_PREFS = "MSAAuthenticatorPrefs";
/**
* The key for the user id.
*/
private static final String USER_ID_KEY = "userId";
/**
* The key for the version code
*/
public static final String VERSION_CODE_KEY = "versionCode";
/**
* The default user id
*/
private static final String DEFAULT_USER_ID = "@@defaultUser";
/**
* The active user id.
*/
private final AtomicReference<String> mUserId = new AtomicReference<>();
/**
* The executors.
*/
private IExecutors mExecutors;
/**
* Indicates whether this authenticator has been initialized.
*/
private boolean mInitialized;
/**
* The context UI interactions should happen with.
*/
private Activity mActivity;
/**
* The logger.
*/
private ILogger mLogger;
/**
* The client id for this authenticator.
* https://dev.onedrive.com/auth/msa_oauth.htm#to-register-your-app
* @return The client id.
*/
public abstract String getClientId();
/**
* The scopes for this application.
* https://dev.onedrive.com/auth/msa_oauth.htm#authentication-scopes
* @return The scopes for this application.
*/
public abstract String[] getScopes();
/**
* The live authentication client.
*/
private LiveAuthClient mAuthClient;
/**
* Initializes the authenticator.
* @param executors The executors to schedule foreground and background tasks.
* @param httpProvider The http provider for sending requests.
* @param activity The activity to create interactive UI on.
* @param logger The logger for diagnostic information.
*/
@Override
public synchronized void init(final IExecutors executors,
final IHttpProvider httpProvider,
final Activity activity,
final ILogger logger) {
if (mInitialized) {
return;
}
mExecutors = executors;
mActivity = activity;
mLogger = logger;
mInitialized = true;
mAuthClient = new LiveAuthClient(activity, getClientId(), Arrays.asList(getScopes()));
final SharedPreferences prefs = getSharedPreferences();
mUserId.set(prefs.getString(USER_ID_KEY, null));
}
/**
* Starts an interactive login asynchronously.
* @param emailAddressHint The hint for the email address during the interactive login.
* @param loginCallback The callback to be called when the login is complete.
*/
@Override
public void login(final String emailAddressHint, final ICallback<IAccountInfo> loginCallback) {
if (!mInitialized) {
throw new IllegalStateException("init must be called");
}
if (loginCallback == null) {
throw new InvalidParameterException("loginCallback");
}
mLogger.logDebug("Starting login async");
mExecutors.performOnBackground(new Runnable() {
@Override
public void run() {
try {
mExecutors.performOnForeground(login(emailAddressHint), loginCallback);
} catch (final ClientException e) {
mExecutors.performOnForeground(e, loginCallback);
}
}
});
}
/**
* Starts an interactive login.
* @param emailAddressHint The hint for the email address during the interactive login.
* @return The account info.
* @throws ClientException An exception occurs if the login was unable to complete for any reason.
*/
@Override
public synchronized IAccountInfo login(final String emailAddressHint) throws ClientException {
if (!mInitialized) {
throw new IllegalStateException("init must be called");
}
mLogger.logDebug("Starting login");
final AtomicReference<ClientException> error = new AtomicReference<>();
final SimpleWaiter waiter = new SimpleWaiter();
final LiveAuthListener listener = new LiveAuthListener() {
@Override
public void onAuthComplete(final LiveStatus liveStatus,
final LiveConnectSession liveConnectSession,
final Object o) {
if (liveStatus == LiveStatus.NOT_CONNECTED) {
mLogger.logDebug("Received invalid login failure from silent authentication with MSA, ignoring.");
} else {
mLogger.logDebug("Successful interactive login");
waiter.signal();
}
}
@Override
public void onAuthError(final LiveAuthException e,
final Object o) {
OneDriveErrorCodes code = OneDriveErrorCodes.AuthenticationFailure;
if (e.getError().equals(SIGN_IN_CANCELLED_MESSAGE)) {
code = OneDriveErrorCodes.AuthenticationCancelled;
}
error.set(new ClientAuthenticatorException("Unable to login with MSA", e, code));
mLogger.logError(error.get().getMessage(), error.get());
waiter.signal();
}
};
mActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
mAuthClient.login(mActivity, /* scopes */null, /* user object */ null, emailAddressHint, listener);
}
});
mLogger.logDebug("Waiting for MSA callback");
waiter.waitForSignal();
final ClientException exception = error.get();
if (exception != null) {
throw exception;
}
final String userId;
if (emailAddressHint != null) {
userId = emailAddressHint;
} else {
userId = DEFAULT_USER_ID;
}
mUserId.set(userId);
final SharedPreferences prefs = getSharedPreferences();
prefs.edit()
.putString(USER_ID_KEY, mUserId.get())
.putInt(VERSION_CODE_KEY, BuildConfig.VERSION_CODE)
.apply();
return getAccountInfo();
}
/**
* Starts a silent login asynchronously.
* @param loginCallback The callback to be called when the login is complete.
*/
@Override
public void loginSilent(final ICallback<IAccountInfo> loginCallback) {
if (!mInitialized) {
throw new IllegalStateException("init must be called");
}
if (loginCallback == null) {
throw new InvalidParameterException("loginCallback");
}
mLogger.logDebug("Starting login silent async");
mExecutors.performOnBackground(new Runnable() {
@Override
public void run() {
try {
mExecutors.performOnForeground(loginSilent(), loginCallback);
} catch (final ClientException e) {
mExecutors.performOnForeground(e, loginCallback);
}
}
});
}
/**
* Starts a silent login.
* @return The account info.
* @throws ClientException An exception occurs if the login was unable to complete for any reason.
*/
@Override
public synchronized IAccountInfo loginSilent() throws ClientException {
if (!mInitialized) {
throw new IllegalStateException("init must be called");
}
mLogger.logDebug("Starting login silent");
final int userIdStoredMinVersion = 10112;
if (getSharedPreferences().getInt(VERSION_CODE_KEY, 0) >= userIdStoredMinVersion
&& mUserId.get() == null) {
mLogger.logDebug("No login information found for silent authentication");
return null;
}
final SimpleWaiter loginSilentWaiter = new SimpleWaiter();
final AtomicReference<ClientException> error = new AtomicReference<>();
final boolean waitForCallback = mAuthClient.loginSilent(new LiveAuthListener() {
@Override
public void onAuthComplete(final LiveStatus liveStatus,
final LiveConnectSession liveConnectSession,
final Object o) {
if (liveStatus == LiveStatus.NOT_CONNECTED) {
error.set(new ClientAuthenticatorException("Failed silent login, interactive login required",
OneDriveErrorCodes.AuthenticationFailure));
mLogger.logError(error.get().getMessage(), error.get());
} else {
mLogger.logDebug("Successful silent login");
}
loginSilentWaiter.signal();
}
@Override
public void onAuthError(final LiveAuthException e,
final Object o) {
OneDriveErrorCodes code = OneDriveErrorCodes.AuthenticationFailure;
if (e.getError().equals(SIGN_IN_CANCELLED_MESSAGE)) {
code = OneDriveErrorCodes.AuthenticationCancelled;
}
error.set(new ClientAuthenticatorException("Login silent authentication error", e, code));
mLogger.logError(error.get().getMessage(), error.get());
loginSilentWaiter.signal();
}
});
if (!waitForCallback) {
mLogger.logDebug("MSA silent auth fast-failed");
return null;
}
mLogger.logDebug("Waiting for MSA callback");
loginSilentWaiter.waitForSignal();
final ClientException exception = error.get();
if (exception != null) {
throw exception;
}
return getAccountInfo();
}
/**
* Log the current user out.
* @param logoutCallback The callback to be called when the logout is complete.
*/
@Override
public void logout(final ICallback<Void> logoutCallback) {
if (!mInitialized) {
throw new IllegalStateException("init must be called");
}
if (logoutCallback == null) {
throw new InvalidParameterException("logoutCallback");
}
mLogger.logDebug("Starting logout async");
mExecutors.performOnBackground(new Runnable() {
@Override
public void run() {
try {
logout();
mExecutors.performOnForeground((Void) null, logoutCallback);
} catch (final ClientException e) {
mExecutors.performOnForeground(e, logoutCallback);
}
}
});
}
/**
* Log the current user out.
* @throws ClientException An exception occurs if the logout was unable to complete for any reason.
*/
@Override
public synchronized void logout() throws ClientException {
if (!mInitialized) {
throw new IllegalStateException("init must be called");
}
mLogger.logDebug("Starting logout");
final SimpleWaiter logoutWaiter = new SimpleWaiter();
final AtomicReference<ClientException> error = new AtomicReference<>();
mAuthClient.logout(new LiveAuthListener() {
@Override
public void onAuthComplete(final LiveStatus liveStatus,
final LiveConnectSession liveConnectSession,
final Object o) {
mLogger.logDebug("Logout completed");
logoutWaiter.signal();
}
@Override
public void onAuthError(final LiveAuthException e, final Object o) {
error.set(new ClientAuthenticatorException("MSA Logout failed",
e,
OneDriveErrorCodes.AuthenticationFailure));
mLogger.logError(error.get().getMessage(), error.get());
logoutWaiter.signal();
}
});
mLogger.logDebug("Waiting for logout to complete");
logoutWaiter.waitForSignal();
mLogger.logDebug("Clearing all MSA Authenticator shared preferences");
final SharedPreferences prefs = getSharedPreferences();
prefs.edit()
.clear()
.putInt(VERSION_CODE_KEY, BuildConfig.VERSION_CODE)
.apply();
mUserId.set(null);
final ClientException exception = error.get();
if (exception != null) {
throw exception;
}
}
/**
* Gets the current account info for this authenticator.
* @return NULL if no account is available.
*/
@Override
public IAccountInfo getAccountInfo() {
final LiveConnectSession session = mAuthClient.getSession();
if (session == null) {
return null;
}
return new MyMSAAccountInfo(this, session, mLogger);
}
/**
* Gets the shared preferences for this authenticator.
* @return The shared preferences.
*/
private SharedPreferences getSharedPreferences() {
return mActivity.getSharedPreferences(MSA_AUTHENTICATOR_PREFS, Context.MODE_PRIVATE);
}
}

View File

@ -1,37 +0,0 @@
package keepass2android.javafilestorage.skydrive;
import com.microsoft.live.LiveAuthException;
import com.microsoft.live.LiveAuthListener;
import com.microsoft.live.LiveConnectSession;
import com.microsoft.live.LiveStatus;
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)
{
status = _status;
session = _session;
done = true;
}
}

View File

@ -1,22 +0,0 @@
package keepass2android.javafilestorage.skydrive;
public class SkyDriveException extends Exception {
/**
*
*/
private static final long serialVersionUID = 4594684204315150764L;
private String mCode;
public SkyDriveException(String message, String code)
{
super(message);
mCode = code;
}
public String getCode() {
return mCode;
}
}

View File

@ -1,33 +0,0 @@
package keepass2android.javafilestorage.skydrive;
import org.json.JSONObject;
public class SkyDriveFile extends SkyDriveObject {
public static final String TYPENAME = "file";
public SkyDriveFile(JSONObject file) {
super(file);
}
public long getSize() {
return mObject.optLong("size");
}
public int getCommentsCount() {
return mObject.optInt("comments_count");
}
public boolean getCommentsEnabled() {
return mObject.optBoolean("comments_enabled");
}
public String getSource() {
return mObject.optString("source");
}
public boolean getIsEmbeddable() {
return mObject.optBoolean("is_embeddable");
}
}

View File

@ -1,15 +0,0 @@
package keepass2android.javafilestorage.skydrive;
import org.json.JSONObject;
public class SkyDriveFolder extends SkyDriveObject {
public static final String TYPENAME = "folder";
public SkyDriveFolder(JSONObject object) {
super(object);
}
public int getCount() {
return mObject.optInt("count");
}
}

View File

@ -1,114 +0,0 @@
package keepass2android.javafilestorage.skydrive;
import org.json.JSONObject;
public abstract class SkyDriveObject {
public static class From {
private final JSONObject mFrom;
public From(JSONObject from) {
assert from != null;
mFrom = from;
}
public String getName() {
return mFrom.optString("name");
}
public String getId() {
return mFrom.optString("id");
}
public JSONObject toJson() {
return mFrom;
}
}
public static class SharedWith {
private final JSONObject mSharedWidth;
public SharedWith(JSONObject sharedWith) {
assert sharedWith != null;
mSharedWidth = sharedWith;
}
public String getAccess() {
return mSharedWidth.optString("access");
}
public JSONObject toJson() {
return mSharedWidth;
}
}
public static SkyDriveObject create(JSONObject skyDriveObject) {
String type = skyDriveObject.optString("type");
if (type.equals(SkyDriveFolder.TYPENAME)) {
return new SkyDriveFolder(skyDriveObject);
} else if (type.equals(SkyDriveFile.TYPENAME)) {
return new SkyDriveFile(skyDriveObject);
} else return null;
}
protected final JSONObject mObject;
public SkyDriveObject(JSONObject object) {
assert object != null;
mObject = object;
}
public String getId() {
return mObject.optString("id");
}
public From getFrom() {
return new From(mObject.optJSONObject("from"));
}
public String getName() {
return mObject.optString("name");
}
public String getParentId() {
return mObject.optString("parent_id");
}
public String getDescription() {
return mObject.isNull("description") ? null : mObject.optString("description");
}
public String getType() {
return mObject.optString("type");
}
public String getLink() {
return mObject.optString("link");
}
public String getCreatedTime() {
return mObject.optString("created_time");
}
public String getUpdatedTime() {
return mObject.optString("updated_time");
}
public String getUploadLocation() {
return mObject.optString("upload_location");
}
public SharedWith getSharedWith() {
return new SharedWith(mObject.optJSONObject("shared_with"));
}
public JSONObject toJson() {
return mObject;
}
}

View File

@ -0,0 +1,230 @@
package keepass2android.javafilestorage.webdav;
import android.util.Xml;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.Reader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
/**
* Created by Philipp on 21.09.2016.
*/
public class PropfindXmlParser
{
private final String ns = "DAV:";
public static class Response
{
public Response()
{
propstat = new ArrayList<PropStat>();
}
public String href;
public URL getAbsoluteUri(URL requestUrl) throws MalformedURLException {
String serverUrl = requestUrl.getProtocol() + "://" + requestUrl.getHost();
if (requestUrl.getPort() > 0)
serverUrl += ":" + requestUrl.getPort();
return new URL(new URL(serverUrl),href);
}
public static class PropStat {
public PropStat()
{
prop = new Prop();
}
public boolean isOk()
{
if (status == null)
return false;
String[] parts = status.split(" ");
if (parts.length < 2)
return false;
return parts[1].equals("200");
}
public static class Prop {
public String DisplayName;
public String LastModified;
public String ContentLength;
}
public String status;
public Prop prop;
}
ArrayList<PropStat> propstat;
public PropStat.Prop getOkProp()
{
for (PropStat p: propstat)
{
if (p.isOk())
return p.prop;
}
return null;
}
}
public List<Response> parse(Reader in) throws XmlPullParserException, IOException {
List<Response> responses = new ArrayList<Response>();
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES,true);
parser.setInput(in);
parser.nextTag();
parser.require(XmlPullParser.START_TAG, ns, "multistatus");
while (parser.next() != XmlPullParser.END_TAG) {
android.util.Log.d("PARSE", "1eventtype=" + parser.getEventType());
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
String name = parser.getName();
android.util.Log.d("PARSE", "1name = " + name);
// Starts by looking for the entry tag
if (name.equals("response")) {
responses.add(readResponse(parser));
} else {
skip(parser);
}
}
return responses;
}
private Response readResponse(XmlPullParser parser) throws IOException, XmlPullParserException {
Response response = new Response();
parser.require(XmlPullParser.START_TAG, ns, "response");
android.util.Log.d("PARSE", "readResponse");
while (parser.next() != XmlPullParser.END_TAG) {
android.util.Log.d("PARSE", "2eventtype=" + parser.getEventType());
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
String name = parser.getName();
android.util.Log.d("PARSE", "2name=" + name);
if (name.equals("href")) {
response.href = readText(parser);
} else if (name.equals("propstat")) {
response.propstat.add(readPropStat(parser));
} else {
skip(parser);
}
}
return response;
}
// For the tags title and summary, extracts their text values.
private String readText(XmlPullParser parser) throws IOException, XmlPullParserException {
String result = "";
if (parser.next() == XmlPullParser.TEXT) {
result = parser.getText();
parser.nextTag();
}
android.util.Log.d("PARSE", "read text " + result);
return result;
}
private Response.PropStat readPropStat(XmlPullParser parser) throws IOException, XmlPullParserException {
Response.PropStat propstat = new Response.PropStat();
parser.require(XmlPullParser.START_TAG, ns, "propstat");
android.util.Log.d("PARSE", "readPropStat");
while (parser.next() != XmlPullParser.END_TAG) {
android.util.Log.d("PARSE", "3eventtype=" + parser.getEventType());
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
String name = parser.getName();
android.util.Log.d("PARSE", "3name=" + name);
if (name.equals("prop"))
{
propstat.prop = readProp(parser);
} else if (name.equals("status"))
{
propstat.status = readText(parser);
} else {
skip(parser);
}
}
return propstat;
}
private Response.PropStat.Prop readProp(XmlPullParser parser) throws IOException, XmlPullParserException {
Response.PropStat.Prop prop = new Response.PropStat.Prop();
parser.require(XmlPullParser.START_TAG, ns, "prop");
android.util.Log.d("PARSE", "readProp");
while (parser.next() != XmlPullParser.END_TAG) {
android.util.Log.d("PARSE", "eventtype=" + parser.getEventType());
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
String name = parser.getName();
android.util.Log.d("PARSE", "4name = " + name);
if (name.equals("getcontentlength"))
{
prop.ContentLength = readText(parser);
} else if (name.equals("getlastmodified")) {
prop.LastModified = readText(parser);
} else if (name.equals("displayname")) {
prop.DisplayName = readText(parser);
} else {
skip(parser);
}
}
return prop;
}
private void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
android.util.Log.d("PARSE", "skipping " + parser.getName());
if (parser.getEventType() != XmlPullParser.START_TAG) {
throw new IllegalStateException();
}
int depth = 1;
while (depth != 0) {
switch (parser.next()) {
case XmlPullParser.END_TAG:
depth--;
break;
case XmlPullParser.START_TAG:
depth++;
break;
}
}
}
}

View File

@ -0,0 +1,115 @@
package keepass2android.javafilestorage.webdav;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
public class WebDavUtil {
/**
* Date formats using for Date parsing.
*/
private static final List<ThreadLocal<SimpleDateFormat>> DATETIME_FORMATS = Arrays.asList(
new ThreadLocal<SimpleDateFormat>()
{
@Override
protected SimpleDateFormat initialValue()
{
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
format.setTimeZone(TimeZone.getTimeZone("UTC"));
return format;
}
},
new ThreadLocal<SimpleDateFormat>()
{
@Override
protected SimpleDateFormat initialValue()
{
SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
format.setTimeZone(TimeZone.getTimeZone("UTC"));
return format;
}
},
new ThreadLocal<SimpleDateFormat>()
{
@Override
protected SimpleDateFormat initialValue()
{
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.sss'Z'", Locale.US);
format.setTimeZone(TimeZone.getTimeZone("UTC"));
return format;
}
},
new ThreadLocal<SimpleDateFormat>()
{
@Override
protected SimpleDateFormat initialValue()
{
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US);
format.setTimeZone(TimeZone.getTimeZone("UTC"));
return format;
}
},
new ThreadLocal<SimpleDateFormat>()
{
@Override
protected SimpleDateFormat initialValue()
{
SimpleDateFormat format = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy", Locale.US);
format.setTimeZone(TimeZone.getTimeZone("UTC"));
return format;
}
},
new ThreadLocal<SimpleDateFormat>()
{
@Override
protected SimpleDateFormat initialValue()
{
SimpleDateFormat format = new SimpleDateFormat("EEEEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US);
format.setTimeZone(TimeZone.getTimeZone("UTC"));
return format;
}
},
new ThreadLocal<SimpleDateFormat>()
{
@Override
protected SimpleDateFormat initialValue()
{
SimpleDateFormat format = new SimpleDateFormat("EEE MMMM d HH:mm:ss yyyy", Locale.US);
format.setTimeZone(TimeZone.getTimeZone("UTC"));
return format;
}
}
);
/**
* Loops over all the possible date formats and tries to find the right one.
*
* @param value ISO date string
* @return Null if there is a parsing failure
*/
public static Date parseDate(String value)
{
if (value == null)
{
return null;
}
Date date = null;
for (ThreadLocal<SimpleDateFormat> format : DATETIME_FORMATS)
{
try
{
date = format.get().parse(value);
break;
}
catch (ParseException e)
{
// We loop through this until we found a valid one.
}
}
return date;
}
}

View File

@ -1,5 +1,3 @@
<resources>
<string name="app_name">JavaFileStorage</string>
</resources>

View File

@ -1 +1 @@
code
android-filechooser-AS

View File

@ -5,14 +5,7 @@
<GradleProjectSettings>
<option name="distributionType" value="LOCAL" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="C:\Program Files\Android\Android Studio\gradle\gradle-2.2.1" />
<option name="gradleJvm" value="1.7" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="gradleHome" value="C:\Program Files\Android\Android Studio1\gradle\gradle-2.14.1" />
</GradleProjectSettings>
</option>
</component>

View File

@ -1,11 +0,0 @@
<component name="libraryTable">
<library name="support-v4-18.0.0">
<CLASSES>
<root url="jar://$USER_HOME$/AppData/Local/Android/android-sdk/extras/android/m2repository/com/android/support/support-v4/18.0.0/support-v4-18.0.0.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="jar://$USER_HOME$/AppData/Local/Android/android-sdk/extras/android/m2repository/com/android/support/support-v4/18.0.0/support-v4-18.0.0-sources.jar!/" />
</SOURCES>
</library>
</component>

View File

@ -1,7 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EntryPointsManager">
<entry_points version="2.0" />
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
<option name="myNullables">
<value>
<list size="4">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="4">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
</list>
</value>
</option>
</component>
<component name="ProjectLevelVcsManager" settingsEditedManually="false">
<OptionsSetting value="true" id="Add" />

View File

@ -3,7 +3,6 @@
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/android-filechooser-AS.iml" filepath="$PROJECT_DIR$/android-filechooser-AS.iml" />
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
</modules>
</component>
</project>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="" />
</component>
</project>

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,16 @@
<option name="BUILDABLE" value="false" />
</configuration>
</facet>
<facet type="android-gradle" name="Android-Gradle">
<configuration>
<option name="GRADLE_PROJECT_PATH" value=":" />
</configuration>
</facet>
<facet type="android" name="Android">
<configuration>
<option name="ALLOW_USER_CONFIGURATION" value="false" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />

View File

@ -2,7 +2,7 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion 23
buildToolsVersion "23.0.0"
buildToolsVersion "23.0.2"
defaultConfig {
minSdkVersion 15
@ -11,12 +11,12 @@ android {
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-project.txt'
}
}
}
dependencies {
compile 'com.android.support:support-v4:18.0.0'
compile 'com.android.support:support-v4:23.0.0'
}

View File

@ -4,7 +4,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.2.3'
classpath 'com.android.tools.build:gradle:2.1.3'
}
}
allprojects {
@ -14,4 +14,7 @@ allprojects {
}
dependencies {
}
android {
buildToolsVersion '23.0.2'
}

View File

@ -1,6 +1,6 @@
#Wed Apr 10 15:27:10 PDT 2013
#Tue Sep 20 20:32:06 CEST 2016
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip

View File

@ -1,7 +1,11 @@
## This file must *NOT* be checked into Version Control Systems,
## This file is automatically generated by Android Studio.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
#
#Tue Mar 22 21:07:31 CET 2016
sdk.dir=C\:\\Users\\Philipp\\AppData\\Local\\Android\\android-sdk
# For customization when using a Version Control System, please read the
# header note.
#Tue Sep 20 20:29:56 CEST 2016
sdk.dir=C\:\\Users\\Philipp\\AppData\\Local\\Xamarin\\Universal\\AndroidSDK