All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.gawst.asyncdb.AsynchronousDbHelper Maven / Gradle / Ivy

package org.gawst.asyncdb;

import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.database.sqlite.SQLiteDatabaseCorruptException;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Pair;

import org.gawst.asyncdb.adapter.UIHandler;
import org.gawst.asyncdb.purge.PurgeHandler;

import java.lang.ref.WeakReference;
import java.util.Collection;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * the main helper class that saves/restore item in memory using a DB storage
 * 

* the storage handling is done in a separate thread * * @param the type of items stored in memory * @author Steve Lhomme * @see AsyncDbHelperHandler */ public abstract class AsynchronousDbHelper implements DataSource.BatchReadingCallback { public final static String TAG = "MemoryDb"; protected final static String STARTUP_TAG = "Startup"; protected final static boolean DEBUG_DB = false; private static final int MSG_LOAD_IN_MEMORY = 100; private static final int MSG_STORE_ITEM = 101; private static final int MSG_STORE_ITEMS = 102; private static final int MSG_REMOVE_ITEM = 103; private static final int MSG_UPDATE_ITEM = 104; private static final int MSG_CLEAR_DATABASE = 105; private static final int MSG_SWAP_ITEMS = 106; private static final int MSG_REPLACE_ITEM = 107; private static final int MSG_CUSTOM_OPERATION = 108; private static final UIHandler UI_HANDLER = new UIHandler(); private WeakReference> mErrorHandler; // not protected for now private final CopyOnWriteArrayList>> mDbListeners = new CopyOnWriteArrayList>>(); private final AtomicBoolean mDataLoaded = new AtomicBoolean(); private final AtomicInteger modifyingTransactionLevel = new AtomicInteger(0); final DataSource dataSource; private final String name; private PurgeHandler purgeHandler; private final static HandlerThread handlerThread = new HandlerThread("AsynchronousDbHelper", android.os.Process.THREAD_PRIORITY_BACKGROUND); static { handlerThread.start(); } /** * A class similar to {@link android.content.AsyncQueryHandler AsyncQueryHandler} to do simple calls asynchronously with a callback when it's done. * Only works with {@link AsynchronousDbHelper} classes that use a {@link org.gawst.asyncdb.source.typed.TypedDatabaseSource DatabaseSource} source. *

You can create your own with custom callback handling with {@link org.gawst.asyncdb.AsyncDbHelperHandler}. */ public class AsyncHandler extends AsyncDbHelperHandler { public AsyncHandler() { super(AsynchronousDbHelper.this); } } /** * @param db The already created {@link android.database.sqlite.SQLiteOpenHelper} to use as storage * @param name Database name for logs * @param logger The {@link org.gawst.asyncdb.Logger} to use for all logs (can be null for the default Android logs) * @param initCookie Cookie to pass to {@link #preloadInit(Object)} */ @SuppressLint("HandlerLeak") protected AsynchronousDbHelper(DataSource db, final String name, Logger logger, Object initCookie) { this.dataSource = db; this.name = name; if (logger!=null) LogManager.setLogger(logger); preloadInit(initCookie); saveStoreHandler.sendEmptyMessage(MSG_LOAD_IN_MEMORY); } private final Handler saveStoreHandler = new Handler(handlerThread.getLooper()) { public void handleMessage(Message msg) { switch (msg.what) { case MSG_LOAD_IN_MEMORY: loadInMemory(); break; case MSG_CLEAR_DATABASE: clearAllData(); break; case MSG_STORE_ITEM: @SuppressWarnings("unchecked") Pair itemToAdd = (Pair) msg.obj; storeItem(itemToAdd.first, itemToAdd.second); break; case MSG_STORE_ITEMS: @SuppressWarnings("unchecked") Pair, PurgeHandler> itemsToAdd = (Pair, PurgeHandler>) msg.obj; storeItems(itemsToAdd.first, itemsToAdd.second); break; case MSG_REMOVE_ITEM: @SuppressWarnings("unchecked") E itemToDelete = (E) msg.obj; removeItem(itemToDelete); break; case MSG_UPDATE_ITEM: @SuppressWarnings("unchecked") E itemToUpdate = (E) msg.obj; updateItem(itemToUpdate); break; case MSG_REPLACE_ITEM: @SuppressWarnings("unchecked") Pair itemsToReplace = (Pair) msg.obj; replaceItem(itemsToReplace.first, itemsToReplace.second); break; case MSG_SWAP_ITEMS: @SuppressWarnings("unchecked") Pair itemsToSwap = (Pair) msg.obj; swapItems(itemsToSwap.first, itemsToSwap.second); break; case MSG_CUSTOM_OPERATION: try { @SuppressWarnings("unchecked") AsynchronousDbOperation operation = (AsynchronousDbOperation) msg.obj; operation.runInMemoryDbOperation(AsynchronousDbHelper.this); } catch (Exception e) { LogManager.logger.w(TAG, name + " failed to run operation " + msg.obj, e); } break; } super.handleMessage(msg); } }; private void loadInMemory() { startLoadingInMemory(); try { if (shouldReloadAllData()) { try { dataSource.queryAll(AsynchronousDbHelper.this); } catch (Exception e) { LogManager.logger.w(STARTUP_TAG, "Can't query source " + dataSource + " in " + name, e); } } } catch (Exception e) { if (e instanceof SQLiteDatabaseCorruptException || e.getCause() instanceof SQLiteDatabaseCorruptException) notifyDatabaseCorrupted(dataSource, name, e); else LogManager.logger.w(STARTUP_TAG, "Can't open database " + name, e); } finally { finishLoadingInMemory(); } } private void clearAllData() { try { dataSource.clearAllData(); } catch (Throwable e) { LogManager.logger.w(TAG, "Failed to empty table " + dataSource + " in " + name, e); } finally { saveStoreHandler.sendEmptyMessage(MSG_LOAD_IN_MEMORY); // reload the DB into memory } } private void storeItem(@NonNull E item, @Nullable PurgeHandler purgeHandler) { ContentValues addValues = null; boolean itemAdded = false; try { addValues = getValuesFromData(item); if (addValues != null) { directStoreItem(addValues); itemAdded = true; } } catch (Exception e) { notifyAddItemFailed(item, addValues, e); } finally { if (itemAdded) { if (purgeHandler != null) { purgeHandler.onElementsAdded(AsynchronousDbHelper.this); } if (!notifyOnSchedule()) { pushModifyingTransaction(); popModifyingTransaction(); } } } } private void storeItems(@NonNull Collection items, @Nullable PurgeHandler purgeHandler) { ContentValues addValues; boolean itemsAdded = false; for (E item : items) { addValues = null; try { addValues = getValuesFromData(item); if (addValues != null) { directStoreItem(addValues); itemsAdded = true; } } catch (Exception e) { notifyAddItemFailed(item, addValues, e); } } if (itemsAdded) { if (purgeHandler != null) { purgeHandler.onElementsAdded(AsynchronousDbHelper.this); } if (!notifyOnSchedule()) { pushModifyingTransaction(); popModifyingTransaction(); } } } private void removeItem(@NonNull E itemToDelete) { try { if (DEBUG_DB) LogManager.logger.d(TAG, name + " remove " + itemToDelete); if (!dataSource.delete(itemToDelete)) { notifyRemoveItemFailed(itemToDelete, new RuntimeException("No item " + itemToDelete + " in " + name)); } else if (!notifyOnSchedule()) { pushModifyingTransaction(); popModifyingTransaction(); } } catch (Throwable e) { notifyRemoveItemFailed(itemToDelete, e); } } private void updateItem(@NonNull E itemToUpdate) { ContentValues updateValues = null; try { updateValues = getValuesFromData(itemToUpdate); if (!directUpdate(itemToUpdate, updateValues)) { notifyUpdateItemFailed(itemToUpdate, updateValues, new RuntimeException("Can't update " + updateValues + " in " + name)); } else if (!notifyOnSchedule()) { pushModifyingTransaction(); popModifyingTransaction(); } } catch (Throwable e) { notifyUpdateItemFailed(itemToUpdate, updateValues, e); } } private void replaceItem(@NonNull E src, @NonNull E replacement) { try { ContentValues replacementValues = getValuesFromData(src); if (directUpdate(replacement, replacementValues)) { if (!notifyOnSchedule()) { pushModifyingTransaction(); popModifyingTransaction(); } } } catch (Throwable e) { notifyReplaceItemFailed(src, replacement, e); } } private void swapItems(@NonNull E first, @NonNull E second) { ContentValues newValuesA = null; try { newValuesA = getValuesFromData(second); if (newValuesA != null) { if (DEBUG_DB) LogManager.logger.d(TAG, name + " update " + second + " with " + newValuesA); directUpdate(first, newValuesA); } } catch (Throwable e) { notifyUpdateItemFailed(first, newValuesA, e); } ContentValues newValuesB = null; try { newValuesB = getValuesFromData(first); if (newValuesB != null) { if (DEBUG_DB) LogManager.logger.d(TAG, name + " update " + first + " with " + newValuesB); directUpdate(second, newValuesB); } } catch (Throwable e) { notifyUpdateItemFailed(second, newValuesB, e); } if (!notifyOnSchedule()) { pushModifyingTransaction(); popModifyingTransaction(); } } /** * Method to call to insert data directly in the database * @param addValues Values that will be written in the database * @throws RuntimeException if the insertion failed */ private void directStoreItem(ContentValues addValues) throws RuntimeException { INSERT_ID inserted = dataSource.insert(addValues); if (DEBUG_DB) LogManager.logger.d(TAG, AsynchronousDbHelper.this+" insert "+addValues+" = "+inserted); if (inserted==null) throw new RuntimeException("failed to add values "+addValues+" in "+ dataSource); } /** * Method to call to update the data directly in the database * @param itemToUpdate Item in the database that needs to be updated * @param updateValues Values that will be updated in the database * @return {@code true} if the data were updated successfully */ protected final boolean directUpdate(E itemToUpdate, ContentValues updateValues) { if (updateValues!=null) { if (DEBUG_DB) LogManager.logger.d(TAG, AsynchronousDbHelper.this+" update "+updateValues+" for "+itemToUpdate); return dataSource.update(itemToUpdate, updateValues/*, SQLiteDatabase.CONFLICT_NONE*/); } return false; } /** * Method called at the end of constructor, just before the data start loading * @param cookie Data that may be needed to initialize all internal storage * */ protected void preloadInit(Object cookie) { } /** * tell the InMemory database that we are about to modify its data *

see also {@link #popModifyingTransaction()} */ protected void pushModifyingTransaction() { modifyingTransactionLevel.incrementAndGet(); } private final Runnable dataChanged = new Runnable() { @Override public void run() { for (WeakReference> l : mDbListeners) { final InMemoryDbListener listener = l.get(); if (listener==null) mDbListeners.remove(l); else listener.onMemoryDbChanged(AsynchronousDbHelper.this); } } }; /** * tell the InMemory database we have finish modifying the data at this level. * Once the pop matches all the pushes {@link org.gawst.asyncdb.InMemoryDbListener#onMemoryDbChanged(AsynchronousDbHelper)} is called *

this is useful to avoid multiple calls to {@link org.gawst.asyncdb.InMemoryDbListener#onMemoryDbChanged(AsynchronousDbHelper)} during a batch of changes *

see also {@link #pushModifyingTransaction()} */ protected void popModifyingTransaction() { if (modifyingTransactionLevel.decrementAndGet()==0) { UI_HANDLER.removeCallbacks(dataChanged); UI_HANDLER.runOnUiThread(dataChanged); } } /** * @return {@code true} if all {@link org.gawst.asyncdb.InMemoryDbListener#onMemoryDbChanged(AsynchronousDbHelper)} should be called right after a schedule call or after the call is processed. * @see #scheduleAddOperation(Object) * @see #scheduleAddOperation(Object, org.gawst.asyncdb.purge.PurgeHandler) * @see #scheduleAddOperation(java.util.Collection) * @see #scheduleAddOperation(java.util.Collection, org.gawst.asyncdb.purge.PurgeHandler) * @see #scheduleRemoveOperation(Object) * @see #scheduleUpdateOperation(Object) * @see #scheduleReplaceOperation(Object, Object) * @see #scheduleSwapOperation(Object, Object) */ protected boolean notifyOnSchedule() { return true; } protected void clearDataInMemory() {} /** * set the listener that will receive error events * @param listener null to remove the listener */ protected void setDbErrorHandler(AsynchronousDbErrorHandler listener) { if (listener==null) mErrorHandler = null; else mErrorHandler = new WeakReference>(listener); } protected PurgeHandler getPurgeHandler() { return purgeHandler; } public void setPurgeHandler(PurgeHandler purgeHandler) { this.purgeHandler = purgeHandler; } /** * Call this after an element is inserted for the purge to do its job. */ public void triggerPurgeHandler() { if (null != purgeHandler) { purgeHandler.onElementsAdded(this); } } public void addListener(final InMemoryDbListener listener) { for (WeakReference> l : mDbListeners) { if (l.get()==null) mDbListeners.remove(l); else if (l.get()==listener) return; } mDbListeners.add(new WeakReference>(listener)); if (mDataLoaded.get()) UI_HANDLER.runOnUiThread(new Runnable() { @Override public void run() { listener.onMemoryDbChanged(AsynchronousDbHelper.this); } }); } public void removeListener(InMemoryDbListener listener) { for (WeakReference> l : mDbListeners) { if (l.get()==null) mDbListeners.remove(l); else if (l.get()==listener) mDbListeners.remove(l); } } /** * delete all the data in memory and in the database */ public final void clear() { pushModifyingTransaction(); clearDataInMemory(); popModifyingTransaction(); saveStoreHandler.sendEmptyMessage(MSG_CLEAR_DATABASE); } private void notifyAddItemFailed(E item, ContentValues values, Throwable cause) { LogManager.logger.d(TAG, this+" failed to add item "+item+(DEBUG_DB ? (" values"+values) : ""), cause); if (mErrorHandler!=null) { final AsynchronousDbErrorHandler listener = mErrorHandler.get(); if (listener==null) mErrorHandler = null; else listener.onAddItemFailed(this, item, values, cause); } pushModifyingTransaction(); popModifyingTransaction(); } private void notifyReplaceItemFailed(E srcItem, E replacement, Throwable cause) { LogManager.logger.i(TAG, this+" failed to replace item "+srcItem+" with "+replacement, cause); if (mErrorHandler!=null) { final AsynchronousDbErrorHandler listener = mErrorHandler.get(); if (listener==null) mErrorHandler = null; else listener.onReplaceItemFailed(this, srcItem, replacement, cause); } pushModifyingTransaction(); popModifyingTransaction(); } private void notifyUpdateItemFailed(E item, ContentValues values, Throwable cause) { LogManager.logger.i(TAG, this+" failed to update item "+item+(DEBUG_DB ? (" values"+values) : ""), cause); if (mErrorHandler!=null) { final AsynchronousDbErrorHandler listener = mErrorHandler.get(); if (listener==null) mErrorHandler = null; else listener.onAddItemFailed(this, item, values, cause); } pushModifyingTransaction(); popModifyingTransaction(); } private void notifyRemoveItemFailed(E item, Throwable cause) { LogManager.logger.i(TAG, this+" failed to remove item "+item, cause); if (mErrorHandler!=null) { final AsynchronousDbErrorHandler listener = mErrorHandler.get(); if (listener==null) mErrorHandler = null; else listener.onRemoveItemFailed(this, item, cause); } pushModifyingTransaction(); popModifyingTransaction(); } private void notifyDatabaseCorrupted(DataSource dataSource, String name, Throwable cause) { LogManager.logger.e(STARTUP_TAG, "table "+ this.dataSource +" is corrupted in "+name); if (mErrorHandler!=null) { final AsynchronousDbErrorHandler listener = mErrorHandler.get(); if (listener==null) mErrorHandler = null; else listener.onCorruption(this); } pushModifyingTransaction(); dataSource.eraseSource(); popModifyingTransaction(); } /** * Transform the element in memory into {@link android.content.ContentValues} that can be saved in the database. *

you can return null and fill the database yourself if you need to * @param data the data to transform * @return a ContentValues element with all data that can be used to restore the data later from the database * @see #addItemInMemory(Object) */ protected abstract ContentValues getValuesFromData(E data) throws RuntimeException; /** * Request to store the item in the database asynchronously *

Will call the {@link org.gawst.asyncdb.AsynchronousDbErrorHandler#onAddItemFailed(AsynchronousDbHelper, Object, android.content.ContentValues, Throwable)} on failure * @param item to add */ protected final void scheduleAddOperation(E item) { scheduleAddOperation(item, purgeHandler); } /** * Request to store the item in the database asynchronously *

Will call the {@link org.gawst.asyncdb.AsynchronousDbErrorHandler#onAddItemFailed(AsynchronousDbHelper, Object, android.content.ContentValues, Throwable) AsynchronousDbErrorHandler.onAddItemFailed()} on failure * @param item to add * @param purgeHandler */ protected final void scheduleAddOperation(E item, PurgeHandler purgeHandler) { if (null != item) { saveStoreHandler.sendMessage(Message.obtain(saveStoreHandler, MSG_STORE_ITEM, new Pair(item, purgeHandler))); if (notifyOnSchedule()) { pushModifyingTransaction(); popModifyingTransaction(); } } } /** * Request to store the items in the database asynchronously *

Will call {@link org.gawst.asyncdb.AsynchronousDbErrorHandler#onAddItemFailed(AsynchronousDbHelper, Object, android.content.ContentValues, Throwable) AsynchronousDbErrorHandler.onAddItemFailed()} on each item failing * @param items to add */ protected final void scheduleAddOperation(Collection items) { scheduleAddOperation(items, purgeHandler); } /** * Request to store the items in the database asynchronously *

Will call {@link org.gawst.asyncdb.AsynchronousDbErrorHandler#onAddItemFailed(AsynchronousDbHelper, Object, android.content.ContentValues, Throwable) AsynchronousDbErrorHandler.onAddItemFailed()} on each item failing * @param items to add * @param purgeHandler */ protected final void scheduleAddOperation(Collection items, PurgeHandler purgeHandler) { if (null != items) { saveStoreHandler.sendMessage(Message.obtain(saveStoreHandler, MSG_STORE_ITEMS, new Pair, PurgeHandler>(items, purgeHandler))); if (notifyOnSchedule()) { pushModifyingTransaction(); popModifyingTransaction(); } } } /** * Request to update the item in the database asynchronously *

{@link org.gawst.asyncdb.source.DatabaseElementHandler#getItemSelectClause(Object)} is used to find the matching item in the database *

Will call {@link org.gawst.asyncdb.AsynchronousDbErrorHandler#onUpdateItemFailed(AsynchronousDbHelper, Object, Throwable) AsynchronousDbErrorHandler.onUpdateItemFailed()} on failure * @see #getValuesFromData(Object) * @param item to update */ protected final void scheduleUpdateOperation(@NonNull E item) { if (null != item) { saveStoreHandler.sendMessage(Message.obtain(saveStoreHandler, MSG_UPDATE_ITEM, item)); if (notifyOnSchedule()) { pushModifyingTransaction(); popModifyingTransaction(); } } } /** * Request to replace an item in the databse with another asynchronously *

{@link org.gawst.asyncdb.source.DatabaseElementHandler#getItemSelectClause(Object)} is used to find the matching item in the database *

Will call {@link org.gawst.asyncdb.AsynchronousDbErrorHandler#onReplaceItemFailed(AsynchronousDbHelper, Object, Object, Throwable) AsynchronousDbErrorHandler.onReplaceItemFailed()} on failure * @param original Item to replace * @param replacement Item to replace with */ protected final void scheduleReplaceOperation(@NonNull E original, @NonNull E replacement) { saveStoreHandler.sendMessage(Message.obtain(saveStoreHandler, MSG_REPLACE_ITEM, new Pair(original, replacement))); if (notifyOnSchedule()) { pushModifyingTransaction(); popModifyingTransaction(); } } protected final void scheduleSwapOperation(@NonNull E itemA, @NonNull E itemB) { saveStoreHandler.sendMessage(Message.obtain(saveStoreHandler, MSG_SWAP_ITEMS, new Pair(itemA, itemB))); if (notifyOnSchedule()) { pushModifyingTransaction(); popModifyingTransaction(); } } /** * Request to delete the item from the database *

Will call the {@link org.gawst.asyncdb.AsynchronousDbErrorHandler#onRemoveItemFailed(AsynchronousDbHelper, Object, Throwable) AsynchronousDbErrorHandler.onRemoveItemFailed()} on failure * @param item to remove */ protected final void scheduleRemoveOperation(E item) { if (null != item) { saveStoreHandler.sendMessage(Message.obtain(saveStoreHandler, MSG_REMOVE_ITEM, item)); if (notifyOnSchedule()) { pushModifyingTransaction(); popModifyingTransaction(); } } } /** * run the operation in the internal thread * @param operation */ public final void scheduleCustomOperation(@NonNull AsynchronousDbOperation operation) { saveStoreHandler.sendMessage(Message.obtain(saveStoreHandler, MSG_CUSTOM_OPERATION, operation)); } /** * called when we are about to read all items from the disk */ protected void startLoadingInMemory() { pushModifyingTransaction(); mDataLoaded.set(false); } public void startLoadingAllItems(int elementCount) {} /** * called after all items have been read from the disk */ protected void finishLoadingInMemory() { mDataLoaded.set(true); popModifyingTransaction(); } @Override public void removeInvalidEntry(final InvalidEntry invalidEntry) { scheduleCustomOperation(new AsynchronousDbOperation() { @Override public void runInMemoryDbOperation(AsynchronousDbHelper db) { // remove the element from the DB forever dataSource.deleteInvalidEntry(invalidEntry); } }); } /** * Tell whether the database loading should be done or not *

If you don't store the elements in memory, you don't need to load the whole data */ protected boolean shouldReloadAllData() { return true; } public boolean isDataLoaded() { return mDataLoaded.get(); } /** Wait until the data are loaded */ public void waitForDataLoaded() { } @Override public String toString() { StringBuilder sb = new StringBuilder(48); sb.append("{AsyncDB:"); sb.append(name); sb.append(' '); sb.append(Integer.toHexString(System.identityHashCode(this))); sb.append('}'); return sb.toString(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy