/** * Copyright Red Hat, Inc, and individual contributors. * * 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 com.feedhenry.sdk.sync; import android.app.Activity; import android.content.Context; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.util.Log; import com.feedhenry.sdk.FH; import com.feedhenry.sdk.FHActCallback; import com.feedhenry.sdk.api.FHActRequest; import com.feedhenry.sdk.exceptions.DataSetNotFound; import com.feedhenry.sdk.exceptions.FHNotReadyException; import com.feedhenry.sdk.utils.FHLog; import java.util.Date; import java.util.HashMap; import java.util.Map; import org.json.fh.JSONObject; /** * The sync client is part of the FeedHenry data sync framework. It provides a * mechanism to manage bi-direction data synchronization. For more details, * please check * <a href="http://docs.feedhenry.com/v3/guides/sync_service.html">data sync * framework docs</a>. */ public class FHSyncClient { private static FHSyncClient mInstance; protected static final String LOG_TAG = "com.feedhenry.sdk.sync.FHSyncClient"; private final Handler mHandler; private Context mContext; private Map<String, FHSyncDataset> mDataSets = new HashMap<String, FHSyncDataset>(); private FHSyncConfig mConfig = new FHSyncConfig(); private FHSyncListener mSyncListener = null; private FHSyncNotificationHandler mNotificationHandler; private boolean mInitialised = false; private MonitorTask mMonitorTask = null; /** * Gets the singleton instance of the sync client. * * @return the sync client instance */ public static FHSyncClient getInstance() { if (null == mInstance) { mInstance = new FHSyncClient(); } return mInstance; } public FHSyncClient() { HandlerThread thread = new HandlerThread("FHSyncClient"); thread.start(); mHandler = new Handler(thread.getLooper()); } /** * Initializes the sync client. Should be called every time an app/activity * starts. * * @param pContext The app context * @param pConfig The sync configuration * @param pListener The sync listener */ public void init(Context pContext, FHSyncConfig pConfig, FHSyncListener pListener) { mContext = pContext.getApplicationContext(); mConfig = pConfig; mSyncListener = pListener; initHandlers(); mInitialised = true; if (null == mMonitorTask) { HandlerThread thread = new HandlerThread("monitor task"); thread.start(); Handler handler = new Handler(thread.getLooper()); mMonitorTask = new MonitorTask(); handler.post(mMonitorTask); } } /** * Initializes the notification handlers. */ private void initHandlers() { if (null != Looper.myLooper()) { mNotificationHandler = new FHSyncNotificationHandler(this.mSyncListener); } else { HandlerThread ht = new HandlerThread("FHSyncClientNotificationHanlder"); ht.start(); mNotificationHandler = new FHSyncNotificationHandler(ht.getLooper(), this.mSyncListener); } } /** * Re-sets the sync listener. * * @param pListener the new sync listener */ public void setListener(FHSyncListener pListener) { mSyncListener = pListener; if (null != mNotificationHandler) { mNotificationHandler.setSyncListener(mSyncListener); } } /** * Uses the sync client to manage a dataset. * * @param pDataId The id of the dataset. * @param pConfig The sync configuration for the dataset. If not specified, * the sync configuration passed in the initDev method will be used * @param pQueryParams Query parameters for the dataset * @throws IllegalStateException thrown if FHSyncClient isn't initialised. */ public void manage(String pDataId, FHSyncConfig pConfig, JSONObject pQueryParams) { manage(pDataId, pConfig, pQueryParams, new JSONObject()); } /** * Uses the sync client to manage a dataset. * * @param pDataId The id of the dataset. * @param pConfig The sync configuration for the dataset. If not specified, * the sync configuration passed in the initDev method will be used * @param pQueryParams Query parameters for the dataset * @param pMetaData Meta for the dataset * @throws IllegalStateException thrown if FHSyncClient isn't initialised. */ public void manage(String pDataId, FHSyncConfig pConfig, JSONObject pQueryParams, JSONObject pMetaData) { if (!mInitialised) { throw new IllegalStateException("FHSyncClient isn't initialised. Have you called the initDev function?"); } FHSyncDataset dataset = mDataSets.get(pDataId); FHSyncConfig syncConfig = mConfig; if (null != pConfig) { syncConfig = pConfig; } if (null != dataset) { dataset.setContext(mContext); dataset.setNotificationHandler(mNotificationHandler); } else { dataset = new FHSyncDataset(mContext, mNotificationHandler, pDataId, syncConfig, pQueryParams, pMetaData); mDataSets.put(pDataId, dataset); dataset.setSyncRunning(false); dataset.setInitialised(true); } dataset.setSyncConfig(syncConfig); dataset.setSyncPending(true); dataset.writeToFile(); } /** * Causes the sync framework to schedule for immediate execution a sync. * * @param pDataId The id of the dataset */ public void forceSync(String pDataId) { FHSyncDataset dataset = mDataSets.get(pDataId); if (null != dataset) { dataset.setSyncPending(true); } } /** * Lists all the data in the dataset with pDataId. * * @param pDataId The id of the dataset * @return all data records. Each record contains a key "uid" with the id * value and a key "data" with the JSON data. */ public JSONObject list(String pDataId) { FHSyncDataset dataset = mDataSets.get(pDataId); JSONObject data = null; if (null != dataset) { data = dataset.listData(); } return data; } /** * Reads a data record with pUID in dataset with pDataId. * * @param pDataId the id of the dataset * @param pUID the id of the data record * @return the data record. Each record contains a key "uid" with the id * value and a key "data" with the JSON data. */ public JSONObject read(String pDataId, String pUID) { FHSyncDataset dataset = mDataSets.get(pDataId); JSONObject data = null; if (null != dataset) { data = dataset.readData(pUID); } return data; } /** * Creates a new data record in dataset with pDataId. * * @param pDataId the id of the dataset * @param pData the actual data * @return the created data record. Each record contains a key "uid" with * the id value and a key "data" with the JSON data. * @throws DataSetNotFound if the dataId is not known */ public JSONObject create(String pDataId, JSONObject pData) throws DataSetNotFound { FHSyncDataset dataset = mDataSets.get(pDataId); if (null != dataset) { return dataset.createData(pData); } else { throw new DataSetNotFound("Unknown dataId : " + pDataId); } } /** * Updates an existing data record in dataset with pDataId. * * @param pDataId the id of the dataset * @param pUID the id of the data record * @param pData the new content of the data record * @return the updated data record. Each record contains a key "uid" with * the id value and a key "data" with the JSON data. * @throws DataSetNotFound if the dataId is not known */ public JSONObject update(String pDataId, String pUID, JSONObject pData) throws DataSetNotFound { FHSyncDataset dataset = mDataSets.get(pDataId); if (null != dataset) { return dataset.updateData(pUID, pData); } else { throw new DataSetNotFound("Unknown dataId : " + pDataId); } } /** * Deletes a data record in the dataset with pDataId. * * @param pDataId the id of the dataset * @param pUID the id of the data record * @return the deleted data record. Each record contains a key "uid" with * the id value and a key "data" with the JSON data. * @throws DataSetNotFound if the dataId is not known */ public JSONObject delete(String pDataId, String pUID) throws DataSetNotFound { FHSyncDataset dataset = mDataSets.get(pDataId); if (null != dataset) { return dataset.deleteData(pUID); } else { throw new DataSetNotFound("Unknown dataId : " + pDataId); } } /** * Lists sync collisions in dataset with id pDataId. * * @param pDataId the id of the dataset * @param pCallback the callback function * @throws FHNotReadyException if FH is not initialized. * */ public void listCollisions(String pDataId, FHActCallback pCallback) throws FHNotReadyException { JSONObject params = new JSONObject(); params.put("fn", "listCollisions"); FHActRequest request = FH.buildActRequest(pDataId, params); request.executeAsync(pCallback); } /** * Removes a sync collision record in the dataset with id pDataId. * * @param pDataId the id of the dataset * @param pCollisionHash the hash value of the collision record * @param pCallback the callback function * @throws FHNotReadyException thrown if FH is not initialized. */ public void removeCollision(String pDataId, String pCollisionHash, FHActCallback pCallback) throws FHNotReadyException { JSONObject params = new JSONObject(); params.put("fn", "removeCollision"); params.put("hash", pCollisionHash); FHActRequest request = FH.buildActRequest(pDataId, params); request.executeAsync(pCallback); } /** * This method will begin synchronization. It should be called in the * {@link Activity#onResume()} block. * * @param listener the listener to. If null the current listener will be * used. * */ public void resumeSync(FHSyncListener listener) { if (listener != null) { this.mSyncListener = listener; } for (FHSyncDataset dataSet : mDataSets.values()) { dataSet.stopSync(false); } } /** * This method will pause synchronization. It should be called in the * {@link Activity#onPause()} block. */ public void pauseSync() { for (FHSyncDataset dataSet : mDataSets.values()) { dataSet.stopSync(true); } this.mSyncListener = null; } /** * Stops the sync process for dataset with id pDataId. * * @param pDataId the id of the dataset */ public void stop(String pDataId) { FHSyncDataset dataset = mDataSets.get(pDataId); if (null != dataset) { dataset.stopSync(true); } } /** * Stops all sync processes for all the datasets managed by the sync client. */ public void destroy() { if (mInitialised) { if (null != mMonitorTask) { mMonitorTask.stopRunning(); } for (String key : mDataSets.keySet()) { stop(key); } mSyncListener = null; mNotificationHandler = null; mDataSets = new HashMap<String, FHSyncDataset>(); mInitialised = false; } } private class MonitorTask implements Runnable { private boolean mKeepRunning = true; public void stopRunning() { mKeepRunning = false; Thread.currentThread().interrupt(); } private void checkDatasets() { if (null != mDataSets) { for (Map.Entry<String, FHSyncDataset> entry : mDataSets.entrySet()) { final FHSyncDataset dataset = entry.getValue(); boolean syncRunning = dataset.isSyncRunning(); if (!syncRunning && !dataset.isStopSync()) { // sync isn't running for dataId at the moment, check if needs to start it Date lastSyncStart = dataset.getSyncStart(); Date lastSyncEnd = dataset.getSyncEnd(); if (null == lastSyncStart) { dataset.setSyncPending(true); } else if (null != lastSyncEnd) { long interval = new Date().getTime() - lastSyncEnd.getTime(); if (interval > dataset.getSyncConfig().getSyncFrequency() * 1000) { Log.d(LOG_TAG, "Should start sync!!"); dataset.setSyncPending(true); } } if (dataset.isSyncPending()) { mHandler.post( new Runnable() { @Override public void run() { dataset.startSyncLoop(); } }); } } } } } @Override public void run() { while (mKeepRunning && !Thread.currentThread().isInterrupted()) { checkDatasets(); try { Thread.sleep(1000); } catch (Exception e) { FHLog.e(LOG_TAG, "MonitorTask thread is interrupted", e); Thread.currentThread().interrupt(); } } } } }