/* * 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 android.content.ContentResolver; import android.content.Context; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Process; import android.util.SparseArray; import fr.eyal.lib.data.communication.rest.ParameterMap; import fr.eyal.lib.data.model.BusinessObject; import fr.eyal.lib.data.model.BusinessObjectDAO; import fr.eyal.lib.data.model.ResponseBusinessObject; import fr.eyal.lib.data.model.provider.BusinessObjectProvider; import fr.eyal.lib.data.service.ServiceHelper.OnRequestFinishedListener; import fr.eyal.lib.data.service.ServiceHelper.OnRequestFinishedRelayer; 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.Out; /** * @author Eyal LEZMY */ public abstract class DataManager implements OnRequestFinishedRelayer { private static final String LOG_TAG = DataManager.class.getSimpleName(); /** * singleton of the class */ protected static DataManager sInstance; /** * ServiceHelper to contact the DataLib */ protected ServiceHelper mServiceHelper; /** * List of listeners who will receive the responses from the DataLib */ protected SparseArray<ArrayList<OnDataListener>> mListenersSparseArray; /** * List of responses stored into the DataManager */ protected SparseArray<Bundle> mDataLibResponsesSparseArray; /** * Connectivity Manager to access to the network configuration */ protected ConnectivityManager mConnectivityManager; /** * The content resolver of the application */ protected ContentResolver mContentResolver; /** * The cache request ids */ protected SparseArray<Object> mDataCacheRequestIds; /* * different types of policies for the request */ /** * <b>Network only</b> This value defines a DataManager request as a "network only" treatment. The DataManager have only to use the DataLib to get the * response. */ public static final int TYPE_NETWORK = 0; /** * <b>Cache only</b> This value defines a DataManager request as a "cache only" treatment. The DataManager have only to use the DataCache to get the * response. */ public static final int TYPE_CACHE = 1; /** * <b>Network otherwise cache</b> This value defines a DataManager request that have to check the connectivity. If a network request is possible it have to * start a network request, if not, it fetchs the information from the cache. */ public static final int TYPE_NETWORK_OTHERWISE_CACHE = 2; /** * <b>Cache then network</b> This value defines a DataManager request that have to send a network request. While waiting for the network response, it has to * fetch an eventual result inside the cache and sending it to the controller. */ public static final int TYPE_CACHE_THEN_NETWORK = 3; //Response returned by the "retrieve" functions public static final int DATACACHE_REQUEST = -1; //The request is a DataCache request public static final int BAD_REQUEST = -2; //An error exists in the function's parameters protected DataManager(final Context context) { // mServiceHelper = ServiceHelper.getInstance(context); // mServiceHelper.addOnRequestFinishedRelayer(this); mListenersSparseArray = new SparseArray<ArrayList<OnDataListener>>(); mDataLibResponsesSparseArray = new SparseArray<Bundle>(); mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); mContentResolver = context.getContentResolver(); mDataCacheRequestIds = new SparseArray<Object>(); } /** * Clients may implements this interface to be notified when a data (network result, cache data or database content) is returned */ public static interface OnDataListener extends ServiceHelper.OnRequestFinishedListener { /** * Event fired when a cache request is finished. * * @param response is the response returned by the cache. */ public void onCacheRequestFinished(int requestId, ResponseBusinessObject response); /** * Event fired when a database query is finished. * * @param code The code that point out the type of the data. It must correspond to the {@link BusinessObjectProvider} constants beginning by "CODE" and * then the name of the expected table (ex: CODE_PREVISION_METEO) * @param data The data array returned after the treatment of the query. The type inside the array inherits from {@link BusinessObjectDAO} */ public void onDataFromDatabase(int code, ArrayList<?> data); } /** * 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 <b>must</b> be detached when onPause is called in an Activity. * </p> * * @param listener The listener to add to this {@link ServiceHelper}. */ public synchronized void addOnDataListener(final int requestId, final OnDataListener listener) { if (listener == null) { Out.i(LOG_TAG, requestId + " adding listener impossible: " + listener); return; } ArrayList<OnDataListener> listeners = mListenersSparseArray.get(requestId); if (listeners == null) { listeners = new ArrayList<OnDataListener>(); 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 removeOnDataListener(final int requestId, final OnDataListener listener) { if (listener == null) return; final ArrayList<OnDataListener> listeners = mListenersSparseArray.get(requestId); if (listeners != null) { listeners.remove(listener); Out.i(LOG_TAG, requestId + " listener removed" + listener); } else { Out.i(LOG_TAG, requestId + " no listener"); } } /** * 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) { if (mDataCacheRequestIds.indexOfKey(id) >= 0) mDataCacheRequestIds.delete(id); mServiceHelper.cancelRequest(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 mServiceHelper.isRequestInProgress(requestId); } /** * Launch the response handling asynchronously * TODO determine if this function is still useful * * @param requestId the request ID * * @return <b>true</b> if the request have been found among the non treated request and <b>false</b> if not */ protected boolean launchResponse(final int requestId) { final Bundle bundle = mDataLibResponsesSparseArray.get(requestId); if (bundle == null) return false; else { //we start launching the response in another thread final Thread responseThread = new Thread(new Runnable() { @Override public void run() { handleResult(bundle.getInt(ServiceHelper.RECEIVER_EXTRA_RESULT_CODE), bundle); } }); responseThread.start(); return true; } } /** * This method manages the result of the {@link ServiceHelper} */ protected synchronized void handleResult(final int resultCode, final Bundle resultData) { final int requestId = resultData.getInt(ServiceHelper.RECEIVER_EXTRA_REQUEST_ID); final int webserviceType = resultData.getInt(ServiceHelper.RECEIVER_EXTRA_WEBSERVICE_TYPE); final int returnCode = resultData.getInt(ServiceHelper.RECEIVER_EXTRA_RETURN_CODE); final String statutMessage = resultData.getString(ServiceHelper.RECEIVER_EXTRA_RESULT_MESSAGE); Out.w(LOG_TAG, "Handle RESULT " + requestId + " !!!"); //state succeed or not of the request (to be sent to the OnRequestFinishedListener) final boolean succeed = (resultCode == BusinessResponse.STATUS_OK); //we extract the different listeners linked to this request final ArrayList<OnDataListener> listeners = mListenersSparseArray.get(requestId); //we delete the erase the request and its listeners mListenersSparseArray.delete(requestId); mDataLibResponsesSparseArray.delete(requestId); //if there is a least one listener we send the signal if (listeners != null && listeners.size() > 0) { //we build the BusinessResponse final BusinessResponse businessResponse = new BusinessResponse(); businessResponse.webserviceType = webserviceType; businessResponse.returnCode = returnCode; businessResponse.status = resultCode; businessResponse.statusMessage = statutMessage; ResponseBusinessObject response = resultData.getParcelable(ServiceHelper.RECEIVER_EXTRA_RESULT); //if there is nothing to get from the Bundle if (response == null) { //we check the result id long resultId = resultData.getLong(ServiceHelper.RECEIVER_EXTRA_RESULT_ID); //if it is present if (resultId != BusinessObjectDAO.ID_INVALID) response = getResponseBusinessObjectById(webserviceType, resultId); //we get te result from te database } businessResponse.response = response; DataLibRequest request = resultData.getParcelable(ServiceHelper.RECEIVER_EXTRA_REQUEST); boolean runOnUIThread = request.isResponseRunningOnUIThread(); for (final OnRequestFinishedListener listener : listeners) { //if the option ask to run the callback 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); } } //if there is no listener } else { //we store the response to be able to send it again later mDataLibResponsesSparseArray.append(requestId, resultData); Out.i(LOG_TAG, "Reponse " + requestId + " mise en attente"); } } /** * Get a ResponseBusinessObject from its type and its id * * @param webserviceType type of the webservice * @param id the id of the {@link ResponseBusinessObject} * @return return the corresponding object or null if the webserviceType is not found */ protected abstract ResponseBusinessObject getResponseBusinessObjectById(int webserviceType, long id); @Override public void onRequestFinished(final int resultCode, final Bundle resultData) { handleResult(resultCode, resultData); } /** * FUNCTIONS TO SEND REQUESTS TO THE DATASERVICE OR RETURN INFORMATION FROM THE DATACACHE */ /** * This method ask to the DataLib to flush the list of cookies stored. After executing this, the next client request won't send any cookie. */ public synchronized void flushCookies() { mServiceHelper.flushCookies(); } /** * Function used to send the datacahe to a listener * * @param listener The listener who will receive the cache data * @param url The url field in the database to get the response from * @return Returns the id to return to the launcher of the retrieve function */ protected int sendDataCache(final OnDataListener listener, final String url, final int type, final int options, ComplexOptions complexOptions) { //we create and launch the data cache access final int requestId = ServiceHelper.generateRequestId(); final DataCacheRunnable runnable = new DataCacheRunnable(requestId, url, type, listener, options, complexOptions); final Thread thread = new Thread(runnable); thread.start(); return requestId; } /** * Launch a request * * @param serviceHelper the {@link ServiceHelper} to use to launch the request * * @param policy Give the policy context of the request using CACHE and/or NETWORK. * * @param datacacheListener The listener who will receive the data from the cache. * This parameter IS NEEDED in case of Datacache access (TYPE_CACHE, TYPE_CACHE_THEN_NETWORK * and TYPE_NETWORK_OTHERWISE_CACHE). This listener won't be used to send DataLib's response. * So, the addOnRequestFinishedListener call is still needed. * * @param params the request parameters * * @param options The options added to the request. The list of constants to use in this filed * can be found in {@link DataLibRequest} (ex: OPTION_CONSERVE_COOKIE or OPTION_NO_DATABASE). * The options can be aggregated thanks to the pipe character '|' (ex: OPTION_CONSERVE_COOKIE | * OPTION_NO_DATABASE). * * @param url the URL of the web service defined inside the {@link ServiceHelper} * * @param webService the web service type defined inside the {@link DataLibService} * * @param serviceClass the {@link Class} of the {@link DataLibService} of the project * * @return Returns the request Id if it have been generated by the DataLib. If there is only * a Datacache access, the id returned is the constant {@link DataManager#DATACACHE_REQUEST}. * In case of treatment error, it returns {@link DataManager#BAD_REQUEST}. * * @throws UnsupportedEncodingException * */ protected int launchRequest(final ServiceHelper serviceHelper, final int policy, final OnDataListener datacacheListener, final ParameterMap params, final int options, final String url, int webService, Class<?> serviceClass, final ComplexOptions complexOptionsCache, final ComplexOptions complexOptionsNetwork, String[] fingerPrintKeys) throws UnsupportedEncodingException { switch (policy) { case TYPE_CACHE: //we create and launch the database access if (datacacheListener != null){ //TODO maybe improve the fingerprint process DataLibRequest request = new DataLibRequest(url, params); return sendDataCache(datacacheListener, request.getFingerprint(fingerPrintKeys), webService, options, complexOptionsCache); } break; case TYPE_CACHE_THEN_NETWORK: //we create and launch the database access if (datacacheListener != null){ //TODO maybe improve the fingerprint process DataLibRequest request = new DataLibRequest(url, params); sendDataCache(datacacheListener, request.getFingerprint(fingerPrintKeys), webService, options, complexOptionsCache); } //we launch the network request return serviceHelper.launchRequest(options, webService, params, serviceClass, url, complexOptionsNetwork); case TYPE_NETWORK: //we launch the network request return serviceHelper.launchRequest(options, webService, params, serviceClass, url, complexOptionsNetwork); case TYPE_NETWORK_OTHERWISE_CACHE: //if the device is connected final NetworkInfo infos = mConnectivityManager.getActiveNetworkInfo(); if (infos != null && infos.isConnected()) { //we launch the network request return serviceHelper.launchRequest(options, webService, params, serviceClass, url, complexOptionsNetwork); } else if (datacacheListener != null) { //TODO maybe improve the fingerprint process DataLibRequest request = new DataLibRequest(url, params); return sendDataCache(datacacheListener, request.getFingerprint(fingerPrintKeys), webService, options, complexOptionsCache); } break; default: break; } return BAD_REQUEST; } /** * This class implement the fetching of information from the Datacache. It allows to execute it in another thread than the one where we launched the helper * function. * * @author Eyal LEZMY */ private class DataCacheRunnable implements Runnable { protected int mRequestId; protected String mId; protected int mType; protected OnDataListener mListener; protected int mOptions; protected ComplexOptions mComplexOptions; protected ResponseBusinessObject mData; protected DataCacheRunnable(final int requestId, final String id, final int type, final OnDataListener listener, final int options, ComplexOptions complexOptions) { super(); mRequestId = requestId; mId = id; mType = type; mListener = listener; mOptions = options; mComplexOptions = complexOptions; } @Override public void run() { //we set the thread as background Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); //we reach the data from cache mData = (ResponseBusinessObject) getBusinessObjectFromCacheByUrl(mType, mId, mComplexOptions); //we create a DatalibRequest corresponding to the request's options DataLibRequest request = new DataLibRequest(); request.option = mOptions; boolean runOnUIThread = request.isResponseRunningOnUIThread(); //we return the result to the listener //on the main thread if it is asked if(runOnUIThread && Thread.currentThread() != Looper.getMainLooper().getThread()){ Handler mainThread = new Handler(Looper.getMainLooper()); mainThread.post(new Runnable() { @Override public void run() { mListener.onCacheRequestFinished(mRequestId, mData); } }); //if the main thread is not asked } else { mListener.onCacheRequestFinished(mRequestId, mData); } } } /** * This function implements the reaching of data from the cache depending on the webservice's type given in the type parameter. * * @param type describes the type of webservice is supposed to be reach inside the database. The value can be found in a {@link DataLibService}'s * daughter class. Ex : WEBSERVICE_PREVISION_METEO * * @return returns the {@code BusinessObjectDAO} if exists or null if the asked webservice don't implements the cache process */ public abstract ResponseBusinessObject getBusinessObjectFromCacheByUrl(int type, String url, ComplexOptions complexOptions); /** * FUNCTIONS TO GET INFORMATION DIRECTLY FROM THE DATABASE */ /** * Private function that starts an asynchronous access to the database. * * @param databaseCode The code corresponding to the business object type to fetch in the database. The possible codes are contained inside the class * {@link BusinessObjectProvider} as constants. * @param where A filter declaring which rows to return, formatted as an SQL WHERE clause (excluding the WHERE itself). Passing null will return all rows * for the given URI. * @param whereArgs You may include ?s in selection, which will be replaced by the values from selectionArgs, in the order that they appear in the * selection. The values will be bound as Strings. * @param order How to order the rows, formatted as an SQL ORDER BY clause (excluding the ORDER BY itself). Passing null will use the default sort order, * which may be unordered. * @param join Tells if the {@link MeteoWeather} objects returned have to get his children's arrays filled thanks to the database. * @param listener The {@link OnDataListener} that will receive the {@link ArrayList} of fetched objects. * @return The request id generated by the {@link DataManager} or BAD_REQUEST constant if the request contains error (ex: listener == null) */ protected int startDatabaseAsyncAccess(final int databaseCode, final String where, final String[] whereArgs, final String order, final boolean join, final OnDataListener listener) { //if there is not listener we send a BAD_REQUEST result if (listener == null) return BAD_REQUEST; //we manage the request's id final int requestId = ServiceHelper.generateRequestId(); mDataCacheRequestIds.append(requestId, null); //we build and start the process to launch final DatabaseRunnable runnable = new DatabaseRunnable(requestId, where, whereArgs, order, join, listener, databaseCode); final Thread thread = new Thread(runnable); thread.start(); return requestId; } /** * This class implement the fetching of information from the Database. It allows to execute it in another thread than the one where we launched the helper * function. * * @author Eyal LEZMY */ private class DatabaseRunnable implements Runnable { protected int mId; protected String mWhere; protected String[] mWhereArgs; protected String mOrder; protected boolean mJoin; protected OnDataListener mListener; protected int mCode; public DatabaseRunnable(final int id, final String mWhere, final String[] mWhereArgs, final String mOrder, final boolean mJoin, final OnDataListener mListener, final int mCode) { super(); mId = id; this.mWhere = mWhere; this.mWhereArgs = mWhereArgs; this.mOrder = mOrder; this.mJoin = mJoin; this.mListener = mListener; this.mCode = mCode; } @SuppressWarnings("unchecked") @Override public void run() { ArrayList<?> result; result = getBusinessObjectsFromDatabase(mCode, mWhere, mWhereArgs, mOrder, mJoin); //we delete the pending request if it exists mDataCacheRequestIds.delete(mId); //we send the result to the listener mListener.onDataFromDatabase(mCode, (ArrayList<BusinessObject>) result); } } /** * Function that implements the database access request depending on the code value in parameter. * * @param code BusinessObjectProvider's code to describe the data you need access * @param where A filter declaring which rows to return, formatted as an SQL WHERE clause (excluding the WHERE itself). Passing null will return all rows * for the given URI. * @param whereArgs You may include ?s in selection, which will be replaced by the values from selectionArgs, in the order that they appear in the * selection. The values will be bound as Strings. * @param order How to order the rows, formatted as an SQL ORDER BY clause (excluding the ORDER BY itself). Passing null will use the default sort order, * which may be unordered. * @param join Tells if the {@link PrevisionMeteo} objects returned have to get his children's arrays filled thanks to the database. * @return */ protected abstract ArrayList<?> getBusinessObjectsFromDatabase(int code, String where, String[] whereArgs, String order, boolean join); }