package com.instructure.canvasapi.utilities;
import android.content.Context;
import android.os.AsyncTask;
import android.util.Log;
import com.instructure.canvasapi.api.BuildInterfaceAPI;
import com.instructure.canvasapi.model.CanvasError;
import java.io.Serializable;
import retrofit.Callback;
import retrofit.RetrofitError;
import retrofit.client.Response;
/**
* CanvasCallback is a parameterized class that handles pagination and caching automatically.
* Created by Joshua Dutton on 8/9/13.
*
* Copyright (c) 2014 Instructure. All rights reserved.
*/
public abstract class CanvasCallback<T> implements Callback<T> {
protected APIStatusDelegate statusDelegate;
// Controls whether of not the cache callbacks are called. Useful for pull-to-refresh, where the cache results should be ignored
protected APICacheStatusDelegate cacheStatusDelegate;
private String cacheFileName;
private boolean isNextPage = false;
private boolean isCancelled = false;
private boolean isFinished = true;
private boolean hasReadFromCache = false;
public static ErrorDelegate defaultErrorDelegate;
private ErrorDelegate errorDelegate;
///////////////////////////////////////////////////////////////////////////
// Getters and Setters
///////////////////////////////////////////////////////////////////////////
public boolean isFinished() {
return isFinished;
}
public boolean isCancelled() {
return isCancelled;
}
public void setFinished(boolean isFinished) {
this.isFinished = isFinished;
}
public boolean hasReadFromCache(){
return hasReadFromCache;
}
public void setHasReadFromCache(boolean hasReadFromCache){
this.hasReadFromCache = hasReadFromCache;
}
public APIStatusDelegate getStatusDelegate() {
return statusDelegate;
}
public APICacheStatusDelegate getCacheStatusDelegate() {
return cacheStatusDelegate;
}
/**
* setIsNextPage sets whether you're on the NextPages (2 or more) of pagination.
* @param nextPage
*/
public void setIsNextPage(boolean nextPage){
isNextPage = nextPage;
}
///////////////////////////////////////////////////////////////////////////
// Constructors
///////////////////////////////////////////////////////////////////////////
/**
* @param statusDelegate Delegate to get the context
*/
public CanvasCallback(APIStatusDelegate statusDelegate) {
setupDelegates(statusDelegate, null);
}
/**
* Overload constructor to override default error delegate.
*/
public CanvasCallback(APIStatusDelegate statusDelegate, ErrorDelegate errorDelegate){
setupDelegates(statusDelegate, errorDelegate);
}
private void setupDelegates(APIStatusDelegate statusDelegate, ErrorDelegate errorDelegate) {
this.statusDelegate = statusDelegate;
if (statusDelegate instanceof APICacheStatusDelegate) {
this.cacheStatusDelegate = (APICacheStatusDelegate) statusDelegate;
}
if (errorDelegate == null) {
this.errorDelegate = getDefaultErrorDelegate(statusDelegate.getContext());
} else {
this.errorDelegate = errorDelegate;
}
if(this.errorDelegate == null){
Log.e(APIHelpers.LOG_TAG, "WARNING: No ErrorDelegate Set.");
}
}
///////////////////////////////////////////////////////////////////////////
// Helpers
///////////////////////////////////////////////////////////////////////////
/**
* Returns the default Error Delegate
* @param context
* @return
*/
public static ErrorDelegate getDefaultErrorDelegate(Context context){
if(defaultErrorDelegate == null ){
String defaultErrorDelegateClass = APIHelpers.getDefaultErrorDelegateClass(context);
if(defaultErrorDelegateClass != null){
try {
Class<?> errorDelegateClass = (Class.forName(defaultErrorDelegateClass));
defaultErrorDelegate = (ErrorDelegate) errorDelegateClass.newInstance();
} catch (Exception E) {
Log.e(APIHelpers.LOG_TAG,"WARNING: Invalid defaultErrorDelegateClass Set: "+defaultErrorDelegateClass);
}
}
}
return defaultErrorDelegate;
}
private void finishLoading() {
isFinished = true;
statusDelegate.onCallbackFinished(SOURCE.API);
}
/**
* @return Current context, can be null
*/
public Context getContext(){
return statusDelegate.getContext();
}
/**
* setShouldCache sets whether or not a call should be cached and the filename where it'll be cached to.
* Should only be called by the API
*/
@Deprecated
public void setShouldCache(String fileName) {
cacheFileName = fileName;
}
/**
* shouldCache is a helper for whether or not a cacheFileName has been set.
* @return
*/
@Deprecated
public boolean shouldCache() {
return cacheFileName != null;
}
/**
* Intended to work as AsyncTask.cancel() does.
* The network call is still made, but no response is made.
*
* Gotchas:
* Cache is still called.
* The callback has to be reinitialized as you can't 'uncancel'
*/
public void cancel(){
isCancelled = true;
}
/**
* readFromCache reads from the cache filename and simultaneously sets the cache filename
* Use {@link BuildInterfaceAPI#buildCacheInterface} instead
* @param path
*/
@Deprecated
public void readFromCache(String path) {
new ReadCacheData().execute(path);
}
@Deprecated
public boolean deleteCache(){
return FileUtilities.DeleteFile(getContext(), cacheFileName);
}
///////////////////////////////////////////////////////////////////////////
// Interface
///////////////////////////////////////////////////////////////////////////
/**
* cache is a function you can override to get the cached values.
* @param t
*/
@Deprecated
public void cache(T t) {
}
public void cache(T t, LinkHeaders linkHeaders, Response response) {
firstPage(t, linkHeaders, response);
}
/**
* firstPage is the first (or only in some cases) of the API response.
* @param t
* @param linkHeaders
* @param response
*/
public abstract void firstPage(T t, LinkHeaders linkHeaders, Response response);
/**
*
* nextPage is the second (or more) page of the API response.
* Defaults to calling firstPage
* Override if you want to change this functionality
* @param t
* @param linkHeaders
* @param response
*/
public void nextPage(T t, LinkHeaders linkHeaders, Response response){
firstPage(t, linkHeaders, response);
}
/**
* onFailure is a way to handle a failure instead using the
* default error handling
* @param retrofitError
* @return true if the failure was handled, false otherwise
*/
public boolean onFailure(RetrofitError retrofitError) {
return false;
}
///////////////////////////////////////////////////////////////////////////
// Retrofit callback methods
///////////////////////////////////////////////////////////////////////////
/**
* If you want caching and pagination, you must call this function using super or leave it alone..
* @param t
* @param response
*/
@Override
public void success(T t, Response response) {
RetrofitCounter.decrement();
// check if it's been cancelled or detached
Log.d("URL_STATUS", APIHelpers.isCachedResponse(response) ? "From cache " + response.getUrl() : "From API " + response.getUrl());
if(isCancelled || t == null || getContext() == null) {
return;
}
new CacheData(t, response).execute(t);
}
/**
* failure calls the correct method on the ErrorDelegate that's been set.
* @param retrofitError
*/
@Override
public void failure(RetrofitError retrofitError) {
RetrofitCounter.decrement();
// check if it's cancelled or detached
if (isCancelled || getContext() == null) {
return;
}
finishLoading();
Log.e(APIHelpers.LOG_TAG, "ERROR: " + retrofitError.getUrl());
Log.e(APIHelpers.LOG_TAG, "ERROR: " + retrofitError.getMessage());
// Return if the failure was already handled
if (onFailure(retrofitError)) {
return;
}
if (errorDelegate == null) {
Log.d(APIHelpers.LOG_TAG, "WARNING: No ErrorDelegate Provided ");
return;
}
CanvasError canvasError;
switch (retrofitError.getKind()) {
case CONVERSION:
canvasError = CanvasError.createError("Conversion Error", "An exception was thrown while (de)serializing a body");
errorDelegate.generalError(retrofitError, canvasError, getContext());
break;
case HTTP:
// A non-200 HTTP status code was received from the server.
handleHTTPError(retrofitError);
break;
case NETWORK:
// An IOException occurred while communicating to the server.
statusDelegate.onNoNetwork();
errorDelegate.noNetworkError(retrofitError, getContext());
break;
case UNEXPECTED:
canvasError = CanvasError.createError("Unexpected Error", "An internal error occurred while attempting to execute a request.");
errorDelegate.generalError(retrofitError, canvasError, getContext());
break;
default:
canvasError = CanvasError.createError("Unexpected Error", "An unexpected error occurred.");
errorDelegate.generalError(retrofitError, canvasError, getContext());
break;
}
}
private void handleHTTPError(RetrofitError retrofitError) {
Response response = retrofitError.getResponse();
if (response == null) {
return;
}
Log.e(APIHelpers.LOG_TAG, "Response code: " + response.getStatus());
Log.e(APIHelpers.LOG_TAG, "Response body: " + response.getBody());
CanvasError canvasError = null;
try {
canvasError = (CanvasError) retrofitError.getBodyAs(CanvasError.class);
} catch (Exception exception) {
}
if (response.getStatus() == 200) {
errorDelegate.generalError(retrofitError, canvasError, getContext());
} else if (response.getStatus() == 401) {
errorDelegate.notAuthorizedError(retrofitError, canvasError, getContext());
} else if (response.getStatus() >= 400 && response.getStatus() < 500) {
errorDelegate.invalidUrlError(retrofitError, getContext());
} else if (response.getStatus() >= 500 && response.getStatus() < 600) {
//don't do anything for a 504 (Unsatisfiable Request (only-if-cached)).
//It will happen when we try to read from the http cache and there isn't
//anything there
if (response.getStatus() == 504 && APIHelpers.isCachedResponse(response)) {
if (!CanvasRestAdapter.isNetworkAvaliable(getContext())) { // Purposely not part of the above if statement. First if statement is prevent the error delegate from a 504 cache response
statusDelegate.onNoNetwork(); // Only call when no items were cached and there isn't a network
}
// do nothing
} else {
errorDelegate.serverError(retrofitError, getContext());
}
}
}
public static enum SOURCE{
API, CACHE;
public boolean isAPI(){
return this == API;
}
public boolean isCache(){
return this == CACHE;
}
}
private class CacheData extends AsyncTask<T, Void, LinkHeaders> {
private T t;
private Response response;
public CacheData(T t, Response response) {
this.t = t;
this.response = response;
}
@Override
protected LinkHeaders doInBackground(T... params) {
LinkHeaders linkHeaders = APIHelpers.parseLinkHeaderResponse(getContext(), response.getHeaders());
if (shouldCache() && !isNextPage && getContext() != null) {
if(t instanceof Serializable) {
try {
FileUtilities.SerializableToFile(getContext(), cacheFileName, (Serializable)params[0]);
} catch (Exception E) {
Log.e(APIHelpers.LOG_TAG, "Could not cache serializable: " + E);
}
}
}
return linkHeaders;
}
@Override
protected void onPostExecute(LinkHeaders linkHeaders) {
super.onPostExecute(linkHeaders);
if (isCancelled) {
return;
}
boolean isCache = APIHelpers.isCachedResponse(response);
boolean isIgnoreCache = false;
if (cacheStatusDelegate != null) {
isIgnoreCache = cacheStatusDelegate.shouldIgnoreCache();
}
if (isCache && !isIgnoreCache) {
Log.v(APIHelpers.LOG_TAG, "Cache");
cache(t);
cache(t, linkHeaders, response);
statusDelegate.onCallbackFinished(SOURCE.CACHE);
} else if (isNextPage) {
nextPage(t, linkHeaders, response);
statusDelegate.onCallbackFinished(SOURCE.API);
} else if (!isCache) {
firstPage(t, linkHeaders, response);
statusDelegate.onCallbackFinished(SOURCE.API);
// since we have had a successful network call, reset the variable that tracks whether the user has seen the
// no network error
if (getContext() != null) {
APIHelpers.setHasSeenNetworkErrorMessage(getContext(), false);
}
}
isFinished = true;
}
}
private class ReadCacheData extends AsyncTask<String, Void, Serializable> {
private String path = null;
@Override
protected Serializable doInBackground(String... params) {
path = params[0];
try {
return FileUtilities.FileToSerializable(getContext(), path);
} catch (Exception E) {
Log.e(APIHelpers.LOG_TAG, "NO CACHE: " + path);
}
return null;
}
@Override
protected void onPostExecute(Serializable serializable) {
super.onPostExecute(serializable);
if (serializable != null && getContext() != null) {
cache((T) serializable);
}
setHasReadFromCache(true);
setShouldCache(path);
statusDelegate.onCallbackFinished(SOURCE.CACHE);
}
}
}