Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(void): Introduce new EmptyResponse type as successor for Void (#541) #542

Merged
merged 7 commits into from
Jan 19, 2024
43 changes: 23 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
[![GitHub stars](https://img.shields.io/github/stars/nextcloud/Android-SingleSignOn.svg)](https://github.com/nextcloud/Android-SingleSignOn/stargazers)
[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)

This library allows you to use accounts as well as the network stack provided by the [nextcloud files app](https://play.google.com/store/apps/details?id=com.nextcloud.client). Therefore you as a developer don't need to worry about asking the user for credentials as well as you don't need to worry about self-signed ssl certificates, two factor authentication, save credential storage etc.
This library allows you to use accounts as well as the network stack provided by the [Nextcloud Files app](https://play.google.com/store/apps/details?id=com.nextcloud.client). Therefore you as a developer don't need to worry about asking the user for credentials as well as you don't need to worry about self-signed ssl certificates, two factor authentication, save credential storage etc.

*Please note that the user needs to install the [nextcloud files app](https://play.google.com/store/apps/details?id=com.nextcloud.client) in order to use those features.* While this might seem like a "no-go" for some developers, we still think that using this library is worth consideration as it makes the account handling much faster and safer.
*Please note that the user needs to install the [Nextcloud Files app](https://play.google.com/store/apps/details?id=com.nextcloud.client) in order to use those features.* While this might seem like a "no-go" for some developers, we still think that using this library is worth consideration as it makes the account handling much faster and safer.

- [How to use this library](#how-to-use-this-library)
- [1) Add this library to your project](#1-add-this-library-to-your-project)
Expand Down Expand Up @@ -45,7 +45,7 @@ repositories {

dependencies {
// Note: Android Gradle Plugin (AGP) version ≥ 7.0.0 is required.
implementation "com.github.nextcloud:Android-SingleSignOn:0.6.0"
implementation "com.github.nextcloud:Android-SingleSignOn:0.8.1"
}
```

Expand Down Expand Up @@ -129,13 +129,13 @@ public NextcloudAPI(Context context, SingleSignOnAccount account, Gson gson) {

You'll notice that there is an optional `ApiConnectedListener` callback parameter in the constructor of the `NextcloudAPI`.
You can use this callback to subscribe to errors that might occur during the initialization of the API.
You can start making requests to the API as soon as you instantiated the `NextcloudAPI` object.
The callback method `onConnected` will be called once the connection to the files app is established.
You can start making calls to the api before that callback is fired as the library will queue your calls until the connection is established[¹](https://github.com/nextcloud/Android-SingleSignOn/issues/400).

ℹ️ You can start making requests to the API before that callback is fired as the library will queue your calls until the connection is established[¹](https://github.com/nextcloud/Android-SingleSignOn/issues/400).

#### 5.1) **Using Retrofit**

##### 5.1.1) Before using this single sign on library, your interface for your retrofit API might look like this:
##### 5.1.1) Before using this Single Sign On library, your interface for your [Retrofit](https://square.github.io/retrofit/) API might look like this:

```java
public interface API {
Expand All @@ -159,7 +159,10 @@ public interface API {
}
```

You might instantiate your retrofit `API` by using something like this:
ℹ️ If your REST endpoint returns an empty body, you need to specify `Observable<EmptyResponse>` as return value rather than `Observable<Void>` because ["Nulls are not allowed in \[RxJava\] 2.x."](https://github.com/ReactiveX/RxJava/issues/5775#issuecomment-353544736).
ℹ️ If you are working with `Call`s you can safely use `Call<Void>` (or `Call<EmptyResponse>`) if you want.

You might instantiate your Retrofit `API` by using something like this:

```java
public class ApiProvider {
Expand Down Expand Up @@ -187,9 +190,9 @@ public class ApiProvider {
}
```

Enjoy! If you're already using retrofit, you don't need to modify your application logic. Just exchange the API and you're good to go!
Enjoy! If you're already using Retrofit, you don't need to modify your application logic. Just exchange the API and you're good to go!

Note: If you need a different mapping between your json-structure and your java-structure you might want to create a custom type adapter using `new GsonBuilder().create().registerTypeAdapter(…)`. Take a look at [this](https://github.com/nextcloud/news-android/blob/783836390b4c27aba285bad1441b53154df16685/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/helper/GsonConfig.java) example for more information.
ℹ️ If you need a different mapping between your JSON structure and your Java structure you might want to create a custom type adapter using `new GsonBuilder().create().registerTypeAdapter(…)`. Take a look at [this](https://github.com/nextcloud/news-android/blob/783836390b4c27aba285bad1441b53154df16685/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/helper/GsonConfig.java) example for more information.

#### 5.2) **Without Retrofit**

Expand Down Expand Up @@ -248,9 +251,9 @@ public class MyActivity extends AppCompatActivity {

#### 5.3) **WebDAV**

The following `WebDAV` Methods are supported: `PROPFIND` / `MKCOL`
Currently the following `WebDAV` Methods are supported: `PROPFIND` / `MKCOL`

The following examples shows how to use the `PROPFIND` method. With a depth of 0.
The following examples shows how to use the `PROPFIND` method with a depth of 0.

```java
final List<String> depth = new ArrayList<>();
Expand All @@ -266,7 +269,7 @@ final var nextcloudRequest = new NextcloudRequest.Builder()

## Additional info

In case that you require some sso features that were introduced in a specific nextcloud files app version, you can run a simple version check using the following helper method:
In case that you require some SSO features that were introduced in a specific Nextcloud Files app version, you can run a simple version check using the following helper method:

```java
final int MIN_NEXTCLOUD_FILES_APP_VERSION_CODE = 30030052;
Expand All @@ -284,25 +287,25 @@ Therefore it is **recommended** to add `-dontobfuscate` to your app-specific pro

With [R8 full mode](https://r8.googlesource.com/r8/+/refs/heads/master/compatibility-faq.md#r8-full-mode) being enabled by default since [AGP 8.0](https://developer.android.com/build/releases/gradle-plugin#default-changes), you will probably need to handle following app-specific rules yourself (or disable full mode):

### gson
According to [gson's sample rules](https://github.com/google/gson/blob/master/examples/android-proguard-example/proguard.cfg#L14), you still need to configure rules for your gson-handled classes.
### Gson
According to [Gson's sample rules](https://github.com/google/gson/blob/master/examples/android-proguard-example/proguard.cfg#L14), you still need to configure rules for your gson-handled classes.
> ```
> # Application classes that will be serialized/deserialized over Gson
> -keep class com.google.gson.examples.android.model.** { <fields>; }
> ```

### Retrofit
The same applies to classes which you're using in the api from step [5.1.1](#511-before-using-this-single-sign-on-library-your-interface-for-your-retrofit-api-might-look-like-this)
The same applies to classes which you're using in the api from step [5.1.1](#511-before-using-this-single-sign-on-library-your-interface-for-your-retrofit-api-might-look-like-this)
```
# Application classes that will be serialized/deserialized by retrofit
# Application classes that will be serialized/deserialized by Retrofit
-keep class com.google.gson.examples.android.model.**
```

If you find working less broad rules, contributions to these rules are welcome!

## Security

Once the user clicks on "Allow" in the login dialog, the Nextcloud Files App will generate a token for your app. Only your app is allowed to use that token. Even if another app will get a hold of that token, it won't be able to make any requests to the nextcloud server as the nextcloud files app matches that token against the namespace of your app.
Once the user clicks on <kbd>Allow</kbd> in the login dialog, the Nextcloud Files App will generate a token for your app. Only your app is allowed to use that token. Even if another app will get a hold of that token, it won't be able to make any requests to the nextcloud server as the nextcloud files app matches that token against the namespace of your app.

![](doc/NextcloudSSO.png)

Expand Down Expand Up @@ -340,13 +343,13 @@ Once the user clicks on "Allow" in the login dialog, the Nextcloud Files App wil
## Troubleshooting

If you are experiencing any issues, the following tips might workaround:
- Disable battery optimizations of the nextcloud files app, especially [in case of a `NextcloudApiNotRespondingException`](https://github.com/nextcloud/Android-SingleSignOn/issues/162)
- Disable battery optimizations of the Nextcloud Files app, especially [in case of a `NextcloudApiNotRespondingException`](https://github.com/nextcloud/Android-SingleSignOn/issues/162)
- [Permit auto start](https://github.com/stefan-niedermann/nextcloud-deck/issues/660#issuecomment-682002392)
- A quickly appearing and disappearing menu when attempting to select an account is often a hint for an outdated Nextcloud files app
- A quickly appearing and disappearing menu when attempting to select an account is often a hint for an outdated Nextcloud Files app

## Flow Diagram

Note that the "Make network request" section in the diagram only shows the workflow if you use the "retrofit" api.
Note that the "Make network request" section in the diagram only shows the workflow if you use the Retrofit API.

![Flow Diagram](doc/NextcloudSingleSignOn.png)

Expand Down
2 changes: 0 additions & 2 deletions lib/detekt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -422,8 +422,6 @@ style:
ForbiddenImport:
active: false
imports: ''
ForbiddenVoid:
active: false
FunctionOnlyReturningConstant:
active: false
ignoreOverridableFunction: true
Expand Down
10 changes: 10 additions & 0 deletions lib/src/main/java/com/nextcloud/android/sso/api/EmptyResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.nextcloud.android.sso.api;

import java.io.Serializable;

/**
* Use {@link EmptyResponse} as type for call which do not return a response body.
* Replaced {@link Void} which was previously used for this scenario since <a href="https://github.com/nextcloud/Android-SingleSignOn/issues/541">Issue #541</a>
*/
public final class EmptyResponse implements Serializable {
}
45 changes: 22 additions & 23 deletions lib/src/main/java/com/nextcloud/android/sso/api/NextcloudAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Type;

import io.reactivex.Observable;
Expand All @@ -48,31 +47,22 @@ public class NextcloudAPI implements AutoCloseable {

private static final String TAG = NextcloudAPI.class.getCanonicalName();

private static final Void NOTHING = getVoidInstance();

private static Void getVoidInstance() {
//noinspection unchecked
final Constructor<Void> constructor = (Constructor<Void>) Void.class.getDeclaredConstructors()[0];
constructor.setAccessible(true);
try {
return constructor.newInstance();
} catch (Exception e) {
throw new IllegalStateException("Should never happen, but did: unable to instantiate Void");
}
}
private static final EmptyResponse EMPTY_RESPONSE = new EmptyResponse();

private final NetworkRequest networkRequest;
private Gson gson;

@Documented
@Target(METHOD)
@Retention(RUNTIME)
public @interface FollowRedirects { }
public @interface FollowRedirects {
}

public interface ApiConnectedListener {
default void onConnected() {
Log.i(TAG, "Single Sign On API successfully connected.");
}

void onError(Exception e);
}

Expand Down Expand Up @@ -117,7 +107,8 @@ public void stop() {
}

public <T> Observable<ParsedResponse<T>> performRequestObservableV2(final Type type, final NextcloudRequest request) {
return Observable.fromPublisher( s -> {
ensureTypeNotVoid(type);
return Observable.fromPublisher(s -> {
try {
final Response response = performNetworkRequestV2(request);
s.onNext(ParsedResponse.of(convertStreamToTargetEntity(response.getBody(), type), response.getPlainHeaders()));
Expand All @@ -129,7 +120,8 @@ public <T> Observable<ParsedResponse<T>> performRequestObservableV2(final Type t
}

public <T> io.reactivex.rxjava3.core.Observable<ParsedResponse<T>> performRequestObservableV3(final Type type, final NextcloudRequest request) {
return io.reactivex.rxjava3.core.Observable.fromPublisher( s -> {
ensureTypeNotVoid(type);
return io.reactivex.rxjava3.core.Observable.fromPublisher(s -> {
try {
final Response response = performNetworkRequestV2(request);
s.onNext(ParsedResponse.of(convertStreamToTargetEntity(response.getBody(), type), response.getPlainHeaders()));
Expand All @@ -142,6 +134,7 @@ public <T> io.reactivex.rxjava3.core.Observable<ParsedResponse<T>> performReques

public <T> T performRequestV2(final @NonNull Type type, NextcloudRequest request) throws Exception {
Log.d(TAG, "performRequestV2() called with: type = [" + type + "], request = [" + request + "]");
ensureTypeNotVoid(type);
final Response response = performNetworkRequestV2(request);
return convertStreamToTargetEntity(response.getBody(), type);
}
Expand All @@ -150,20 +143,20 @@ public <T> T convertStreamToTargetEntity(InputStream inputStream, Type targetEnt
final T result;
try (InputStream os = inputStream;
Reader targetReader = new InputStreamReader(os)) {
if (targetEntity != Void.class) {
if (targetEntity == EmptyResponse.class) {
//noinspection unchecked
result = (T) EMPTY_RESPONSE;
} else {
result = gson.fromJson(targetReader, targetEntity);
if (result == null) {
if (targetEntity == Object.class) {
//noinspection unchecked
return (T) NOTHING;
return (T) EMPTY_RESPONSE;
} else {
throw new IllegalStateException("Could not instantiate \"" +
targetEntity.toString() + "\", because response was null.");
targetEntity + "\", because response was null.");
}
}
} else {
//noinspection unchecked
result = (T) NOTHING;
}
}
return result;
Expand All @@ -176,10 +169,16 @@ public <T> T convertStreamToTargetEntity(InputStream inputStream, Type targetEnt
* @return InputStream answer from server as InputStream
* @throws Exception or {@link SSOException}
*/
public Response performNetworkRequestV2(NextcloudRequest request) throws Exception {
public Response performNetworkRequestV2(@NonNull NextcloudRequest request) throws Exception {
return networkRequest.performNetworkRequestV2(request, request.getBodyAsStream());
}

private void ensureTypeNotVoid(final @NonNull Type type) {
if (type == Void.class) {
throw new IllegalArgumentException(Void.class.getSimpleName() + " is not supported. Use " + EmptyResponse.class.getSimpleName() + " for calls without a response body. See also: https://github.com/nextcloud/Android-SingleSignOn/issues/541");
}
}

protected Gson getGson() {
return gson;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.nextcloud.android.sso.helper;

import com.nextcloud.android.sso.aidl.NextcloudRequest;
import com.nextcloud.android.sso.api.EmptyResponse;
import com.nextcloud.android.sso.api.NextcloudAPI;

import io.reactivex.Completable;
Expand Down Expand Up @@ -29,11 +30,11 @@ public final class ReactivexHelper {
private ReactivexHelper() { }

public static Completable wrapInCompletable(final NextcloudAPI nextcloudAPI, final NextcloudRequest request) {
return Completable.fromAction(() -> nextcloudAPI.performRequestV2(Void.class, request));
return Completable.fromAction(() -> nextcloudAPI.performRequestV2(EmptyResponse.class, request));
}

public static io.reactivex.rxjava3.core.Completable wrapInCompletableV3(final NextcloudAPI nextcloudAPI, final NextcloudRequest request) {
return io.reactivex.rxjava3.core.Completable.fromAction(() -> nextcloudAPI.performRequestV2(Void.class, request));
return io.reactivex.rxjava3.core.Completable.fromAction(() -> nextcloudAPI.performRequestV2(EmptyResponse.class, request));
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import androidx.annotation.NonNull;

import com.nextcloud.android.sso.aidl.NextcloudRequest;
import com.nextcloud.android.sso.api.EmptyResponse;
import com.nextcloud.android.sso.api.AidlNetworkRequest;
import com.nextcloud.android.sso.api.NextcloudAPI;
import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
Expand Down Expand Up @@ -134,26 +135,25 @@ private Response<T> convertExceptionToResponse(int statusCode, String errorMessa
};
}


/**
*
* @param success if <code>true</code>, a Response.success will be returned, otherwise Response.error(520)
*/
public static Call<Void> wrapVoidCall(final boolean success) {
return new Call<Void>() {
public static Call<EmptyResponse> wrapEmptyResponseCall(final boolean success) {
return new Call<>() {
@NonNull
@Override
public Response<Void> execute() {
if(success) {
public Response<EmptyResponse> execute() {
if (success) {
return Response.success(null);
} else {
return Response.error(520, emptyResponseBody);
}
}

@Override
public void enqueue(@NonNull Callback<Void> callback) {
if(success) {
public void enqueue(@NonNull Callback<EmptyResponse> callback) {
if (success) {
callback.onResponse(this, Response.success(null));
} else {
callback.onResponse(this, Response.error(520, emptyResponseBody));
Expand All @@ -177,7 +177,7 @@ public boolean isCanceled() {

@NonNull
@Override
public Call<Void> clone() {
public Call<EmptyResponse> clone() {
throw new UnsupportedOperationException("Not implemented");
}

Expand Down
10 changes: 5 additions & 5 deletions lib/src/test/java/com/nextcloud/android/sso/api/API.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,24 +63,24 @@ Observable<ResponseBody> getStreamingUpdatedItems(
);

@PUT("items/read/multiple")
Call<Void> putMarkItemsRead(@Body String items);
Call<EmptyResponse> putMarkItemsRead(@Body String items);

@PATCH("test")
Call<Void> invalidPATCH();
Call<EmptyResponse> invalidPATCH();

@Headers({
"X-Foo: Bar",
"X-Ping: Pong"
})
@GET("test")
Call<Void> getWithHeader();
Call<EmptyResponse> getWithHeader();

@GET("/test")
Call<Void> getDynamicHeader(@Header("Content-Range") String contentRange);
Call<EmptyResponse> getDynamicHeader(@Header("Content-Range") String contentRange);

@NextcloudAPI.FollowRedirects
@GET("/test")
Call<Void> getFollowRedirects();
Call<EmptyResponse> getFollowRedirects();

@FormUrlEncoded
@POST("/test")
Expand Down
Loading
Loading