/**
* Copyright (C) 2015 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package org.thoughtcrime.securesms.database;
import android.content.Context;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.view.View;
import android.view.ViewGroup;
/**
* RecyclerView.Adapter that manages a Cursor, comparable to the CursorAdapter usable in ListView/GridView.
*/
public abstract class CursorRecyclerViewAdapter extends RecyclerView.Adapter {
private final Context context;
private final DataSetObserver observer = new AdapterDataSetObserver();
@VisibleForTesting static final int HEADER_TYPE = Integer.MIN_VALUE;
@VisibleForTesting static final int FOOTER_TYPE = Integer.MIN_VALUE + 1;
@VisibleForTesting static final long HEADER_ID = Long.MIN_VALUE;
@VisibleForTesting static final long FOOTER_ID = Long.MIN_VALUE + 1;
private Cursor cursor;
private boolean valid;
private @Nullable View header;
private @Nullable View footer;
private static class HeaderFooterViewHolder extends RecyclerView.ViewHolder {
public HeaderFooterViewHolder(View itemView) {
super(itemView);
}
}
protected CursorRecyclerViewAdapter(Context context, Cursor cursor) {
this.context = context;
this.cursor = cursor;
if (cursor != null) {
valid = true;
cursor.registerDataSetObserver(observer);
}
}
protected @NonNull Context getContext() {
return context;
}
public @Nullable Cursor getCursor() {
return cursor;
}
public void setHeaderView(@Nullable View header) {
this.header = header;
}
public void setFooterView(@Nullable View footer) {
this.footer = footer;
}
public boolean hasHeaderView() {
return header != null;
}
public boolean hasFooterView() {
return footer != null;
}
public void changeCursor(Cursor cursor) {
Cursor old = swapCursor(cursor);
if (old != null) {
old.close();
}
}
public Cursor swapCursor(Cursor newCursor) {
if (newCursor == cursor) {
return null;
}
final Cursor oldCursor = cursor;
if (oldCursor != null) {
oldCursor.unregisterDataSetObserver(observer);
}
cursor = newCursor;
if (cursor != null) {
cursor.registerDataSetObserver(observer);
}
valid = cursor != null;
notifyDataSetChanged();
return oldCursor;
}
@Override
public int getItemCount() {
if (!isActiveCursor()) return 0;
return cursor.getCount()
+ (hasHeaderView() ? 1 : 0)
+ (hasFooterView() ? 1 : 0);
}
@SuppressWarnings("unchecked")
@Override
public final void onViewRecycled(ViewHolder holder) {
if (!(holder instanceof HeaderFooterViewHolder)) {
onItemViewRecycled((VH)holder);
}
}
public void onItemViewRecycled(VH holder) {}
@Override
public final ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case HEADER_TYPE: return new HeaderFooterViewHolder(header);
case FOOTER_TYPE: return new HeaderFooterViewHolder(footer);
default: return onCreateItemViewHolder(parent, viewType);
}
}
public abstract VH onCreateItemViewHolder(ViewGroup parent, int viewType);
@SuppressWarnings("unchecked")
@Override
public final void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
if (!isHeaderPosition(position) && !isFooterPosition(position)) {
onBindItemViewHolder((VH)viewHolder, getCursorAtPositionOrThrow(position));
}
}
public abstract void onBindItemViewHolder(VH viewHolder, @NonNull Cursor cursor);
@Override
public final int getItemViewType(int position) {
if (isHeaderPosition(position)) return HEADER_TYPE;
if (isFooterPosition(position)) return FOOTER_TYPE;
return getItemViewType(getCursorAtPositionOrThrow(position));
}
public int getItemViewType(@NonNull Cursor cursor) {
return 0;
}
@Override
public final long getItemId(int position) {
if (isHeaderPosition(position)) return HEADER_ID;
if (isFooterPosition(position)) return FOOTER_ID;
long itemId = getItemId(getCursorAtPositionOrThrow(position));
return itemId <= Long.MIN_VALUE + 1 ? itemId + 2 : itemId;
}
public long getItemId(@NonNull Cursor cursor) {
return cursor.getLong(cursor.getColumnIndexOrThrow("_id"));
}
protected @NonNull Cursor getCursorAtPositionOrThrow(final int position) {
if (!isActiveCursor()) {
throw new IllegalStateException("this should only be called when the cursor is valid");
}
if (!cursor.moveToPosition(getCursorPosition(position))) {
throw new IllegalStateException("couldn't move cursor to position " + position);
}
return cursor;
}
protected boolean isActiveCursor() {
return valid && cursor != null;
}
private boolean isFooterPosition(int position) {
return hasFooterView() && position == getItemCount() - 1;
}
private boolean isHeaderPosition(int position) {
return hasHeaderView() && position == 0;
}
private int getCursorPosition(int position) {
return hasHeaderView() ? position - 1 : position;
}
private class AdapterDataSetObserver extends DataSetObserver {
@Override
public void onChanged() {
super.onChanged();
valid = true;
}
@Override
public void onInvalidated() {
super.onInvalidated();
valid = false;
}
}
}