package com.raizlabs.android.dbflow.list;
import android.annotation.TargetApi;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.raizlabs.android.dbflow.config.FlowManager;
import com.raizlabs.android.dbflow.runtime.FlowContentObserver;
import com.raizlabs.android.dbflow.sql.language.SQLite;
import com.raizlabs.android.dbflow.sql.queriable.ModelQueriable;
import com.raizlabs.android.dbflow.structure.InstanceAdapter;
import com.raizlabs.android.dbflow.structure.Model;
import com.raizlabs.android.dbflow.structure.ModelAdapter;
import com.raizlabs.android.dbflow.structure.cache.ModelCache;
import com.raizlabs.android.dbflow.structure.database.DatabaseWrapper;
import com.raizlabs.android.dbflow.structure.database.transaction.DefaultTransactionQueue;
import com.raizlabs.android.dbflow.structure.database.transaction.ProcessModelTransaction;
import com.raizlabs.android.dbflow.structure.database.transaction.QueryTransaction;
import com.raizlabs.android.dbflow.structure.database.transaction.Transaction;
import java.util.Collection;
import java.util.List;
import java.util.ListIterator;
/**
* Description: Operates very similiar to a {@link java.util.List} except its backed by a table cursor. All of
* the {@link java.util.List} modifications default to the main thread, but it can be set to
* run on the {@link DefaultTransactionQueue}. Register a {@link Transaction.Success}
* on this list to know when the results complete. NOTE: any modifications to this list will be reflected
* on the underlying table.
*/
public class FlowQueryList<TModel> extends FlowContentObserver
implements List<TModel>, IFlowCursorIterator<TModel> {
private static final Handler REFRESH_HANDLER = new Handler(Looper.myLooper());
/**
* Holds the table cursor
*/
private final FlowCursorList<TModel> internalCursorList;
private final Transaction.Success successCallback;
private final Transaction.Error errorCallback;
/**
* If true, we will make all modifications on the {@link DefaultTransactionQueue}, else
* we will run it on the main thread.
*/
private boolean transact = false;
private boolean changeInTransaction = false;
private boolean pendingRefresh = false;
private FlowQueryList(Builder<TModel> builder) {
transact = builder.transact;
changeInTransaction = builder.changeInTransaction;
successCallback = builder.success;
errorCallback = builder.error;
internalCursorList = new FlowCursorList.Builder<>(builder.table)
.cursor(builder.cursor)
.cacheModels(builder.cacheModels)
.modelQueriable(builder.modelQueriable)
.modelCache(builder.modelCache)
.build();
}
/**
* Registers the list for model change events. Internally this refreshes the underlying {@link FlowCursorList}. Call
* {@link #beginTransaction()} to bunch up calls to model changes and then {@link #endTransactionAndNotify()} to dispatch
* and refresh this list when completed.
*/
public void registerForContentChanges(Context context) {
super.registerForContentChanges(context, internalCursorList.table());
}
public void addOnCursorRefreshListener(FlowCursorList.OnCursorRefreshListener<TModel> onCursorRefreshListener) {
internalCursorList.addOnCursorRefreshListener(onCursorRefreshListener);
}
public void removeOnCursorRefreshListener(FlowCursorList.OnCursorRefreshListener<TModel> onCursorRefreshListener) {
internalCursorList.removeOnCursorRefreshListener(onCursorRefreshListener);
}
@Override
public void registerForContentChanges(Context context, Class<?> table) {
throw new RuntimeException(
"This method is not to be used in the FlowQueryList. We should only ever receive" +
" notifications for one class here. Call registerForContentChanges(Context) instead");
}
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
if (!isInTransaction) {
refreshAsync();
} else {
changeInTransaction = true;
}
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
@Override
public void onChange(boolean selfChange, Uri uri) {
super.onChange(selfChange, uri);
if (!isInTransaction) {
refreshAsync();
} else {
changeInTransaction = true;
}
}
/**
* @return a mutable list that does not reflect changes on the underlying DB.
*/
public List<TModel> getCopy() {
return internalCursorList.getAll();
}
public FlowCursorList<TModel> cursorList() {
return internalCursorList;
}
public Transaction.Error error() {
return errorCallback;
}
public Transaction.Success success() {
return successCallback;
}
public boolean changeInTransaction() {
return changeInTransaction;
}
public boolean transact() {
return transact;
}
ModelAdapter<TModel> getModelAdapter() {
return internalCursorList.getModelAdapter();
}
InstanceAdapter<TModel> getInstanceAdapter() {
return internalCursorList.getInstanceAdapter();
}
/**
* @return Constructs a new {@link Builder} that reuses the underlying {@link Cursor}, cache,
* callbacks, and other properties.
*/
public Builder<TModel> newBuilder() {
return new Builder<>(internalCursorList)
.success(successCallback)
.error(errorCallback)
.changeInTransaction(changeInTransaction)
.transact(transact);
}
/**
* Refreshes the content backing this list.
*/
public void refresh() {
internalCursorList.refresh();
}
/**
* Will refresh content at a slightly later time, and multiple subsequent calls to this method within
* a short period of time will be combined into one call.
*/
public void refreshAsync() {
synchronized (this) {
if (pendingRefresh) {
return;
}
pendingRefresh = true;
}
REFRESH_HANDLER.post(refreshRunnable);
}
@Override
public void endTransactionAndNotify() {
if (changeInTransaction) {
changeInTransaction = false;
refresh();
}
super.endTransactionAndNotify();
}
/**
* Adds an item to this table, but does not allow positonal insertion. Same as calling
* {@link #add(TModel)}
*
* @param location Not used.
* @param model The model to save
*/
@Override
public void add(int location, TModel model) {
add(model);
}
/**
* Adds an item to this table
*
* @param model The model to save
* @return always true
*/
@Override
public boolean add(TModel model) {
Transaction transaction = FlowManager.getDatabaseForTable(internalCursorList.table())
.beginTransactionAsync(new ProcessModelTransaction.Builder<>(saveModel)
.add(model).build())
.error(internalErrorCallback)
.success(internalSuccessCallback).build();
if (transact) {
transaction.execute();
} else {
transaction.executeSync();
}
return true;
}
/**
* Adds all items to this table, but
* does not allow positional insertion. Same as calling {@link #addAll(java.util.Collection)}
*
* @param location Not used.
* @param collection The list of items to add to the table
* @return always true
*/
@Override
public boolean addAll(int location, Collection<? extends TModel> collection) {
return addAll(collection);
}
/**
* Adds all items to this table.
*
* @param collection The list of items to add to the table
* @return always true
*/
@SuppressWarnings("unchecked")
@Override
public boolean addAll(Collection<? extends TModel> collection) {
// cast to normal collection, we do not want subclasses of this table saved
final Collection<TModel> tmpCollection = (Collection<TModel>) collection;
Transaction transaction = FlowManager.getDatabaseForTable(internalCursorList.table())
.beginTransactionAsync(new ProcessModelTransaction.Builder<>(saveModel)
.addAll(tmpCollection).build())
.error(internalErrorCallback)
.success(internalSuccessCallback).build();
if (transact) {
transaction.execute();
} else {
transaction.executeSync();
}
return true;
}
/**
* Deletes all items from the table. Be careful as this will clear data!
*/
@Override
public void clear() {
Transaction transaction = FlowManager.getDatabaseForTable(internalCursorList.table())
.beginTransactionAsync(new QueryTransaction.Builder<>(
SQLite.delete().from(internalCursorList.table())).build())
.error(internalErrorCallback)
.success(internalSuccessCallback)
.build();
if (transact) {
transaction.execute();
} else {
transaction.executeSync();
}
}
/**
* Checks to see if the table contains the object only if its a {@link TModel}
*
* @param object A model class. For interface purposes, this must be an Object.
* @return always false if its anything other than the current table. True if {@link com.raizlabs.android.dbflow.structure.Model#exists()} passes.
*/
@SuppressWarnings("unchecked")
@Override
public boolean contains(Object object) {
boolean contains = false;
if (internalCursorList.table().isAssignableFrom(object.getClass())) {
TModel model = ((TModel) object);
contains = internalCursorList.getInstanceAdapter().exists(model);
}
return contains;
}
/**
* If the collection is null or empty, we return false.
*
* @param collection The collection to check if all exist within the table.
* @return true if all items exist in table, false if at least one fails.
*/
@Override
public boolean containsAll(@NonNull Collection<?> collection) {
boolean contains = !(collection.isEmpty());
if (contains) {
for (Object o : collection) {
if (!contains(o)) {
contains = false;
break;
}
}
}
return contains;
}
@Override
public long getCount() {
return internalCursorList.getCount();
}
@Nullable
@Override
public TModel getItem(long position) {
return internalCursorList.getItem(position);
}
@Override
public Cursor cursor() {
return internalCursorList.cursor();
}
/**
* Returns the item from the backing {@link FlowCursorList}. First call
* will load the model from the cursor, while subsequent calls will use the cache.
*
* @param row the row from the internal {@link FlowCursorList} query that we use.
* @return A model converted from the internal {@link FlowCursorList}. For
* performance improvements, ensure caching is turned on.
*/
@Override
public TModel get(int row) {
return internalCursorList.getItem(row);
}
@Override
public int indexOf(Object object) {
throw new UnsupportedOperationException(
"We cannot determine which index in the table this item exists at efficiently");
}
@Override
public boolean isEmpty() {
return internalCursorList.isEmpty();
}
/**
* @return An iterator from {@link FlowCursorList#getAll()}.
* Be careful as this method will convert all data under this table into a list of {@link TModel} in the UI thread.
*/
@NonNull
@Override
public FlowCursorIterator<TModel> iterator() {
return new FlowCursorIterator<>(this);
}
@Override
public FlowCursorIterator<TModel> iterator(int startingLocation, long limit) {
return new FlowCursorIterator<>(this, startingLocation, limit);
}
@Override
public int lastIndexOf(Object object) {
throw new UnsupportedOperationException(
"We cannot determine which index in the table this item exists at efficiently");
}
/**
* @return A list iterator from the {@link FlowCursorList#getAll()}.
* Be careful as this method will convert all data under this table into a list of {@link TModel} in the UI thread.
*/
@NonNull
@Override
public ListIterator<TModel> listIterator() {
return new FlowCursorIterator<>(this);
}
/**
* @param location The index to start the iterator.
* @return A list iterator from the {@link FlowCursorList#getAll()}.
* Be careful as this method will convert all data under this table into a list of {@link TModel} in the UI thread.
*/
@NonNull
@Override
public ListIterator<TModel> listIterator(int location) {
return new FlowCursorIterator<>(this, location);
}
/**
* Deletes a {@link TModel} at a specific position within the stored {@link Cursor}.
* If {@link #transact} is true, the delete does not happen immediately. Avoid using this operation
* many times. If you need to remove multiple, use {@link #removeAll(Collection)}
*
* @param location The location within the table to remove the item from
* @return The removed item.
*/
@Override
public TModel remove(int location) {
TModel model = internalCursorList.getItem(location);
Transaction transaction = FlowManager.getDatabaseForTable(internalCursorList.table())
.beginTransactionAsync(new ProcessModelTransaction.Builder<>(deleteModel)
.add(model).build())
.error(internalErrorCallback)
.success(internalSuccessCallback).build();
if (transact) {
transaction.execute();
} else {
transaction.executeSync();
}
return model;
}
/**
* Removes an item from this table on the {@link DefaultTransactionQueue} if
* {@link #transact} is true.
*
* @param object A model class. For interface purposes, this must be an Object.
* @return true if the item was removed. Always false if the object is not from the same table as this list.
*/
@SuppressWarnings("unchecked")
@Override
public boolean remove(Object object) {
boolean removed = false;
// if its a ModelClass
if (internalCursorList.table().isAssignableFrom(object.getClass())) {
TModel model = ((TModel) object);
Transaction transaction = FlowManager.getDatabaseForTable(internalCursorList.table())
.beginTransactionAsync(new ProcessModelTransaction.Builder<>(deleteModel)
.add(model).build())
.error(internalErrorCallback)
.success(internalSuccessCallback).build();
if (transact) {
transaction.execute();
} else {
transaction.executeSync();
}
removed = true;
}
return removed;
}
/**
* Removes all items from this table in one transaction based on the list passed. This may happen in the background
* if {@link #transact} is true.
*
* @param collection The collection to remove.
* @return Always true. Will cause a {@link ClassCastException} if the collection is not of type {@link TModel}
*/
@SuppressWarnings("unchecked")
@Override
public boolean removeAll(@NonNull Collection<?> collection) {
// if its a ModelClass
Collection<TModel> modelCollection = (Collection<TModel>) collection;
Transaction transaction = FlowManager.getDatabaseForTable(internalCursorList.table())
.beginTransactionAsync(new ProcessModelTransaction.Builder<>(deleteModel)
.addAll(modelCollection).build())
.error(internalErrorCallback)
.success(internalSuccessCallback).build();
if (transact) {
transaction.execute();
} else {
transaction.executeSync();
}
return true;
}
/**
* Retrieves the full list of {@link TModel} items from the table, removes these from the list, and
* then deletes the remaining members. This is not that efficient.
*
* @param collection The collection if models to keep in the table.
* @return Always true.
*/
@Override
public boolean retainAll(@NonNull Collection<?> collection) {
List<TModel> tableList = internalCursorList.getAll();
tableList.removeAll(collection);
Transaction transaction = FlowManager.getDatabaseForTable(internalCursorList.table())
.beginTransactionAsync(new ProcessModelTransaction.Builder<>(tableList, deleteModel)
.build())
.error(internalErrorCallback)
.success(internalSuccessCallback).build();
if (transact) {
transaction.execute();
} else {
transaction.executeSync();
}
return true;
}
/**
* Will not use the index, rather just call a {@link Model#update()}
*
* @param location Not used.
* @param object The object to update
* @return the updated model.
*/
@Override
public TModel set(int location, TModel object) {
return set(object);
}
/**
* Updates a Model {@link Model#update()} . If {@link #transact}
* is true, this update happens in the BG, otherwise it happens immediately.
*
* @param object The object to update
* @return The updated model.
*/
public TModel set(TModel object) {
Transaction transaction = FlowManager.getDatabaseForTable(internalCursorList.table())
.beginTransactionAsync(new ProcessModelTransaction.Builder<>(updateModel)
.add(object)
.build())
.error(internalErrorCallback)
.success(internalSuccessCallback).build();
if (transact) {
transaction.execute();
} else {
transaction.executeSync();
}
return object;
}
@Override
public int size() {
return (int) internalCursorList.getCount();
}
@NonNull
@Override
public List<TModel> subList(int start, int end) {
List<TModel> tableList = internalCursorList.getAll();
return tableList.subList(start, end);
}
@NonNull
@Override
public Object[] toArray() {
List<TModel> tableList = internalCursorList.getAll();
return tableList.toArray();
}
@NonNull
@Override
public <T> T[] toArray(T[] array) {
List<TModel> tableList = internalCursorList.getAll();
return tableList.toArray(array);
}
@Override
public void close() {
internalCursorList.close();
}
private final ProcessModelTransaction.ProcessModel<TModel> saveModel =
new ProcessModelTransaction.ProcessModel<TModel>() {
@Override
public void processModel(TModel model, DatabaseWrapper wrapper) {
getModelAdapter().save(model);
}
};
private final ProcessModelTransaction.ProcessModel<TModel> updateModel =
new ProcessModelTransaction.ProcessModel<TModel>() {
@Override
public void processModel(TModel model, DatabaseWrapper wrapper) {
getModelAdapter().update(model);
}
};
private final ProcessModelTransaction.ProcessModel<TModel> deleteModel =
new ProcessModelTransaction.ProcessModel<TModel>() {
@Override
public void processModel(TModel model, DatabaseWrapper wrapper) {
getModelAdapter().delete(model);
}
};
private final Transaction.Error internalErrorCallback = new Transaction.Error() {
@Override
public void onError(Transaction transaction, Throwable error) {
if (errorCallback != null) {
errorCallback.onError(transaction, error);
}
}
};
private final Transaction.Success internalSuccessCallback = new Transaction.Success() {
@Override
public void onSuccess(Transaction transaction) {
if (!isInTransaction) {
refreshAsync();
} else {
changeInTransaction = true;
}
if (successCallback != null) {
successCallback.onSuccess(transaction);
}
}
};
private final Runnable refreshRunnable = new Runnable() {
@Override
public void run() {
synchronized (this) {
pendingRefresh = false;
}
refresh();
}
};
public static class Builder<TModel> {
private final Class<TModel> table;
private boolean transact;
private boolean changeInTransaction;
private Cursor cursor;
private boolean cacheModels = true;
private ModelQueriable<TModel> modelQueriable;
private ModelCache<TModel, ?> modelCache;
private Transaction.Success success;
private Transaction.Error error;
private Builder(FlowCursorList<TModel> cursorList) {
table = cursorList.table();
cursor = cursorList.cursor();
cacheModels = cursorList.cachingEnabled();
modelQueriable = cursorList.modelQueriable();
modelCache = cursorList.modelCache();
}
public Builder(Class<TModel> table) {
this.table = table;
}
public Builder(@NonNull ModelQueriable<TModel> modelQueriable) {
this(modelQueriable.getTable());
modelQueriable(modelQueriable);
}
public Builder<TModel> cursor(Cursor cursor) {
this.cursor = cursor;
return this;
}
public Builder<TModel> modelQueriable(ModelQueriable<TModel> modelQueriable) {
this.modelQueriable = modelQueriable;
return this;
}
public Builder<TModel> transact(boolean transact) {
this.transact = transact;
return this;
}
public Builder<TModel> modelCache(ModelCache<TModel, ?> modelCache) {
this.modelCache = modelCache;
return this;
}
/**
* If true, when an operation occurs when we call endTransactionAndNotify, we refresh content.
*/
public Builder<TModel> changeInTransaction(boolean changeInTransaction) {
this.changeInTransaction = changeInTransaction;
return this;
}
public Builder<TModel> cacheModels(boolean cacheModels) {
this.cacheModels = cacheModels;
return this;
}
public Builder<TModel> success(Transaction.Success success) {
this.success = success;
return this;
}
public Builder<TModel> error(Transaction.Error error) {
this.error = error;
return this;
}
public FlowQueryList<TModel> build() {
return new FlowQueryList<>(this);
}
}
}