/** * 2011 Foxykeep (http://datadroid.foxykeep.com) * <p> * Licensed under the Beerware License : <br /> * As long as you retain this notice you can do whatever you want with this stuff. If we meet some * day, and you think this stuff is worth it, you can buy me a beer in return */ package com.foxykeep.datadroid.requestmanager; import com.foxykeep.datadroid.service.RequestService; import com.foxykeep.datadroid.util.DataDroidLog; import android.app.Activity; import android.app.Fragment; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.ResultReceiver; import android.support.v4.util.LruCache; import org.apache.http.HttpStatus; import java.lang.ref.WeakReference; import java.util.Collections; import java.util.EventListener; import java.util.HashMap; import java.util.HashSet; import java.util.Set; /** * This class allows to send requests through a {@link RequestService}. * <p> * This class needs to be subclassed in your project. * <p> * You can check the following page to see a tutorial on how to implement a webservice call using * the {@link RequestManager} : <a * href="http://www.datadroidlib.com/installation">http://www.datadroidlib.com/installation</a>. * * @author Foxykeep */ public abstract class RequestManager { private static final String TAG = RequestManager.class.getSimpleName(); /** * Clients may implements this interface to be notified when a request is finished. * * @author Foxykeep */ public static interface RequestListener extends EventListener { /** * Event fired when a request is finished. * * @param request The {@link Request} defining the request. * @param resultData The result of the service execution. */ public void onRequestFinished(Request request, Bundle resultData); /** * Event fired when a request encountered a connection error. * * @param request The {@link Request} defining the request. * @param statusCode The HTTP status code returned by the server (if the request succeeded * by the HTTP status code was not {@link HttpStatus#SC_OK}) or -1 if it was a * connection problem */ public void onRequestConnectionError(Request request, int statusCode); /** * Event fired when a request encountered a data error. * * @param request The {@link Request} defining the request. */ public void onRequestDataError(Request request); /** * Event fired when a request encountered a custom error. * * @param request The {@link Request} defining the request. * @param resultData The result of the service execution. */ public void onRequestCustomError(Request request, Bundle resultData); } public static final String RECEIVER_EXTRA_ERROR_TYPE = "com.foxykeep.datadroid.extra.error"; public static final String RECEIVER_EXTRA_CONNECTION_ERROR_STATUS_CODE = "com.foxykeep.datadroid.extra.connectionErrorStatusCode"; public static final int ERROR_TYPE_CONNEXION = 1; public static final int ERROR_TYPE_DATA = 2; public static final int ERROR_TYPE_CUSTOM = 3; private final Context mContext; private final Class<? extends RequestService> mRequestService; private final HashMap<Request, RequestReceiver> mRequestReceiverMap; private final LruCache<Request, Bundle> mMemoryCache; protected RequestManager(Context context, Class<? extends RequestService> requestService) { mContext = context.getApplicationContext(); mRequestService = requestService; mRequestReceiverMap = new HashMap<Request, RequestReceiver>(); mMemoryCache = new LruCache<Request, Bundle>(30); } /** * Add a {@link RequestListener} to this {@link RequestManager} to a specific {@link Request}. * Clients may use it in order to be notified when the corresponding request is completed. * <p> * The listener is automatically removed when the request is completed and they are notified. * <p> * <b>Warning !! </b> If it's an {@link Activity} or a {@link Fragment} that is used as a * listener, it must be detached when {@link Activity#onPause} is called in an {@link Activity}. * * @param listener The listener called when the Request is completed. * @param request The {@link Request} to listen to. */ public final void addRequestListener(RequestListener listener, Request request) { if (listener == null) { return; } if (request == null) { throw new IllegalArgumentException("Request cannot be null."); } RequestReceiver requestReceiver = mRequestReceiverMap.get(request); if (requestReceiver == null) { DataDroidLog.w(TAG, "You tried to add a listener to a non-existing request."); return; } requestReceiver.addListenerHolder(new ListenerHolder(listener)); } /** * Remove a {@link RequestListener} to this {@link RequestManager} from every {@link Request}s * which it is listening to. * * @param listener The listener to remove. */ public final void removeRequestListener(RequestListener listener) { removeRequestListener(listener, null); } /** * Remove a {@link RequestListener} to this {@link RequestManager} from a specific * {@link Request}. * * @param listener The listener to remove. * @param request The {@link Request} associated with this listener. If null, the listener will * be removed from every request it is currently associated with. */ public final void removeRequestListener(RequestListener listener, Request request) { if (listener == null) { return; } ListenerHolder holder = new ListenerHolder(listener); if (request != null) { RequestReceiver requestReceiver = mRequestReceiverMap.get(request); if (requestReceiver != null) { requestReceiver.removeListenerHolder(holder); } } else { for (RequestReceiver requestReceiver : mRequestReceiverMap.values()) { requestReceiver.removeListenerHolder(holder); } } } /** * Return whether a {@link Request} is still in progress or not. * * @param request The request. * @return Whether the request is still in progress or not. */ public final boolean isRequestInProgress(Request request) { return mRequestReceiverMap.containsKey(request); } /** * Call the given listener <b>synchronously</b> with the memory cached data corresponding to the * request. * <p> * The method called in the listener will be * {@link RequestListener#onRequestFinished(Request, Bundle)}. * <p> * If no cached data is found, {@link RequestListener#onRequestConnectionError(Request, int)} * will be called instead * * @param listener The listener to call with the data if any. * @param request The request associated with the memory cached data. */ public final void callListenerWithCachedData(RequestListener listener, Request request) { if (request == null) { throw new IllegalArgumentException("Request cannot be null."); } if (listener == null) { return; } if (request.isMemoryCacheEnabled()) { Bundle bundle = mMemoryCache.get(request); if (bundle != null) { listener.onRequestFinished(request, bundle); } else { listener.onRequestConnectionError(request, -1); } } } /** * Execute the {@link Request}. * * @param request The request to execute. * @param listener The listener called when the Request is completed. */ public final void execute(Request request, RequestListener listener) { if (request == null) { throw new IllegalArgumentException("Request cannot be null."); } if (mRequestReceiverMap.containsKey(request)) { DataDroidLog.d(TAG, "This request is already in progress. Adding the new listener to it."); // This exact request is already in progress. Adding the new listener. addRequestListener(listener, request); // Just check if the new request has the memory cache enabled. if (request.isMemoryCacheEnabled()) { // If true, enable it in the RequestReceiver (if it's not the case already) mRequestReceiverMap.get(request).enableMemoryCache(); } return; } DataDroidLog.d(TAG, "Creating a new request and adding the listener to it."); RequestReceiver requestReceiver = new RequestReceiver(request); mRequestReceiverMap.put(request, requestReceiver); addRequestListener(listener, request); Intent intent = new Intent(mContext, mRequestService); intent.putExtra(RequestService.INTENT_EXTRA_RECEIVER, requestReceiver); intent.putExtra(RequestService.INTENT_EXTRA_REQUEST, request); mContext.startService(intent); } private final class RequestReceiver extends ResultReceiver { private final Request mRequest; private final Set<ListenerHolder> mListenerHolderSet; private boolean mMemoryCacheEnabled; /* package */ RequestReceiver(Request request) { super(new Handler(Looper.getMainLooper())); mRequest = request; mListenerHolderSet = Collections.synchronizedSet(new HashSet<ListenerHolder>()); mMemoryCacheEnabled = request.isMemoryCacheEnabled(); // Clear the old memory cache if any mMemoryCache.remove(request); } /* package */ void enableMemoryCache() { mMemoryCacheEnabled = true; } /* package */ void addListenerHolder(ListenerHolder listenerHolder) { synchronized (mListenerHolderSet) { mListenerHolderSet.add(listenerHolder); } } /* package */ void removeListenerHolder(ListenerHolder listenerHolder) { synchronized (mListenerHolderSet) { mListenerHolderSet.remove(listenerHolder); } } @Override public void onReceiveResult(int resultCode, Bundle resultData) { if (mMemoryCacheEnabled) { mMemoryCache.put(mRequest, resultData); } mRequestReceiverMap.remove(mRequest); // Call the available listeners synchronized (mListenerHolderSet) { for (ListenerHolder listenerHolder : mListenerHolderSet) { listenerHolder.onRequestFinished(mRequest, resultCode, resultData); } } } } private final class ListenerHolder { private final WeakReference<RequestListener> mListenerRef; private final int mHashCode; /* package */ ListenerHolder(RequestListener listener) { mListenerRef = new WeakReference<RequestListener>(listener); mHashCode = 31 + listener.hashCode(); } /* package */ void onRequestFinished(Request request, int resultCode, Bundle resultData) { mRequestReceiverMap.remove(request); RequestListener listener = mListenerRef.get(); if (listener != null) { if (resultCode == RequestService.ERROR_CODE) { switch (resultData.getInt(RECEIVER_EXTRA_ERROR_TYPE)) { case ERROR_TYPE_DATA: listener.onRequestDataError(request); break; case ERROR_TYPE_CONNEXION: int statusCode = resultData.getInt(RECEIVER_EXTRA_CONNECTION_ERROR_STATUS_CODE); listener.onRequestConnectionError(request, statusCode); break; case ERROR_TYPE_CUSTOM: listener.onRequestCustomError(request, resultData); break; } } else { listener.onRequestFinished(request, resultData); } } } @Override public boolean equals(Object o) { if (o instanceof ListenerHolder) { ListenerHolder oHolder = (ListenerHolder) o; return mListenerRef != null && oHolder.mListenerRef != null && mHashCode == oHolder.mHashCode; } return false; } @Override public int hashCode() { return mHashCode; } } }