diff --git a/.travis.yml b/.travis.yml index cb97c9a..607a3b0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,14 +6,10 @@ android: components: - tools - platform-tools - - build-tools-27.0.3 - - android-27 - - extra-android-m2repository - - extra-google-m2repository - licenses: - - android-sdk-license-.+ + - build-tools-28.0.3 + - android-28 before_install: - - yes | sdkmanager "platforms;android-27" + - yes | sdkmanager "platforms;android-28" script: - "./gradlew build check --daemon" after_failure: "cat $TRAVIS_BUILD_DIR/app/build/outputs/lint-results.xml" diff --git a/README.md b/README.md index 3884220..9a5b758 100755 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ approach that keeps the details of the OAuth process abstracted from the end-use This library leverages a few key libraries underneath to power the functionality: * [scribe-java](https://github.com/scribejava/scribejava) - Simple OAuth library for handling the authentication flow. - * [Android Async HTTP](https://github.com/loopj/android-async-http) - Simple asynchronous HTTP requests with JSON parsing. + * [Android Async HTTP](https://github.com/codepath/asynchttpclient) - Simple asynchronous HTTP requests with JSON parsing. ## Installation @@ -24,7 +24,7 @@ Next, add this line to your `app/build.gradle` file: ```gradle dependencies { - compile 'com.codepath.libraries:android-oauth-handler:1.2.5' + compile 'com.codepath.libraries:android-oauth-handler:2.1.3' } ``` @@ -51,12 +51,12 @@ public class TwitterClient extends OAuthBaseClient { public TwitterClient(Context context) { super(context, REST_API_INSTANCE, REST_URL, - REST_CONSUMER_KEY, REST_CONSUMER_SECRET, REST_CALLBACK_URL); + REST_CONSUMER_KEY, REST_CONSUMER_SECRET, null, REST_CALLBACK_URL); } // ENDPOINTS BELOW - public void getHomeTimeline(int page, AsyncHttpResponseHandler handler) { + public void getHomeTimeline(int page, JsonHttpResponseHandler handler) { String apiUrl = getApiUrl("statuses/home_timeline.json"); RequestParams params = new RequestParams(); params.put("page", String.valueOf(page)); @@ -169,9 +169,9 @@ with a `JsonHttpResponseHandler` handler: // SomeActivity.java RestClient client = RestClientApp.getRestClient(); client.getHomeTimeline(1, new JsonHttpResponseHandler() { - public void onSuccess(int statusCode, Header[] headers, JSONArray json) { + public void onSuccess(int statusCode, Headers headers, JSON json) { // Response is automatically parsed into a JSONArray - // json.getJSONObject(0).getLong("id"); + // json.jsonArray.getJSONObject(0).getLong("id"); } }); ``` @@ -180,27 +180,18 @@ Based on the JSON response (array or object), you need to declare the expected t ```java RestClient client = RestClientApp.getRestClient(); -client.get("http://www.google.com", new AsyncHttpResponseHandler() { +client.get("http://www.google.com", new JsonHttpResponseHandler() { @Override - public void onSuccess(int statusCode, Header[] headers, String response) { + public void onSuccess(int statusCode, Headers headers, String response) { System.out.println(response); } }); ``` -Check out [Android Async HTTP Docs](http://loopj.com/android-async-http/) for more request creation details. +Check out [Android Async HTTP Docs](https://github.com/codepath/asynchttpclient) for more request creation details. ## Extra Functionality -### Adding Request Headers - -In certain cases, requests will require a particular custom header to be passed through the client. In this case, you can add custom headers to the client that will be added to all requests with: - -```java -RestClient client = RestApplication.getRestClient(); -// Specify the header to append to the request -client.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1)"); -``` ### Access Authorization @@ -226,40 +217,32 @@ This can be helpful in cases where you must add a flag such as when encountering You can log out by clearing the access token at any time through the client object: -You can log out by clearing the access token at any time through the client object: - ```java RestClient client = RestApplication.getRestClient(); client.clearAccessToken(); ``` -### Enabling a Proxy +### Debugging -In order to [troubleshoot API calls](http://guides.codepath.com/android/Troubleshooting-API-calls) using a method such as Charles Proxy, you'll want to enable proxy support with: +In order to [troubleshoot API calls](http://guides.codepath.com/android/Troubleshooting-API-calls), you can take advantage of the Stetho library: +Next, initialize Stetho inside your Application object: ```java -RestClient client = RestApplication.getRestClient(); -client.enableProxy(); +public class MyApplication extends Application { + public void onCreate() { + super.onCreate(); + Stetho.initializeWithDefaults(this); + } +} ``` -Proxies are useful for monitoring the network traffic but require a custom SSL certificate to be added to your emulator or device. Because Android API 24 and above now require [explicit control](https://developer.android.com/training/articles/security-config.html) on custom SSL certificates that are used in apps, you will need to allow for added certs to be added by specifying `res/xml/network_security_config.xml` in your app: - +Edit the manifest.xml file in your project. To let the Android operating system know that you have a custom Application class, add an attribute called `android:name` to the manifest’s application tag and set the value to the name of your custom Application class. ```xml - - - - - - - - - + ``` -Inside your AndroidManifest.xml file, make sure to include this `networkSecurityConfig` parameter: - -```xml - oauthParameters; - - public ScribeRequestAdapter( - cz.msebera.android.httpclient.client.methods.HttpUriRequest httpUriRequest) { - - super(getMethod(httpUriRequest.getMethod()), httpUriRequest.getURI().toString()); - - this.httpUriRequest = httpUriRequest; - this.oauthParameters = new HashMap(); - } - - public static Verb getMethod(String method) { - switch (method) { - case "GET": - return Verb.GET; - case "POST": - return Verb.POST; - case "DELETE": - return Verb.DELETE; - case "PUT": - return Verb.PUT; - case "PATCH": - return Verb.PATCH; - case "OPTIONS": - return Verb.OPTIONS; - } - throw new IllegalStateException(method); - } - // Adds OAuth parameter with associated value - @Override - public void addOAuthParameter(String key, String value) { - this.oauthParameters.put(key, value); - } - - // Returns OAuth parameters - @Override - public Map getOauthParameters() { - return this.oauthParameters; - } - - // Adds header entry with associated value - @Override - public void addHeader(String key, String value) { - this.httpUriRequest.addHeader(key, value); - } - - // Add query string parameters to the HTTP Request. - @Override - public void addQuerystringParameter(String key, String value) { - // Workaround since some OAuth2 require "access_token" and others "oauth_token" - if (key.equals(OAuthConstants.ACCESS_TOKEN)) { addQuerystringParameter(OAuthConstants.TOKEN, value); } - // Workaround, convert URI to Uri, build on the URL to add the new query parameter and then update the HTTP Request - Uri updatedUri = Uri.parse(httpUriRequest.getURI().toString()).buildUpon().appendQueryParameter(key, value).build(); - ((HttpRequestBase) httpUriRequest).setURI(URI.create(updatedUri.toString())); - } - - // Returns query strings embedded in the URL - @Override - public ParameterList getQueryStringParams() { - try { - return parseQueryParams(); - } catch (UnsupportedEncodingException e) { - return new ParameterList(); - } - } - - // Returns params parsed from the entity body - @Override - public ParameterList getBodyParams() { - if (getVerb() == Verb.GET || getVerb() == Verb.DELETE) { return new ParameterList(); } - else { return parseEntityParams(); } - } - - // Returns the full URL with query strings - @Override - public String getCompleteUrl() { - return getHttpRequest().getURI().toString(); - } - - // Returns the base URL without query strings or host - @Override - public String getSanitizedUrl() { - return getCompleteUrl().replaceAll("\\?.*", "").replace("\\:\\d{4}", ""); - } - - // Returns Verb enum for the request method (i.e Verb.GET) - @Override - public Verb getVerb() { - return Verb.valueOf(getHttpRequest().getMethod()); - } - - // Returns simple string representation of the request - // i.e @Request(GET http://foo.com/bar) - @Override - public String toString() - { - return String.format("@Request(%s %s)", getVerb(), getCompleteUrl()); - } - - // Returns the underlying HTTP request - protected HttpUriRequest getHttpRequest() { - return this.httpUriRequest; - } - - // Parses and returns the entity provided as a ParameterList - private ParameterList parseEntityParams() { - cz.msebera.android.httpclient.HttpEntity entity = null; - List parameters = null; - try{ - entity = ((HttpEntityEnclosingRequestBase) httpUriRequest).getEntity(); - parameters = new ArrayList( URLEncodedUtils.parse(entity)); - } catch (Exception e) { - return new ParameterList(); - } - - ParameterList list = new ParameterList(); - for (NameValuePair pair : parameters) { - list.add(pair.getName(), pair.getValue()); - } - return list; - } - - // Returns the ParameterList of query parameters parsed from the URL string - private ParameterList parseQueryParams() throws UnsupportedEncodingException { - ParameterList params = new ParameterList(); - String queryString = URI.create(getCompleteUrl()).getQuery(); - if (queryString == null) { return params; } - for (String param : queryString.split("&")) { - String pair[] = param.split("="); - String key = URLDecoder.decode(pair[0], "UTF-8"); - String value = ""; - if (pair.length > 1) { - value = URLDecoder.decode(pair[1], "UTF-8"); - } - params.add(new String(key), new String(value)); - } - return params; - } - - @Override - public String getRealm() { - return null; - } - -} diff --git a/build.gradle b/build.gradle index 42d6ed0..4055c94 100644 --- a/build.gradle +++ b/build.gradle @@ -1,31 +1,18 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { - jcenter() google() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.0.1' + classpath 'com.android.tools.build:gradle:4.0.1' } } -plugins { - id "com.jfrog.bintray" version "1.7.3" -} - -plugins { - id "com.github.dcendents.android-maven" version "2.0" -} - -bintray { - publications = [] - configurations = [] -} - allprojects { repositories { - jcenter() google() + jcenter() } } /* diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..199d16e --- /dev/null +++ b/gradle.properties @@ -0,0 +1,20 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d19e467..dfb6cb5 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sat Mar 31 23:33:37 PDT 2018 +#Sun Aug 11 22:16:42 PDT 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip diff --git a/library/build.gradle b/library/build.gradle new file mode 100644 index 0000000..c25a507 --- /dev/null +++ b/library/build.gradle @@ -0,0 +1,144 @@ +apply plugin: 'com.android.library' +apply plugin: 'maven-publish' +apply plugin: 'signing' + +ext { + + GROUP = 'com.codepath.libraries' + BASE_VERSION = "2.3" + VERSION_NAME = "2.3.0" + POM_PACKAGING = "aar" + POM_DESCRIPTION = "CodePath OAuth Handler" + + POM_ARTIFACT_ID = "android-oauth-handler" + POM_NAME = "CodePath OAuth Handler" + POM_URL = "https://github.com/codepath/android-oauth-handler/" + POM_SCM_URL = "https://github.com/codepath/android-oauth-handler/" + POM_SCM_CONNECTION = "scm:git:https://github.com/codepath/android-oauth-handler.git" + POM_SCM_DEV_CONNECTION = "scm:git:git@github.com:codepath/android-oauth-handler.git" + + POM_LICENCE_NAME = "The Apache Software License, Version 2.0" + POM_LICENCE_URL = "http://www.apache.org/licenses/LICENSE-2.0.txt" + POM_LICENCE_DIST = "repo" + + POM_DEVELOPER_ID = "codepath" + POM_DEVELOPER_NAME = "CodePath, Inc." +} + +version = VERSION_NAME +group = GROUP +archivesBaseName = POM_ARTIFACT_ID + +android { + compileSdkVersion 30 + + defaultConfig { + versionCode 1 + versionName VERSION_NAME + minSdkVersion 21 + targetSdkVersion 30 + } + + // Related to https://github.com/scribejava/scribejava/issues/480 + // Scribe expects Java 7 or this custom Apache library + lintOptions { + lintConfig rootProject.file('gradle/lint.xml') + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + } + } +} + +afterEvaluate { + publishing { + publications { + // Creates a Maven publication called "release". + release(MavenPublication) { + // Applies the component for the release build variant. + from components.release + + // You can then customize attributes of the publication as shown below. + groupId = GROUP + artifactId = POM_ARTIFACT_ID + version = VERSION_NAME + + pom { + name = POM_NAME + url = POM_URL + description = POM_DESCRIPTION + licenses { + license { + name = POM_LICENCE_NAME + url = POM_LICENCE_URL + } + } + developers { + developer { + id = POM_DEVELOPER_ID + name = POM_DEVELOPER_NAME + } + } + scm { + url = POM_SCM_URL + } + } + } + } + repositories { + maven { + name = "Sonatype" + credentials { + username = System.getenv('NEXUS_USERNAME') + password = System.getenv('NEXUS_PASSWORD') + } + def releasesRepoUrl = 'https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/' + def snapshotsRepoUrl = 'https://s01.oss.sonatype.org/content/repositories/snapshots/' + url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl + setUrl(url) + } + } + } +} + +signing { + // gpg on MacOS is the same as gpg2 + // ln -s /usr/local/bin/gpg /usr/local/bin/gpg2 + // Make sure to populate the variables in gradle.properties + // signing.gnupg.keyName=XXX + // signing.gnupg.passpharse + useGpgCmd() + sign(publishing.publications) +} + +task sourcesJar(type: Jar) { + from android.sourceSets.main.java.srcDirs + archiveClassifier.set("sources") +} + +artifacts { + archives sourcesJar +} + +ext { + supportVersion = '28.0.0' +} + +dependencies { + api "androidx.appcompat:appcompat:1.3.0" + api 'com.codepath.libraries:asynchttpclient:2.1.1' + api 'com.github.scribejava:scribejava-apis:4.1.1' + api 'com.github.scribejava:scribejava-httpclient-okhttp:4.1.1' + implementation 'se.akerfeldt:okhttp-signpost:1.1.0' + implementation 'oauth.signpost:signpost-core:1.2.1.2' + implementation 'com.facebook.stetho:stetho:1.5.1' + implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1' + implementation "com.squareup.okhttp3:logging-interceptor:4.7.2" + +} + +task jar(type: Jar) { + from android.sourceSets.main.java.srcDirs +} diff --git a/app/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml similarity index 100% rename from app/src/main/AndroidManifest.xml rename to library/src/main/AndroidManifest.xml diff --git a/library/src/main/java/com/codepath/oauth/OAuthAsyncHttpClient.java b/library/src/main/java/com/codepath/oauth/OAuthAsyncHttpClient.java new file mode 100644 index 0000000..b9dd7a4 --- /dev/null +++ b/library/src/main/java/com/codepath/oauth/OAuthAsyncHttpClient.java @@ -0,0 +1,69 @@ +package com.codepath.oauth; + +import com.codepath.asynchttpclient.AsyncHttpClient; +import com.facebook.stetho.okhttp3.StethoInterceptor; +import com.github.scribejava.core.model.OAuth1AccessToken; +import com.github.scribejava.core.model.OAuth2AccessToken; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; + +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.logging.HttpLoggingInterceptor; +import se.akerfeldt.okhttp.signpost.OkHttpOAuthConsumer; +import se.akerfeldt.okhttp.signpost.SigningInterceptor; + +public class OAuthAsyncHttpClient extends AsyncHttpClient { + + protected OAuthAsyncHttpClient(OkHttpClient httpClient) { + super(httpClient); + } + + private static String BEARER = "Bearer"; + + public static HttpLoggingInterceptor createLogger() { + HttpLoggingInterceptor logger = new HttpLoggingInterceptor(); + logger.level(HttpLoggingInterceptor.Level.HEADERS); + return logger; + } + + public static OAuthAsyncHttpClient create(String consumerKey, String consumerSecret, OAuth1AccessToken token) { + OkHttpOAuthConsumer consumer = new OkHttpOAuthConsumer(consumerKey, consumerSecret); + HttpLoggingInterceptor logging = createLogger(); + + consumer.setTokenWithSecret(token.getToken(), token.getTokenSecret()); + OkHttpClient httpClient = new OkHttpClient.Builder() + .addInterceptor(logging) + .addNetworkInterceptor(new StethoInterceptor()) + .addInterceptor(new SigningInterceptor(consumer)).build(); + + OAuthAsyncHttpClient asyncHttpClient = new OAuthAsyncHttpClient(httpClient); + return asyncHttpClient; + } + + public static OAuthAsyncHttpClient create(final OAuth2AccessToken token) { + final String bearer = String.format("%s %s", BEARER, token.getAccessToken()); + + HttpLoggingInterceptor logging = createLogger(); + + OkHttpClient httpClient = new OkHttpClient.Builder() + .addInterceptor(logging) + .addNetworkInterceptor(new StethoInterceptor()) + .addInterceptor(new Interceptor() { + @NotNull + @Override + public Response intercept(@NotNull Chain chain) throws IOException { + Request originalRequest = chain.request(); + Request authedRequest = originalRequest.newBuilder().header("Authorization", bearer).build(); + return chain.proceed(authedRequest); + } + }).build(); + + OAuthAsyncHttpClient asyncHttpClient = new OAuthAsyncHttpClient(httpClient); + return asyncHttpClient; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/codepath/oauth/OAuthBaseClient.java b/library/src/main/java/com/codepath/oauth/OAuthBaseClient.java similarity index 74% rename from app/src/main/java/com/codepath/oauth/OAuthBaseClient.java rename to library/src/main/java/com/codepath/oauth/OAuthBaseClient.java index 1db4680..038a76e 100755 --- a/app/src/main/java/com/codepath/oauth/OAuthBaseClient.java +++ b/library/src/main/java/com/codepath/oauth/OAuthBaseClient.java @@ -5,6 +5,8 @@ import android.content.SharedPreferences; import android.net.Uri; +import androidx.annotation.Nullable; + import com.github.scribejava.core.builder.api.BaseApi; import com.github.scribejava.core.model.OAuth1AccessToken; import com.github.scribejava.core.model.OAuth1RequestToken; @@ -17,6 +19,7 @@ public abstract class OAuthBaseClient { protected String baseUrl; protected Context context; + protected OAuthTokenClient tokenClient; protected OAuthAsyncHttpClient client; protected SharedPreferences prefs; protected SharedPreferences.Editor editor; @@ -45,11 +48,11 @@ public static OAuthBaseClient getInstance(Class klass return instance; } - public OAuthBaseClient(Context c, BaseApi apiInstance, String consumerUrl, String consumerKey, String consumerSecret, String callbackUrl) { + public OAuthBaseClient(Context c, final BaseApi apiInstance, String consumerUrl, final String consumerKey, final String consumerSecret, @Nullable String scope, String callbackUrl) { this.baseUrl = consumerUrl; this.callbackUrl = callbackUrl; - client = new OAuthAsyncHttpClient(apiInstance, consumerKey, - consumerSecret, callbackUrl, new OAuthAsyncHttpClient.OAuthTokenHandler() { + tokenClient = new OAuthTokenClient(apiInstance, consumerKey, + consumerSecret, callbackUrl, scope, new OAuthTokenClient.OAuthTokenHandler() { // Store request token and launch the authorization URL in the browser @Override @@ -59,6 +62,7 @@ public void onReceivedRequestToken(Token requestToken, String authorizeUrl, Stri OAuth1RequestToken oAuth1RequestToken = (OAuth1RequestToken) requestToken; editor.putString(OAUTH1_REQUEST_TOKEN, oAuth1RequestToken.getToken()); editor.putString(OAUTH1_REQUEST_TOKEN_SECRET, oAuth1RequestToken.getTokenSecret()); + editor.putInt(OAuthConstants.VERSION, 1); editor.commit(); } } @@ -70,21 +74,23 @@ public void onReceivedRequestToken(Token requestToken, String authorizeUrl, Stri OAuthBaseClient.this.context.startActivity(intent); } - // Store the access token in preferences, set the token in the client and fire the success callback + // Store the access token in preferences, set the token in the tokenClient and fire the success callback @Override public void onReceivedAccessToken(Token accessToken, String oAuthVersion) { if (oAuthVersion == OAUTH1_VERSION) { OAuth1AccessToken oAuth1AccessToken = (OAuth1AccessToken) accessToken; - client.setAccessToken(accessToken); + tokenClient.setAccessToken(accessToken); + instantiateClient(consumerKey, consumerSecret, oAuth1AccessToken); editor.putString(OAuthConstants.TOKEN, oAuth1AccessToken.getToken()); editor.putString(OAuthConstants.TOKEN_SECRET, oAuth1AccessToken.getTokenSecret()); editor.putInt(OAuthConstants.VERSION, 1); editor.commit(); } else if (oAuthVersion == OAUTH2_VERSION) { OAuth2AccessToken oAuth2AccessToken = (OAuth2AccessToken) accessToken; - client.setAccessToken(accessToken); + instantiateClient(consumerKey, consumerSecret, oAuth2AccessToken); + tokenClient.setAccessToken(accessToken); editor.putString(OAuthConstants.TOKEN, oAuth2AccessToken.getAccessToken()); editor.putString(OAuthConstants.SCOPE, oAuth2AccessToken.getScope()); editor.putString(OAuthConstants.REFRESH_TOKEN, oAuth2AccessToken.getRefreshToken()); @@ -106,16 +112,29 @@ public void onFailure(Exception e) { // Store preferences namespaced by the class and consumer key used this.prefs = this.context.getSharedPreferences("OAuth_" + apiInstance.getClass().getSimpleName() + "_" + consumerKey, 0); this.editor = this.prefs.edit(); - // Set access token in the client if already stored in preferences - if (this.checkAccessToken() != null) { - client.setAccessToken(this.checkAccessToken()); + // Set access token in the tokenClient if already stored in preferences + Token accessToken = this.checkAccessToken(); + if (accessToken != null) { + tokenClient.setAccessToken(accessToken); + instantiateClient(consumerKey, consumerSecret, accessToken); } } + public void instantiateClient(String consumerKey, String consumerSecret, Token token) { + + if (token instanceof OAuth1AccessToken) { + client = OAuthAsyncHttpClient.create(consumerKey, consumerSecret, (OAuth1AccessToken)(token)); + } else if (token instanceof OAuth2AccessToken){ + client = OAuthAsyncHttpClient.create((OAuth2AccessToken) token); + } else { + throw new IllegalStateException("unrecognized token type" + token); + } + + } // Fetches a request token and retrieve and authorization url // Should open a browser in onReceivedRequestToken once the url has been received public void connect() { - client.fetchRequestToken(); + tokenClient.fetchRequestToken(); } // Retrieves access token given authorization url @@ -123,7 +142,7 @@ public void authorize(Uri uri, OAuthAccessHandler handler) { this.accessHandler = handler; if (checkAccessToken() == null && uri != null) { // TODO: check UriServiceCallback with intent:// scheme - client.fetchAccessToken(getOAuth1RequestToken(), uri); + tokenClient.fetchAccessToken(getOAuth1RequestToken(), uri); } else if (checkAccessToken() != null) { // already have access token this.accessHandler.onLoginSuccess(); @@ -143,14 +162,19 @@ public Token checkAccessToken() { return null; } - protected OAuthAsyncHttpClient getClient() { - return client; + protected OAuthTokenClient getTokenClient() { + return tokenClient; } - // Returns the request token stored during the request token phase - protected OAuth1RequestToken getOAuth1RequestToken() { - return new OAuth1RequestToken(prefs.getString(OAUTH1_REQUEST_TOKEN, ""), - prefs.getString(OAUTH1_REQUEST_TOKEN_SECRET, "")); + // Returns the request token stored during the request token phase (OAuth1 only) + protected @Nullable Token getOAuth1RequestToken() { + int oAuthVersion = prefs.getInt(OAuthConstants.VERSION, 0); + + if (oAuthVersion == 1) { + return new OAuth1RequestToken(prefs.getString(OAUTH1_REQUEST_TOKEN, ""), + prefs.getString(OAUTH1_REQUEST_TOKEN_SECRET, "")); + } + return null; } // Assigns the base url for the API @@ -165,7 +189,7 @@ protected String getApiUrl(String path) { // Removes the access tokens (for signing out) public void clearAccessToken() { - client.setAccessToken(null); + tokenClient.setAccessToken(null); editor.remove(OAuthConstants.TOKEN); editor.remove(OAuthConstants.TOKEN_SECRET); editor.remove(OAuthConstants.REFRESH_TOKEN); @@ -173,9 +197,9 @@ public void clearAccessToken() { editor.commit(); } - // Returns true if the client is authenticated; false otherwise. + // Returns true if the tokenClient is authenticated; false otherwise. public boolean isAuthenticated() { - return client.getAccessToken() != null; + return tokenClient.getAccessToken() != null; } // Sets the flags used when launching browser to authenticate through OAuth @@ -189,12 +213,4 @@ public static interface OAuthAccessHandler { public void onLoginFailure(Exception e); } - - public void enableProxy() { - client.setProxy(System.getProperty("http.proxyHost"), Integer.parseInt(System.getProperty("http.proxyPort"))); - } - - public void addHeader(String headerName, String headerValue) { - client.addHeader(headerName, headerValue); - } } diff --git a/app/src/main/java/com/codepath/oauth/OAuthLoginActionBarActivity.java b/library/src/main/java/com/codepath/oauth/OAuthLoginActionBarActivity.java similarity index 97% rename from app/src/main/java/com/codepath/oauth/OAuthLoginActionBarActivity.java rename to library/src/main/java/com/codepath/oauth/OAuthLoginActionBarActivity.java index beea737..def658f 100755 --- a/app/src/main/java/com/codepath/oauth/OAuthLoginActionBarActivity.java +++ b/library/src/main/java/com/codepath/oauth/OAuthLoginActionBarActivity.java @@ -1,10 +1,12 @@ package com.codepath.oauth; -import com.codepath.utils.GenericsUtil; - import android.content.Intent; import android.net.Uri; -import android.support.v7.app.AppCompatActivity; + +import androidx.appcompat.app.AppCompatActivity; + +import com.codepath.utils.GenericsUtil; + // This is the ActionBarActivity supportv7 version of LoginActivity public abstract class OAuthLoginActionBarActivity extends diff --git a/app/src/main/java/com/codepath/oauth/OAuthLoginActivity.java b/library/src/main/java/com/codepath/oauth/OAuthLoginActivity.java similarity index 97% rename from app/src/main/java/com/codepath/oauth/OAuthLoginActivity.java rename to library/src/main/java/com/codepath/oauth/OAuthLoginActivity.java index 53d4a8a..64fa354 100755 --- a/app/src/main/java/com/codepath/oauth/OAuthLoginActivity.java +++ b/library/src/main/java/com/codepath/oauth/OAuthLoginActivity.java @@ -2,7 +2,8 @@ import android.content.Intent; import android.net.Uri; -import android.support.v4.app.FragmentActivity; + +import androidx.fragment.app.FragmentActivity; import com.codepath.utils.GenericsUtil; diff --git a/app/src/main/java/com/codepath/oauth/OAuthLoginFragment.java b/library/src/main/java/com/codepath/oauth/OAuthLoginFragment.java similarity index 96% rename from app/src/main/java/com/codepath/oauth/OAuthLoginFragment.java rename to library/src/main/java/com/codepath/oauth/OAuthLoginFragment.java index 10645d2..9fe5c72 100755 --- a/app/src/main/java/com/codepath/oauth/OAuthLoginFragment.java +++ b/library/src/main/java/com/codepath/oauth/OAuthLoginFragment.java @@ -2,7 +2,8 @@ import android.net.Uri; import android.os.Bundle; -import android.support.v4.app.Fragment; + +import androidx.fragment.app.Fragment; import com.codepath.utils.GenericsUtil; diff --git a/library/src/main/java/com/codepath/oauth/OAuthTokenClient.java b/library/src/main/java/com/codepath/oauth/OAuthTokenClient.java new file mode 100755 index 0000000..d76079d --- /dev/null +++ b/library/src/main/java/com/codepath/oauth/OAuthTokenClient.java @@ -0,0 +1,158 @@ +package com.codepath.oauth; + +import android.net.Uri; + +import com.github.scribejava.core.builder.ServiceBuilder; +import com.github.scribejava.core.builder.api.BaseApi; +import com.github.scribejava.core.exceptions.OAuthException; +import com.github.scribejava.core.model.OAuth1AccessToken; +import com.github.scribejava.core.model.OAuth1RequestToken; +import com.github.scribejava.core.model.OAuth2AccessToken; +import com.github.scribejava.core.model.OAuthAsyncRequestCallback; +import com.github.scribejava.core.model.OAuthConstants; +import com.github.scribejava.core.model.Token; +import com.github.scribejava.core.oauth.OAuth10aService; +import com.github.scribejava.core.oauth.OAuth20Service; +import com.github.scribejava.core.oauth.OAuthService; +import com.github.scribejava.httpclient.okhttp.OkHttpHttpClientConfig; + +/* + * OAuthTokenClient is responsible for managing the request and access token exchanges and then + * signing all requests with the OAuth signature after access token has been retrieved and stored. + * The client is based on AsyncHttpClient for async http requests and uses Scribe to manage the OAuth authentication. + */ +public class OAuthTokenClient { + + private BaseApi apiInstance; + private OAuthTokenHandler handler; + private Token accessToken; + private OAuthService service; + + // Requires the apiClass, consumerKey, consumerSecret and callbackUrl along with the TokenHandler + public OAuthTokenClient(BaseApi apiInstance, String consumerKey, String consumerSecret, String callbackUrl, + String scope, OAuthTokenHandler handler) { + this.apiInstance = apiInstance; + this.handler = handler; + if (callbackUrl == null) { callbackUrl = OAuthConstants.OUT_OF_BAND; }; + if(scope == null) { + this.service = new ServiceBuilder() + .apiKey(consumerKey) + .apiSecret(consumerSecret).callback(callbackUrl) + .httpClientConfig(OkHttpHttpClientConfig.defaultConfig()) + .build(apiInstance); + } else { + this.service = new ServiceBuilder() + .apiKey(consumerKey) + .apiSecret(consumerSecret).callback(callbackUrl) + .httpClientConfig(OkHttpHttpClientConfig.defaultConfig()) + .scope(scope) // OAuth2 requires scope + .build(apiInstance); + } + } + + // Get a request token and the authorization url + // Once fetched, fire the onReceivedRequestToken for the request token handler + // Works for both OAuth1.0a and OAuth2 + public void fetchRequestToken() { + if (service.getVersion() == "1.0") { + final OAuth10aService oAuth10aService = (OAuth10aService) service; + oAuth10aService.getRequestTokenAsync(new OAuthAsyncRequestCallback() { + @Override + public void onCompleted(OAuth1RequestToken requestToken) { + String authorizeUrl = oAuth10aService.getAuthorizationUrl((OAuth1RequestToken) requestToken); + handler.onReceivedRequestToken(requestToken, authorizeUrl, service.getVersion()); + + } + + @Override + public void onThrowable(Throwable t) { + handler.onFailure(new Exception(t.getMessage())); + } + }); + } + if (service.getVersion() == "2.0") { + OAuth20Service oAuth20Service = (OAuth20Service) service; + String authorizeUrl = oAuth20Service.getAuthorizationUrl(null); + handler.onReceivedRequestToken(null, authorizeUrl, service.getVersion()); + } + } + + // Get the access token by exchanging the requestToken to the defined URL + // Once receiving the access token, fires the onReceivedAccessToken method on the handler + public void fetchAccessToken(final Token requestToken, final Uri uri) { + + Uri authorizedUri = uri; + + if (service.getVersion() == "1.0") { + // Use verifier token to fetch access token + + if (authorizedUri.getQuery().contains(OAuthConstants.VERIFIER)) { + String oauth_verifier = authorizedUri.getQueryParameter(OAuthConstants.VERIFIER); + OAuth1RequestToken oAuth1RequestToken = (OAuth1RequestToken) requestToken; + OAuth10aService oAuth10aService = (OAuth10aService) service; + + oAuth10aService.getAccessTokenAsync(oAuth1RequestToken, oauth_verifier, + new OAuthAsyncRequestCallback() { + + @Override + public void onCompleted(OAuth1AccessToken oAuth1AccessToken) { + setAccessToken(oAuth1AccessToken); + handler.onReceivedAccessToken(oAuth1AccessToken, service.getVersion()); + } + + @Override + public void onThrowable(Throwable e) { + handler.onFailure(new OAuthException(e.getMessage())); + } + }); + + } + else { // verifier was null + throw new OAuthException("No verifier code was returned with uri '" + uri + "' " + + "and access token cannot be retrieved"); + } + } else if (service.getVersion() == "2.0") { + if (authorizedUri.getQuery().contains(OAuthConstants.CODE)) { + String code = authorizedUri.getQueryParameter(OAuthConstants.CODE); + OAuth20Service oAuth20Service = (OAuth20Service) service; + oAuth20Service.getAccessToken(code, new OAuthAsyncRequestCallback() { + @Override + public void onCompleted(OAuth2AccessToken accessToken) { + setAccessToken(accessToken); + handler.onReceivedAccessToken(accessToken, service.getVersion()); + + } + + @Override + public void onThrowable(Throwable t) { + + } + }); + } + else { // verifier was null + handler.onFailure(new OAuthException("No code was returned with uri '" + uri + "' " + + "and access token cannot be retrieved")); + } + } + } + + // Set the access token used for signing requests + public void setAccessToken(Token accessToken) { + if (accessToken == null) { + this.accessToken = null; + } else { + this.accessToken = accessToken; + } + } + + public Token getAccessToken() { + return this.accessToken; + } + + // Defines the interface handler for different token handlers + public interface OAuthTokenHandler { + public void onReceivedRequestToken(Token requestToken, String authorizeUrl, String oAuthVersion); + public void onReceivedAccessToken(Token accessToken, String oAuthVersion); + public void onFailure(Exception e); + } +} \ No newline at end of file diff --git a/library/src/main/java/com/codepath/utils/GenericsUtil.java b/library/src/main/java/com/codepath/utils/GenericsUtil.java new file mode 100644 index 0000000..66298f9 --- /dev/null +++ b/library/src/main/java/com/codepath/utils/GenericsUtil.java @@ -0,0 +1,80 @@ +package com.codepath.utils; + +import java.lang.reflect.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +@SuppressWarnings("rawtypes") +public class GenericsUtil { + public static List> getTypeArguments(Class baseClass, + Class childClass) { + Map resolvedTypes = new HashMap(); + Type type = childClass; + // start walking up the inheritance hierarchy until we hit baseClass + while (!getClass(type).equals(baseClass)) { + if (type instanceof Class) { + type = ((Class) type).getGenericSuperclass(); + } else { + ParameterizedType parameterizedType = (ParameterizedType) type; + assert parameterizedType != null; + Class rawType = (Class) parameterizedType.getRawType(); + + Type[] actualTypeArguments = parameterizedType + .getActualTypeArguments(); + TypeVariable[] typeParameters = rawType.getTypeParameters(); + for (int i = 0; i < actualTypeArguments.length; i++) { + resolvedTypes + .put(typeParameters[i], actualTypeArguments[i]); + } + + if (!rawType.equals(baseClass)) { + type = rawType.getGenericSuperclass(); + } + } + } + + // finally, for each actual type argument provided to baseClass, + // determine (if possible) + // the raw class for that type argument. + Type[] actualTypeArguments; + if (type instanceof Class) { + actualTypeArguments = ((Class) type).getTypeParameters(); + } else { + assert !(type == null); + actualTypeArguments = ((ParameterizedType) type) + .getActualTypeArguments(); + } + List> typeArgumentsAsClasses = new ArrayList>(); + // resolve types by chasing down type variables. + for (Type baseType : actualTypeArguments) { + while (resolvedTypes.containsKey(baseType)) { + baseType = resolvedTypes.get(baseType); + } + typeArgumentsAsClasses.add(getClass(baseType)); + } + return typeArgumentsAsClasses; + } + + private static Class getClass(Type type) { + if (type instanceof Class) { + return (Class) type; + } else if (type instanceof ParameterizedType) { + return getClass(((ParameterizedType) type).getRawType()); + } else if (type instanceof GenericArrayType) { + Type componentType = ((GenericArrayType) type) + .getGenericComponentType(); + Class componentClass = getClass(componentType); + if (componentClass != null) { + return Array.newInstance(componentClass, 0).getClass(); + } else { + return null; + } + } else { + return null; + } + } + +} diff --git a/app/src/main/res/.gitkeep b/library/src/main/res/.gitkeep similarity index 100% rename from app/src/main/res/.gitkeep rename to library/src/main/res/.gitkeep diff --git a/app/src/main/res/layout/.gitkeep b/library/src/main/res/layout/.gitkeep similarity index 100% rename from app/src/main/res/layout/.gitkeep rename to library/src/main/res/layout/.gitkeep diff --git a/settings.gradle b/settings.gradle index e7b4def..d8f14a1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':app' +include ':library'