/* * Copyright (C) 2012 Eyal LEZMY (http://www.eyal.fr) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package fr.eyal.lib.data.service; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.EventListener; import java.util.HashMap; import java.util.Map; import android.annotation.TargetApi; import android.content.Context; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.ResultReceiver; import android.util.SparseArray; import fr.eyal.lib.data.communication.rest.ParameterMap; import fr.eyal.lib.data.model.ResponseBusinessObject; import fr.eyal.lib.data.service.model.BusinessResponse; import fr.eyal.lib.data.service.model.ComplexOptions; import fr.eyal.lib.data.service.model.DataLibRequest; import fr.eyal.lib.util.FileManager; import fr.eyal.lib.util.Out; /** * @author Eyal LEZMY */ public class ServiceHelper { private static final String LOG_TAG = ServiceHelper.class.getSimpleName(); private static final int MAX_RANDOM_REQUEST_ID = Integer.MAX_VALUE; private static int REQUEST_ID_CPT = 0; public static int BAD_REQUEST_ID = -1; /** * Singleton of the ServiceHelper */ protected static ServiceHelper sInstance; /** * List of requests currently computing */ protected SparseArray<Intent> mRequestSparseArray; /** * Context of execution */ protected Context mContext; /** * Sparse List of RequestFinishedListener. Each request can be connected to one or several listeners */ protected SparseArray<ArrayList<OnRequestFinishedListener>> mListenersSparseArray; /** * List or relayers. Every relayers wil receive each Bundle comming from the Service */ protected ArrayList<OnRequestFinishedRelayer> mRelayersArray; /** * HandlerThread used by ServiceHelper */ protected ServiceHelperWorker mHandlerThread; /** * Handler used by the EvalReceiver */ protected Handler mHandler; /** * Receiver that receives the result from the DataLibService */ protected EvalReceiver mEvalReceiver; //List of bundled information that can be received from the Service protected static final String RECEIVER_EXTRA_REQUEST_ID = "requestId"; protected static final String RECEIVER_EXTRA_RESULT = "result"; protected static final String RECEIVER_EXTRA_RESULT_ID = "resultId"; protected static final String RECEIVER_EXTRA_REQUEST = "request"; protected static final String RECEIVER_EXTRA_RETURN_CODE = "returnCode"; protected static final String RECEIVER_EXTRA_RESULT_CODE = "resultCode"; protected static final String RECEIVER_EXTRA_RESULT_MESSAGE = "Message"; protected static final String RECEIVER_EXTRA_WEBSERVICE_TYPE = "webserviceType"; public static final String RECEIVER_EXTRA_RESULT_OPTIONS = "options"; public static final String RECEIVER_EXTRA_RESULT_COMPLEX_OPTIONS = "complexOptions"; private static final String HANDLER_THREAD_NAME = "ServiceHelper-Thread"; /** * Get the instance of the {@link ServiceHelper} * * @param context The context of execution. Any Context can be put here, the application context will be automatically used for the {@link ServiceHelper} * @return Returns the singleton */ public static ServiceHelper getInstance(final Context context) { synchronized (ServiceHelper.class) { if (sInstance == null) { sInstance = new ServiceHelper(context.getApplicationContext()); } } return sInstance; } protected ServiceHelper(final Context context) { mContext = context; mRequestSparseArray = new SparseArray<Intent>(); mListenersSparseArray = new SparseArray<ArrayList<OnRequestFinishedListener>>(); mRelayersArray = new ArrayList<OnRequestFinishedRelayer>(); //we initialize the FileManager FileManager.getInstance(context); mHandler = new Handler(); mEvalReceiver = new EvalReceiver(mHandler); // //we create the HandlerThread that will take care of the Service responses through the EvalReciever // mHandlerThread = new ServiceHelperWorker(HANDLER_THREAD_NAME); // mHandlerThread.start(); } /** * The HandlerThread that will take care of the Service responses through the EvalReciever */ class ServiceHelperWorker extends HandlerThread { public ServiceHelperWorker(String name) { super(name); } @Override public synchronized void start() { super.start(); mHandler = new Handler(getLooper()); mEvalReceiver = new EvalReceiver(mHandler); } } /** * The ResultReceiver that will receive the result from the Service */ private class EvalReceiver extends ResultReceiver { @TargetApi(Build.VERSION_CODES.CUPCAKE) EvalReceiver(final Handler h) { super(h); } @Override public void onReceiveResult(final int resultCode, final Bundle resultData) { handleResult(resultCode, resultData); } } /** * Clients may implements this interface to be notified when a request is finished */ public static interface OnRequestFinishedListener extends EventListener { /** * Event fired when a request is finished. * * @param requestId the id of the request (to see if this is the right request) * @param resultCode the result code (-1 if there was an error) * @param payload the payload. Can be any Object depending on the request */ public void onRequestFinished(int requestId, boolean suceed, BusinessResponse response); } /** * DataManager may implements this interface to be notified when a request is finished */ public static interface OnRequestFinishedRelayer extends EventListener { /** * Event fired when a request is finished. * * @param resultCode the result code (-1 if there was an error) * @param resultData the result Bundle received from the Service */ public void onRequestFinished(int resultCode, Bundle resultData); } /** * Add a {@link OnRequestFinishedListener} to this {@link ServiceHelper}. Clients may use it in order to listen to events fired when a request is finished. * <p> * <b>Warning !! </b> The listener must be detached when onPause is called in an Activity. * </p> * * @param listener The listener to add to this {@link ServiceHelper}. */ public synchronized void addOnRequestFinishedListener(final int requestId, final OnRequestFinishedListener listener) { if (listener == null) return; ArrayList<OnRequestFinishedListener> listeners = mListenersSparseArray.get(requestId); if (listeners == null) { listeners = new ArrayList<OnRequestFinishedListener>(); mListenersSparseArray.append(requestId, listeners); } if (!listeners.contains(listener)) { listeners.add(listener); Out.i(LOG_TAG, requestId + " Adding listener " + listener); } else { Out.i(LOG_TAG, requestId + " Already contains " + listener); } } /** * Remove a {@link OnRequestFinishedListener} to this {@link ServiceHelper}. * * @param listenerThe listener to remove to this {@link ServiceHelper}. */ public synchronized void removeOnRequestFinishedListener(final int requestId, final OnRequestFinishedListener listener) { if (listener == null) return; ArrayList<OnRequestFinishedListener> listeners = mListenersSparseArray.get(requestId); if (listeners != null) { listeners.remove(listener); Out.i(LOG_TAG, requestId + " Suppression du listener " + listener); } else { Out.i(LOG_TAG, requestId + " Pas de liste de listeners"); } } /** * Add a {@link OnRequestFinishedRelayer} to this {@link ServiceHelper}. Clients may use it in order to listen to events fired when a request is finished. * <p> * <b>Warning !! </b> The listener must be detached when onPause is called in an Activity. * </p> * * @param relayer The relayer to add to this {@link ServiceHelper}. */ public synchronized void addOnRequestFinishedRelayer(final OnRequestFinishedRelayer relayer) { if (relayer == null) return; if (!mRelayersArray.contains(relayer)) { mRelayersArray.add(relayer); } } /** * Remove a {@link OnRequestFinishedRelayer} to this {@link ServiceHelper}. * * @param relayer The relayer to remove to this {@link ServiceHelper}. */ public synchronized void removeOnRequestFinishedRelayer(final OnRequestFinishedRelayer relayer) { if (relayer == null) return; mRelayersArray.remove(relayer); } /** * Cancel the request. This cancel the response's return of the request * * @param id The id of the requests */ public synchronized void cancelRequest(final int id) { //we erase the request from the requests list mRequestSparseArray.delete(id); } /** * Returns whether a request (specified by an id) is still in progress or not * * @param requestId the id of the request * @return */ public boolean isRequestInProgress(final int requestId) { return (mRequestSparseArray.indexOfKey(requestId) >= 0); } /** * This method manages the result of the service */ protected synchronized void handleResult(final int resultCode, final Bundle resultData) { int requestId = resultData.getInt(RECEIVER_EXTRA_REQUEST_ID); int webserviceType = resultData.getInt(RECEIVER_EXTRA_WEBSERVICE_TYPE); int returnCode = resultData.getInt(RECEIVER_EXTRA_RETURN_CODE); String statutMessage = resultData.getString(RECEIVER_EXTRA_RESULT_MESSAGE); //state succeed or not of the request (to be sent to the OnRequestFinishedListener) boolean succeed = (resultCode == BusinessResponse.STATUS_OK); //if the request is still active if (mRequestSparseArray.indexOfKey(requestId) >= 0) { //we erase the request from the requests list mRequestSparseArray.delete(requestId); //if there is at least one relayer if (mRelayersArray.size() > 0) { for (OnRequestFinishedRelayer relayer : mRelayersArray) { relayer.onRequestFinished(resultCode, resultData); } } //we extract the different listeners linked to this request ArrayList<OnRequestFinishedListener> listeners = mListenersSparseArray.get(requestId); //if there is a least one listener we send the signal if (listeners != null && listeners.size() > 0) { //we build the BusinessResponse BusinessResponse businessResponse = new BusinessResponse(); businessResponse.webserviceType = webserviceType; businessResponse.returnCode = returnCode; businessResponse.status = resultCode; businessResponse.statusMessage = statutMessage; //we add the response business object businessResponse.response = (ResponseBusinessObject) resultData.getParcelable(RECEIVER_EXTRA_RESULT); DataLibRequest request = resultData.getParcelable(RECEIVER_EXTRA_REQUEST); boolean runOnUIThread = request.isResponseRunningOnUIThread(); //we send the result to each Listener, waiting for this request's response for (OnRequestFinishedListener listener : listeners) { //if the option asks to run the callback on the UI Thread and e are not on the UI Thread if(runOnUIThread && Thread.currentThread() != Looper.getMainLooper().getThread()){ Handler mainThread = new Handler(Looper.getMainLooper()); mainThread.post(new ResponseRunnable(listener, requestId, succeed, businessResponse) ); //we launch it on the UI Thread // else we launch the callback on the same thread } else { listener.onRequestFinished(requestId, succeed, businessResponse); } } //we delete the erase the request's listeners mListenersSparseArray.delete(requestId); Out.i(LOG_TAG, "Response " + requestId + " sent!"); //if there is no listener } else { //we store the response to be able to send it again later Out.i(LOG_TAG, "The response " + requestId + " does not have any listener"); } } } /** * FUNCTIONS TO SEND REQUESTS TO THE SERVICE */ /** * This method ask to the DataLib to flush the list of cookies stored. After executing this, the next client's request won't send any cookie. */ public synchronized void flushCookies() { Intent i = new Intent(mContext, DataLibService.class); i.putExtra(DataLibService.INTENT_EXTRA_PROCESSOR_TYPE, DataLibService.COOKIES_FLUSH); mContext.startService(i); } public synchronized static int generateRequestId() { int id = REQUEST_ID_CPT++; if (id > MAX_RANDOM_REQUEST_ID) REQUEST_ID_CPT = 0; return id; } public int checkRequestExists(final Intent intent) { String sourceUrl = intent.getStringExtra(DataLibService.INTENT_EXTRA_URL); ParameterMap sourceParams = intent.getParcelableExtra(DataLibService.INTENT_EXTRA_PARAMS); String sourceParamsUrl = ""; if(sourceParams != null) sourceParamsUrl = sourceParams.urlEncode(true); // Check if a request is already launched final int requestSparseArrayLength = mRequestSparseArray.size(); for (int i = 0; i < requestSparseArrayLength; i++) { //TODO check the exception occuring sometime here when there is a very intensive request rythme final Intent savedIntent = mRequestSparseArray.valueAt(i); if(savedIntent == null) continue; String savedUrl = savedIntent.getStringExtra(DataLibService.INTENT_EXTRA_URL); ParameterMap savedParams = savedIntent.getParcelableExtra(DataLibService.INTENT_EXTRA_PARAMS); String savedParamsUrl = ""; if(savedParams != null) savedParamsUrl = savedParams.urlEncode(true); //we compare the whole URL if (savedUrl.equals(sourceUrl) && savedParamsUrl.equals(sourceParamsUrl)) return mRequestSparseArray.keyAt(i); } return BAD_REQUEST_ID; } /** * Launch the request to the Service through an Intent * * @param options * @param webserviceType * @param params * @param service * @param url * @return * @throws UnsupportedEncodingException */ public int launchRequest(final int options, final int webserviceType, final ParameterMap params, final Class<?> service, final String url, final ComplexOptions complexOptions) throws UnsupportedEncodingException { return launchRequest(options, webserviceType, params, service, url, complexOptions, -1); } /** * Launch the request to the Service through an Intent * * @param options * @param webserviceType * @param params * @param service * @param url * @param parseType * @return * @throws UnsupportedEncodingException */ public int launchRequest(final int options, final int webserviceType, final ParameterMap params, final Class<?> service, final String url, final ComplexOptions complexOptions, final int parseType) throws UnsupportedEncodingException { //we buid the Intent to send Intent i = new Intent(mContext, service); i.putExtra(DataLibService.INTENT_EXTRA_RECEIVER, mEvalReceiver); i.putExtra(DataLibService.INTENT_EXTRA_PROCESSOR_TYPE, webserviceType); i.putExtra(DataLibService.INTENT_EXTRA_REQUEST_OPTION, options); if (parseType > 0) i.putExtra(DataLibService.INTENT_EXTRA_PARSE_TYPE, parseType); i.putExtra(DataLibService.INTENT_EXTRA_URL, url); i.putExtra(DataLibService.INTENT_EXTRA_PARAMS, params); i.putExtra(DataLibService.INTENT_EXTRA_COMPLEX_OPTIONS, complexOptions); int requestId = checkRequestExists(i); //if a requestId have been found if (requestId > 0) return requestId; //we return it to the user requestId = generateRequestId(); mListenersSparseArray.append(requestId, new ArrayList<ServiceHelper.OnRequestFinishedListener>()); i.putExtra(DataLibService.INTENT_EXTRA_REQUEST_ID, requestId); mContext.startService(i); mRequestSparseArray.append(requestId, i); return requestId; } /** * Get the URL which is identifying a specific request thanks to its base url and the parameters * * @param urlBase the base url of the service * @param params a HashMap the parameters to transmit * @return returns the url generated * @throws UnsupportedEncodingException */ public static String getServiceUrl(final String urlBase, final ParameterMap params) throws UnsupportedEncodingException { return urlBase + "?" + params.urlEncode(); } /** * Get the application context * * @return returns the application context if defined or <code>null</code> */ public Context getApplicationContext(){ return mContext; } }