Skip to content

Commit 6408f60

Browse files
authored
Merge pull request #21 from stanwood/develop
Master merge for final 1.1.0 release
2 parents ad88a90 + 63dd15b commit 6408f60

17 files changed

+504
-38
lines changed

README.md

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
[![Release](https://jitpack.io/v/stanwood/Network_android.svg?style=flat-square)](https://jitpack.io/#stanwood/Network_android)
1+
[![Release](https://jitpack.io/v/stanwood/framework-network-android.svg?style=flat-square)](https://jitpack.io/#stanwood/framework-network-android)
2+
[![Build Status](https://www.bitrise.io/app/983e6342cc5e0e24/status.svg?token=QtXUf2lbVhJrANROaTkluQ)](https://www.bitrise.io/app/983e6342cc5e0e24)
23

34
# stanwood Network Utilities (Android)
45

@@ -23,12 +24,92 @@ Then add this to you app's `build.gradle`:
2324

2425
```groovy
2526
dependencies {
26-
implementation 'com.github.stanwood:Network_android:<insert latest version here>' // aar version available as well
27+
implementation 'com.github.stanwood:framework-network-android:<insert latest version here>' // aar version available as well
2728
}
2829
```
2930

3031
## Usage
3132

3233
Refer to the extensive javadoc of the provided classes for details on how to use them. Right now there are solutions for the following use cases:
3334

34-
- handle offline situations and caching when using OkHttp (`cache` package)
35+
- handle offline situations and caching when using OkHttp (`cache` package)
36+
- generic token based authentication handling with OkHttp (`auth` package)
37+
38+
### cache
39+
40+
TODO
41+
42+
### auth
43+
44+
The `auth` package contains classes for handling token based authentication with OkHttp. Generally this
45+
is done via Authenticators and Interceptors.
46+
47+
Integration into both existing means of token retrieval as well as from scratch is simple.
48+
49+
First you need to implement both `TokenReaderWriter` and `AuthenticationProvider`.
50+
The first is used for reading and writing tokens from/to requests.
51+
The second provides means to get authentication data (such as tokens and sign-in status).
52+
Refer to the javadoc for more details on how to implement these interfaces.
53+
In the future optional modules will provide implementations for common use cases.
54+
55+
Then create an instance of `io.stanwood.framework.network.auth.Authenticator`:
56+
```java
57+
Authenticator authenticator = new Authenticator(
58+
authenticationProvider,
59+
tokenReaderWriter,
60+
response -> Log.e("Authentication failed permanently!")
61+
);
62+
```
63+
64+
And an instance of `AuthInterceptor`:
65+
```java
66+
AuthInterceptor authInterceptor = new AuthInterceptor(
67+
appContext,
68+
authenticationProvider,
69+
tokenReaderWriter
70+
);
71+
```
72+
73+
Construct an `OkHttpClient` and pass the interceptor and the authenticator:
74+
```java
75+
new OkHttpClient.Builder()
76+
.authenticator(authenticator)
77+
.addInterceptor(authInterceptor)
78+
.build();
79+
```
80+
81+
That's it. When using this `OkHttpClient` instance you'll benefit from fully transparent token handling.
82+
83+
*If your app uses multiple authentication methods make sure to implement an own subclass of `AuthenticationProvider`
84+
for each method and use an own `OkHttpClient` for each!*
85+
86+
#### A note on authentication exception handling
87+
88+
The library provides an own exception class called `AuthenticationException`. You can listen for this
89+
class in your Retrofit `Callback.onFailure(Call, Throwable)` to check for authentication related
90+
errors.
91+
92+
However up until now we are not able to fire this exception everywhere - especially the
93+
`Authenticator` suffers from a likely [okhttp bug](https://github.com/square/okhttp/issues/3872)
94+
which prevents us from firing exceptions there. In case of errors you will receive a `NullPointerException`
95+
instead which probably won't help you much for automated handling as this exception is basically
96+
caused by every interceptor/authenticator returning `null` for the expected Request/Response.
97+
98+
For the time being we recommend to take a best effort approach here and additionally check for 401 response code
99+
in Retrofit's `Callback.onSuccess()` for when an issue in the Authenticator occured (if there was no
100+
issue you won't get a 401 propagated to `Callback.onSuccess()` as in case of successful authentication
101+
after receiving a 401 for the initial request your callback will get the response code of the following
102+
originally intended request).
103+
104+
If you're having problems retrieving a token in the `AuthenticationProvider` (e.g. due to an invalid
105+
refresh token) always try to resolve those issues there as well if possible. Throwing an
106+
`AuthenticationException` should just be a last resort.
107+
108+
Alternatively you can also subclass the `Authenticator` class and override
109+
`onAuthenticationFailed()` if you want to trigger special handling for failed authentication from
110+
which we couldn't recover with our default handling. This usually tends to be a bit harder to get
111+
right as it expects you to modify the failed request directly without any means of intercepting the
112+
response. Thus handling token retrieval issues should preferably handled in the `AuthenticationProvider`
113+
as explained above.
114+
115+
We're looking forward to streamlining this as soon as the okhttp bug has been resolved.

app/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ android {
88
compileSdkVersion 27
99
defaultConfig {
1010
applicationId "io.stanwood.framework.network.sample"
11-
minSdkVersion 21
11+
minSdkVersion 16
1212
targetSdkVersion 27
1313
versionCode 1
1414
versionName "1.0"
@@ -24,7 +24,7 @@ android {
2424

2525
dependencies {
2626
implementation fileTree(include: ['*.jar'], dir: 'libs')
27-
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
27+
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
2828
implementation 'com.android.support:appcompat-v7:27.0.2'
2929
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
3030
testImplementation 'junit:junit:4.12'

build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
// Top-level build file where you can add configuration options common to all sub-projects/modules.
22

33
buildscript {
4-
ext.kotlin_version = '1.2.0'
4+
ext.kotlin_version = '1.2.21'
55
repositories {
66
google()
77
jcenter()
88
}
99
dependencies {
10-
classpath 'com.android.tools.build:gradle:3.0.1'
10+
classpath 'com.android.tools.build:gradle:3.1.0-beta2'
1111
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
1212
classpath 'com.github.dcendents:android-maven-gradle-plugin:2.0'
1313

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
#Wed Dec 13 15:44:17 CET 2017
1+
#Mon Feb 12 16:58:25 CET 2018
22
distributionBase=GRADLE_USER_HOME
33
distributionPath=wrapper/dists
44
zipStoreBase=GRADLE_USER_HOME
55
zipStorePath=wrapper/dists
6-
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
6+
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip

network/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ android {
77
compileSdkVersion 27
88

99
defaultConfig {
10-
minSdkVersion 21
10+
minSdkVersion 16
1111
targetSdkVersion 27
1212
versionCode 1
1313
versionName "1.0"
@@ -31,7 +31,7 @@ dependencies {
3131
implementation 'com.android.support:support-annotations:27.0.2'
3232
testImplementation 'junit:junit:4.12'
3333
androidTestImplementation 'com.android.support.test:runner:1.0.1'
34-
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
34+
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
3535
api 'com.squareup.okhttp3:okhttp:3.9.1'
3636
}
3737

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2-
package="io.stanwood.framework.network" />
2+
package="io.stanwood.framework.network" >
3+
4+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
5+
</manifest>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.stanwood.framework.network.auth;
2+
3+
/**
4+
* A collection of header keys used by {@link AuthInterceptor} AND {@link Authenticator}
5+
* as well as their signed-in variants.
6+
*/
7+
@SuppressWarnings("WeakerAccess")
8+
public abstract class AuthHeaderKeys {
9+
10+
/**
11+
* This header is set by the Authenticators / Auth Interceptors to determine when to retry a
12+
* request with a fresh token.
13+
*/
14+
public static final String RETRY_WITH_REFRESH_HEADER_KEY = "RetryWithRefresh";
15+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package io.stanwood.framework.network.auth;
2+
3+
import android.content.Context;
4+
import android.support.annotation.NonNull;
5+
import android.support.annotation.Nullable;
6+
7+
import java.io.IOException;
8+
9+
import io.stanwood.framework.network.util.ConnectionState;
10+
import okhttp3.Interceptor;
11+
import okhttp3.Request;
12+
import okhttp3.Response;
13+
14+
/**
15+
* This class is used by okhttp to authenticate requests.
16+
*/
17+
public class AuthInterceptor implements Interceptor {
18+
19+
@NonNull
20+
private final ConnectionState connectionState;
21+
@NonNull
22+
private final AuthenticationProvider authenticationProvider;
23+
@NonNull
24+
private final TokenReaderWriter tokenReaderWriter;
25+
@Nullable
26+
private final OnAuthenticationFailedListener onAuthenticationFailedListener;
27+
28+
public AuthInterceptor(
29+
@NonNull Context applicationContext,
30+
@NonNull AuthenticationProvider authenticationProvider,
31+
@NonNull TokenReaderWriter tokenReaderWriter,
32+
@Nullable OnAuthenticationFailedListener onAuthenticationFailedListener
33+
34+
) {
35+
this.connectionState = new ConnectionState(applicationContext);
36+
this.authenticationProvider = authenticationProvider;
37+
this.tokenReaderWriter = tokenReaderWriter;
38+
this.onAuthenticationFailedListener = onAuthenticationFailedListener;
39+
}
40+
41+
@Override
42+
public Response intercept(@NonNull Chain chain) throws IOException {
43+
Request request = chain.request();
44+
request = tokenReaderWriter.removeToken(request);
45+
final Request.Builder requestBuilder = request.newBuilder();
46+
47+
if (connectionState.isConnected()) {
48+
String token;
49+
synchronized (authenticationProvider.getLock()) {
50+
try {
51+
token = authenticationProvider.getToken(false);
52+
} catch (AuthenticationException e) {
53+
onAuthenticationFailed();
54+
throw e;
55+
}
56+
}
57+
58+
if (token == null) {
59+
onAuthenticationFailed();
60+
throw new AuthenticationException();
61+
}
62+
63+
requestBuilder.header(AuthHeaderKeys.RETRY_WITH_REFRESH_HEADER_KEY, "true");
64+
request = tokenReaderWriter.write(requestBuilder.build(), token);
65+
} else {
66+
// we're offline, clean up headers for cache handling
67+
request = requestBuilder.removeHeader(AuthHeaderKeys.RETRY_WITH_REFRESH_HEADER_KEY).build();
68+
}
69+
70+
return chain.proceed(request);
71+
}
72+
73+
private void onAuthenticationFailed() {
74+
if (onAuthenticationFailedListener != null) {
75+
onAuthenticationFailedListener.onAuthenticationFailed(null);
76+
}
77+
}
78+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package io.stanwood.framework.network.auth;
2+
3+
import java.io.IOException;
4+
5+
/**
6+
* Exception class used by {@link AuthInterceptor} and in the future by {@link Authenticator} to
7+
* indicate issues during token retrieval.
8+
* <br><br>
9+
* Right now we cannot use this for the Authenticator due to a likely
10+
* <a href="https://github.com/square/okhttp/issues/3872">bug in okhttp</a> which keeps the
11+
* connection open when throwing exceptions in Authenticators.
12+
*/
13+
public class AuthenticationException extends IOException {
14+
15+
public static final String DEFAULT_MESSAGE = "Error while trying to retrieve auth token";
16+
17+
public AuthenticationException() {
18+
super(DEFAULT_MESSAGE);
19+
}
20+
21+
public AuthenticationException(String message) {
22+
super(message);
23+
}
24+
25+
public AuthenticationException(String message, Throwable cause) {
26+
super(message, cause);
27+
}
28+
29+
public AuthenticationException(Throwable cause) {
30+
super(DEFAULT_MESSAGE, cause);
31+
}
32+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package io.stanwood.framework.network.auth;
2+
3+
import android.support.annotation.NonNull;
4+
import android.support.annotation.Nullable;
5+
6+
/**
7+
* Main class to provide authentication information and locks. Used by
8+
* Authenticators and Interceptors.
9+
* <br><br>
10+
* Implement one for each authentication method!
11+
*/
12+
public interface AuthenticationProvider {
13+
14+
/**
15+
* Lock used by Authenticator / Auth Interceptor when requesting tokens. Provide a
16+
* final static Object here.
17+
*
18+
* @return lock
19+
*/
20+
@NonNull
21+
Object getLock();
22+
23+
/**
24+
* Retrieves a token for authenticated access
25+
*
26+
* @param forceRefresh whether a new token shall be retrieved from the server and not from cache
27+
* @return token
28+
*
29+
* @throws AuthenticationException if the token cannot be retrieved
30+
*/
31+
@Nullable
32+
String getToken(boolean forceRefresh) throws AuthenticationException;
33+
}

0 commit comments

Comments
 (0)