package org.edx.mobile.http;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.edx.mobile.util.images.ErrorUtils;
import org.edx.mobile.view.common.TaskMessageCallback;
import org.edx.mobile.view.common.TaskProgressCallback;
import java.io.IOException;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import roboguice.RoboGuice;
/**
* Generic abstract implementation of Retrofit's
* {@link retrofit2.Callback} interface, that takes care of delivering
* status and error information to the proper callbacks. It also
* provides (and delegates to) a simpler callback interface for
* subclasses, stripping out unnecessary parameters, and redirecting
* all responses with error codes to the failure callback method (as it
* used to be in the implementation in Retrofit 1).
*
* @param <T> The successful response body type.
*/
public abstract class ErrorHandlingCallback<T> implements Callback<T> {
/**
* A Context for resolving the error message strings.
*/
@NonNull
private final Context context;
/**
* The trigger for initiating the call. This is used to determine the type of error message to
* deliver.
*/
@NonNull
private final CallTrigger callTrigger;
/**
* The callback to invoke on start and finish of the request.
*/
@Nullable
private final TaskProgressCallback progressCallback;
/**
* The callback to invoke for delivering any error messages.
*/
@Nullable
private final TaskMessageCallback messageCallback;
/**
* Create a new instance of this class.
*
* @param context A Context for resolving the error message strings. Note that for convenience,
* this will be checked to determine whether it's implementing any of the
* callback interfaces, and will be registered as such if so. If this is not the
* desired outcome, then one of the alternative constructors should be used
* instead, with the relevant callback parameters explicitly passed as null (this
* may require casting the null in case of ambiguity when using a constructor
* that only sets one callback explicitly).
* @param callTrigger The trigger for initiating the call. This is used to determine the type of
* error message to deliver.
*/
public ErrorHandlingCallback(@NonNull final Context context,
@NonNull final CallTrigger callTrigger) {
this(context, callTrigger,
context instanceof TaskProgressCallback ? (TaskProgressCallback) context : null,
context instanceof TaskMessageCallback ? (TaskMessageCallback) context : null);
}
/**
* Create a new instance of this class.
*
* @param context A Context for resolving the error message strings. Note that for convenience,
* this will be checked to determine whether it's implementing the
* {@link TaskMessageCallback} interface, and will be registered as such if so.
* If this is not the desired outcome, then the other constructor should be used
* that takes this callback parameter, and it should be explicitly set as null.
* @param callTrigger The trigger for initiating the call. This is used to determine the type of
* error message to deliver.
* @param progressCallback The callback to invoke on start and finish of the request. Note that
* since no callback method in this class is invoked upon request
* initiation, it assumes that it's being initiated immediately, and
* thus invokes that start callback immediately as well.
*/
public ErrorHandlingCallback(@NonNull final Context context,
@NonNull final CallTrigger callTrigger,
@Nullable final TaskProgressCallback progressCallback) {
this(context, callTrigger,
progressCallback,
context instanceof TaskMessageCallback ? (TaskMessageCallback) context : null);
}
/**
* Create a new instance of this class.
*
* @param context A Context for resolving the error message strings. Note that for convenience,
* this will be checked to determine whether it's implementing the
* {@link TaskProgressCallback} interface, and will be registered as such if so.
* If this is not the desired outcome, then the other constructor should be used
* that takes this callback parameter, and it should be explicitly set as null.
* @param callTrigger The trigger for initiating the call. This is used to determine the type of
* error message to deliver.
* @param messageCallback The callback to invoke for delivering any error messages.
*/
public ErrorHandlingCallback(@NonNull final Context context,
@NonNull final CallTrigger callTrigger,
@Nullable final TaskMessageCallback messageCallback) {
this(context, callTrigger,
context instanceof TaskProgressCallback ? (TaskProgressCallback) context : null,
messageCallback);
}
/**
* Create a new instance of this class.
*
* @param context A Context for resolving the error message strings.
* @param callTrigger The trigger for initiating the call. This is used to determine the type of
* error message to deliver.
* @param progressCallback The callback to invoke on start and finish of the request. Note that
* since no callback method in this class is invoked upon request
* initiation, it assumes that it's being initiated immediately, and
* thus invokes that start callback immediately as well.
* @param messageCallback The callback to invoke for delivering any error messages.
*/
public ErrorHandlingCallback(@NonNull final Context context,
@NonNull final CallTrigger callTrigger,
@Nullable final TaskProgressCallback progressCallback,
@Nullable final TaskMessageCallback messageCallback) {
this.context = context;
this.callTrigger = callTrigger;
this.progressCallback = progressCallback;
this.messageCallback = messageCallback;
// For the convenience of subclasses
RoboGuice.injectMembers(context, this);
if (progressCallback != null) {
progressCallback.startProcess();
}
}
/**
* The original callback method invoked by Retrofit upon receiving an HTTP response. This method
* definition provides extra information that's not needed by most individual callback
* implementations, and is also invoked when HTTP error status codes are encountered (forcing
* the implementation to manually check for success in each case). Therefore this implementation
* delegates to {@link #onResponse(Object)} in the case where it receives a successful HTTP
* status code, and to {@link #onFailure(Call, Throwable)} otherwise, passing an instance of
* {@link HttpResponseStatusException} with the relevant error status code. This method is
* declared as final, as subclasses are meant to be implementing the abstract
* {@link #onResponse(Object)} method instead of this one.
* <p>
* This implementation takes care of invoking the callback for request process completion.
*
* @param call The Call object that was used to enqueue the request.
* @param response The HTTP response data.
*/
@Override
public final void onResponse(@NonNull Call<T> call, @NonNull Response<T> response) {
if (!response.isSuccessful()) {
onFailure(call, new HttpResponseStatusException(response.code()));
} else {
if (progressCallback != null) {
progressCallback.finishProcess();
}
onResponse(response.body());
}
}
/**
* The original callback method invoked by Retrofit upon failure to receive an HTTP response,
* whether due to encountering a network error while waiting for the response, or some other
* unexpected error while constructing the request or processing the response. It's also invoked
* by the {@link #onResponse(Call, Response)} implementation when it receives an HTTP error
* status code. However, this method definition provides extra information that's not needed by
* most individual callback implementation, so this implementation only delegates to
* {@link #onFailure(Throwable)}.
* <p>
* This implementation takes care of delivering the appropriate error message to it's registered
* callback, and invoking the callback for request process completion. It should only be
* overridden if the subclass wants to handle or control these actions itself; otherwise
* subclasses should override the empty {@link #onFailure(Throwable)} callback method instead.
*
* @param call The Call object that was used to enqueue the request.
* @param error An {@link IOException} if the request failed due to a network failure, an
* {HttpResponseStatusException} if the failure was due to receiving an error code,
* or any {@link Throwable} implementation if one was thrown unexpectedly while
* creating the request or processing the response.
*/
@Override
public void onFailure(@NonNull Call<T> call, @NonNull Throwable error) {
if (progressCallback != null) {
progressCallback.finishProcess();
}
if (messageCallback != null && !call.isCanceled()) {
messageCallback.onMessage(callTrigger.getMessageType(),
ErrorUtils.getErrorMessage(error, context));
}
onFailure(error);
}
/**
* Callback method for a successful HTTP response.
*
* @param responseBody The response body, converted to an instance of it's associated Java
* class.
*/
protected abstract void onResponse(@NonNull final T responseBody);
/**
* Callback method for when the HTTP response was not received successfully, whether due to a
* network failure, receiving an HTTP error status code, or encountering an unexpected exception
* or error during the request creation or response processing phase.
*
* @param error An {@link IOException} if the request failed due to a network failure, an
* {HttpResponseStatusException} if the failure was due to receiving an error code,
* or any {@link Throwable} implementation if one was thrown unexpectedly while
* creating the request or processing the response.
*/
protected void onFailure(@NonNull final Throwable error) {}
}