/*
* Copyright 2017 requery.io
*
* 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 io.requery.android;
import android.database.Cursor;
import android.os.Handler;
import android.support.v7.widget.RecyclerView;
import io.requery.meta.EntityModel;
import io.requery.meta.Type;
import io.requery.proxy.EntityProxy;
import io.requery.query.Result;
import io.requery.sql.EntityDataStore;
import io.requery.sql.ResultSetIterator;
import io.requery.util.function.Function;
import java.io.Closeable;
import java.sql.SQLException;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/**
* An implementation of {@link android.support.v7.widget.RecyclerView.Adapter} specifically for
* displaying items from a {@link EntityDataStore} query. To use extend this class and implement
* {@link #performQuery()} and {@link #onBindViewHolder(Object, RecyclerView.ViewHolder, int)}.
*
* @param <E> entity element type
* @param <VH> view holder type
*
* @author Nikhil Purushe
*/
@SuppressWarnings("WeakerAccess")
public abstract class QueryRecyclerAdapter<E, VH extends RecyclerView.ViewHolder> extends
RecyclerView.Adapter<VH> implements Closeable {
private final Handler handler;
private final Function<E, EntityProxy<E>> proxyProvider;
private ResultSetIterator<E> iterator;
private boolean createdExecutor;
private ExecutorService executor;
private Future<Result<E>> queryFuture;
/**
* Creates a new adapter instance.
*
* @param model database entity model
* @param type entity class type
*/
protected QueryRecyclerAdapter(EntityModel model, Class<E> type) {
this(model.typeOf(type));
}
/**
* Creates a new adapter instance without any type mapping.
*/
protected QueryRecyclerAdapter() {
this(null);
}
/**
* Creates a new adapter instance mapped to the given type.
*
* @param type entity type
*/
protected QueryRecyclerAdapter(Type<E> type) {
setHasStableIds(true);
proxyProvider = type == null ? null : type.getProxyProvider();
handler = new Handler();
}
/**
* Call this to clean up the underlying result.
*/
@Override
public void close() {
if (queryFuture != null) {
queryFuture.cancel(true);
}
if (iterator != null) {
iterator.close();
iterator = null;
}
setExecutor(null);
}
/**
* @return the underlying iterator being used or null if none
*/
protected ResultSetIterator<E> getIterator() {
return iterator;
}
/**
* Sets the {@link Executor} used for running the query off the main thread.
*
* @param executor ExecutorService to use for the background query.
*/
public void setExecutor(ExecutorService executor) {
if (createdExecutor && this.executor != null) {
this.executor.shutdown();
}
this.executor = executor;
}
/**
* Sets the results the adapter should display.
*
* @param iterator result set iterator
*/
public void setResult(ResultSetIterator<E> iterator) {
if (this.iterator != null) {
this.iterator.close();
}
this.iterator = iterator;
notifyDataSetChanged();
}
/**
* Schedules the query to be run in the background. After completion
* {@link #setResult(ResultSetIterator)} will be called to update the contents of the adapter.
* Note if an {@link Executor} was not specified one will be created automatically to run the
* query.
*/
public void queryAsync() {
if (this.executor == null) {
this.executor = Executors.newSingleThreadExecutor();
createdExecutor = true;
}
if (queryFuture != null && !queryFuture.isDone()) {
queryFuture.cancel(true);
}
queryFuture = executor.submit(new Callable<Result<E>>() {
@Override
public Result<E> call() {
final Result<E> result = performQuery();
// main work happens here
final ResultSetIterator<E> iterator = (ResultSetIterator<E>) result.iterator();
handler.post(new Runnable() {
@Override
public void run() {
setResult(iterator);
}
});
return result;
}
});
}
/**
* Implement this method with your query operation. Note this method is executed on a
* background thread not the main thread.
*
* @see #setExecutor(ExecutorService)
*
* @return query result set
*/
public abstract Result<E> performQuery();
@Override
public void onBindViewHolder(VH holder, int position) {
E item = iterator.get(position);
onBindViewHolder(item, holder, position);
}
/**
* Called to display the data at the specified position for the given item. The item is
* retrieved from the result iterator.
*
* @param item entity element to bind to the view
* @param holder view holder to be updated
* @param position position index of the view
*/
public abstract void onBindViewHolder(E item, VH holder, int position);
@Override
public long getItemId(int position) {
E item = iterator.get(position);
if (item == null) {
throw new IllegalStateException();
}
Object key = null;
if (proxyProvider != null) {
EntityProxy<? extends E> proxy = proxyProvider.apply(item);
key = proxy.key();
}
return key == null ? item.hashCode() : key.hashCode();
}
@Override
public int getItemCount() {
if (iterator == null) {
return 0;
}
try {
Cursor cursor = iterator.unwrap(Cursor.class);
return cursor.getCount();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public int getItemViewType(int position) {
E item = iterator.get(position);
return getItemViewType(item);
}
/**
* Return the view type of the item.
*
* @param item being checked
* @return integer identifying the type of the view
*/
protected int getItemViewType(E item) {
return 0;
}
@Override
public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
super.onDetachedFromRecyclerView(recyclerView);
close();
setExecutor(null);
}
}