/** * The MIT License (MIT) * * Copyright (c) 2013 Artur Malabarba * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * the Software, and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ package com.malabarba.hugesqlitecursor; import java.util.ArrayList; import android.annotation.TargetApi; import android.content.ContentResolver; import android.database.CharArrayBuffer; import android.database.ContentObserver; import android.database.Cursor; import android.database.DataSetObserver; import android.database.MergeCursor; import android.database.sqlite.SQLiteCursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.text.TextUtils; import android.util.Log; /** * A Cursor for SQLite queries, provided as a replacement for * SQLiteCursor, which loads the result rows on-demand. This makes it * several orders of magnitude more efficient (in time AND space) when * query results are Huge. * * See the constructor doc below for more information. * * @author Artur Malabarba * */ public class HugeSQLiteCursor implements Cursor { private final static String COUNT_COLUMN_NAME = "Count_01lc8a182nd110masdlk5"; private final static String TAG = "HugeSQLiteCursor"; private int mIdColumn = -1; private int mCount = -1; private int partialCount = -1; // Patial Count private String mTable; private String[] mColumns; private String mWhere; private String[] mSearchText; private int mStep; private Cursor mCursor; private ArrayList<Cursor> mCursors = new ArrayList<Cursor> (); private SQLiteDatabase db; /** * Creates an instance of HugeSQLiteCursor which is populated as * necessary with the query results. This means that queries with * huge results won't take long to load, in fact they'll take * almost exactly as long as a query with "{@link step}" results. * * The cursor created should behave identically to the one you'd * get by running db.query(table, columns, selection, * selectionArgs, null, null, "_id", null), except for performance * of course. * * In other words, this constructor is almost identical to a * query(...) command, except results will be sorted by the "_id" * column (this is necessary for HugeSQLiteCursor to work) and * will be loaded only on demand. This also has the consequence * that if the database changes before the cursor has been fully * loaded, then further loaded results will take into accout the * new database. * * @param db The SQLiteDatabase to use for the query. * @param step The cursor will be populated in increments of size * {@link step}. This should be invisible to the cursor * adapter being used, as further increments are * automatically loaded as necessary. The value * directly decides the initial load time of the * cursor: small values will initialize faster, but * will need to perform more frequent re-queries as the * user scrolls down the list (though you can manually * increase the "buffer" by using loadUpTo(...)). * Either way, this should never be less then the * number of items which fit on the screen, or more * then one query will be immediately necessary, * slowing down initialization. * @param table The table name to compile the query against. * @param columns A list of which columns to return. Passing null * will return all columns, which is discouraged to * prevent reading data from storage that isn't going * to be used. If neither a "*" nor a "_id" column is * present, then a "_id" column will be added at the * end. * @param selection A filter declaring which rows to return, formatted as an * SQL WHERE clause (excluding the WHERE itself). Passing null * will return all rows for the given table. * @param selectionArgs You may include ?s in selection, which * will be replaced by the values from selectionArgs, * in order that they appear in the selection. The * values will be bound as Strings. * @return A {@link HugeSQLiteCursor} object. * @see SQLiteCursor */ public HugeSQLiteCursor(SQLiteDatabase db, String step, String table, String[] columns, String selection, String[] searchText) { this.db = db; mTable = table; mWhere = selection; mSearchText = searchText; mStep = Integer.parseInt(step); if (columns == null) { mIdColumn = -2; // throw new IllegalArgumentException("columns can't be null " // +"(I need to know selection _id column is)"); } else { // Find the _id column, if it exists final int length = columns.length; for (int i = 0; i < length; i++) { // if (columns[i] == null) // throw new IllegalArgumentException("columns can't contain null before _id " // +"(I need to know selection _id column is)"); if ("*".equals(columns[i])) mIdColumn = -2; // throw new IllegalArgumentException("columns can't contain * before _id " // +"(I need to know selection _id column is)"); else if ("_id".equals(columns[i])) mIdColumn = i; } } // Add the _id column, if it doesn't exist (-1 means neither _id nor * were found) if (mIdColumn == -1) { mIdColumn = columns.length; mColumns = new String[mIdColumn + 1]; if (mIdColumn > 0) System.arraycopy(columns, 0, mColumns, 0, mIdColumn); mColumns[mIdColumn] = "_id"; } else mColumns = columns; // Write the columns request for the cursor String concatColumns = ""; if (mColumns == null) concatColumns = "*,"; else for(String str: mColumns) if (str != null) concatColumns += str + ", "; concatColumns += "(SELECT Count(*) FROM " + mTable + whereOrEmpty() + ") as "+COUNT_COLUMN_NAME; // We have doubled the number of ?s so we need to double the search text String[] doubleSearchText = null; if (mSearchText != null) { int size = mSearchText.length; doubleSearchText = new String[2 * size]; System.arraycopy(mSearchText, 0, doubleSearchText, 0, size); System.arraycopy(mSearchText, 0, doubleSearchText, size, size); } // Make the raw query and add it straight away mCursors.add(db.rawQuery("SELECT "+ concatColumns + " FROM " + ((mTable == null)?"":mTable) + whereOrEmpty() + " ORDER BY _id LIMIT " + step, doubleSearchText)); // mCursors.add(db.query(mTable, mColumns, mWhere, mSearchText, null, null, "_id", step)); mCursor = new MergeCursor(mCursors.toArray(new Cursor[mCursors.size()])); } private String whereOrEmpty() {return ((TextUtils.isEmpty(mWhere))? "" : " where " + mWhere);} private int queryCount(String[] searchText) { final int old = mCursor.getPosition(); mCursor.moveToFirst(); final int count = mCursor.getInt(mCursor.getColumnIndex(COUNT_COLUMN_NAME)); mCursor.moveToPosition(old); return count; } /** * How many rows have been loaded so far? * @return Number of rows loaded so far. * @see loadUpTo */ public int getPartialCount() { if (partialCount < 0) { if (mIdColumn < 0) mIdColumn = mCursor.getColumnIndex("_id"); partialCount = mCursor.getCount(); if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG,"Counted Partial Size ("+partialCount+")"); } return partialCount; } /* (non-Javadoc) * @see android.database.Cursor#getCount() */ @Override public int getCount() { if (mCount < 0) { mCount = (getPartialCount() > 0)? queryCount(mSearchText) : 0; if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG,"Counted Full Size ("+mCount+")"); } return mCount; } /* (non-Javadoc) * @see android.database.Cursor#moveToPosition(int) */ @Override public boolean moveToPosition(int arg0) { loadUpTo(arg0+mStep); if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG,"Moving to "+arg0); return mCursor.moveToPosition(arg0); } /** * Make sure the cursor is loaded at least up to position pos, but it could be a lot more. * * This allows you to manually "buffer" the cursor after it has * finished initializing. For instance, calling * cursor.loadUpTo(cursor.getCount()) loads the entire cursor. * This could be called once the app is not doing any work so that * items don't have to be loaded real-time when the user starts * scrolling (useful for slower systems). It can also be used if * you'd like to finalize the cursor because you're about to * change something in the database. * * @param pos The minimum position you want to be loaded. * @return void */ public void loadUpTo(int last) { int pCount = getPartialCount(); if ((pCount < getCount()) && (last >= pCount)) { //isFinished if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG,"Loading HugeSQLiteCursor up to ["+last+"] (currently it's "+(pCount-1)+")."); // Save the position for latter final int oldPos = mCursor.getPosition(); // Get the last _id we have mCursor.moveToLast(); final int lastId = mCursor.getInt(mIdColumn); // How Many steps to we need to add? final int times = (last + 1 - pCount)/mStep + (((last + 1 - pCount) % mStep) == 0 ? 1 : 2); // 1 more then necessary, for safety. Cursor newCursor = db.query(mTable, mColumns, ((mWhere == null)? "": mWhere + " AND ") + "_id > " + lastId, mSearchText, null, null, "_id", Integer.toString(times*mStep)); // isFinished = (newCursor.getCount() < times*mStep); mCursors.add(newCursor); mCursor = new MergeCursor(mCursors.toArray(new Cursor[mCursors.size()])); partialCount = mCursor.getCount(); mCursor.moveToPosition(oldPos); if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG,"Loaded HugeSQLiteCursor up to ["+(partialCount-1)+"]."); } } /* (non-Javadoc) * @see android.database.Cursor#isLast() */ @Override public boolean isLast() { int cnt = getCount(); return ((getPosition() == (cnt - 1))) && (cnt != 0); //&& isFinished; } /* (non-Javadoc) * @see android.database.Cursor#isAfterLast() */ @Override public final boolean isAfterLast() { return (getCount() == 0) || (getPosition() == getCount()); } // Methods that don't change /* (non-Javadoc) * @see android.database.Cursor#close() */ @Override public void close() {mCursor.close();} /* (non-Javadoc) * @see android.database.Cursor#copyStringToBuffer(int, android.database.CharArrayBuffer) */ @Override public void copyStringToBuffer(int arg0, CharArrayBuffer arg1) {mCursor.copyStringToBuffer(arg0, arg1);} /* (non-Javadoc) * @see android.database.Cursor#deactivate() */ @Override @Deprecated public void deactivate() {mCursor.deactivate();} /* (non-Javadoc) * @see android.database.Cursor#getBlob(int) */ @Override public byte[] getBlob(int arg0) {return mCursor.getBlob(arg0);} /* (non-Javadoc) * @see android.database.Cursor#getColumnCount() */ @Override public int getColumnCount() {return mCursor.getColumnCount();} /* (non-Javadoc) * @see android.database.Cursor#getColumnIndex(java.lang.String) */ @Override public int getColumnIndex(String columnName) {return mCursor.getColumnIndex(columnName);} /* (non-Javadoc) * @see android.database.Cursor#getColumnIndexOrThrow(java.lang.String) */ @Override public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {return mCursor.getColumnIndexOrThrow(columnName);} /* (non-Javadoc) * @see android.database.Cursor#getColumnName(int) */ @Override public String getColumnName(int columnIndex) {return mCursor.getColumnName(columnIndex);} /* (non-Javadoc) * @see android.database.Cursor#getColumnNames() */ @Override public String[] getColumnNames() {return mCursor.getColumnNames();} /* (non-Javadoc) * @see android.database.Cursor#getDouble(int) */ @Override public double getDouble(int columnIndex) {return mCursor.getDouble(columnIndex);} /* (non-Javadoc) * @see android.database.Cursor#getExtras() */ @Override public Bundle getExtras() {return mCursor.getExtras();} /* (non-Javadoc) * @see android.database.Cursor#getFloat(int) */ @Override public float getFloat(int columnIndex) {return mCursor.getFloat(columnIndex);} /* (non-Javadoc) * @see android.database.Cursor#getInt(int) */ @Override public int getInt(int columnIndex) {return mCursor.getInt(columnIndex);} /* (non-Javadoc) * @see android.database.Cursor#getLong(int) */ @Override public long getLong(int columnIndex) {return mCursor.getLong(columnIndex);} /* (non-Javadoc) * @see android.database.Cursor#getPosition() */ @Override public int getPosition() {return mCursor.getPosition();} /* (non-Javadoc) * @see android.database.Cursor#getShort(int) */ @Override public short getShort(int columnIndex) {return mCursor.getShort(columnIndex);} /* (non-Javadoc) * @see android.database.Cursor#getString(int) */ @Override public String getString(int columnIndex) {return mCursor.getString(columnIndex);} /* (non-Javadoc) * @see android.database.Cursor#getType(int) */ @TargetApi(Build.VERSION_CODES.HONEYCOMB) @Override public int getType(int columnIndex) {return mCursor.getType(columnIndex);} /* (non-Javadoc) * @see android.database.Cursor#getWantsAllOnMoveCalls() */ @Override public boolean getWantsAllOnMoveCalls() {return mCursor.getWantsAllOnMoveCalls();} /* (non-Javadoc) * @see android.database.Cursor#isBeforeFirst() */ @Override public boolean isBeforeFirst() {return mCursor.isBeforeFirst();} /* (non-Javadoc) * @see android.database.Cursor#isClosed() */ @Override public boolean isClosed() {return mCursor.isClosed();} /* (non-Javadoc) * @see android.database.Cursor#isFirst() */ @Override public boolean isFirst() {return mCursor.isFirst();} /* (non-Javadoc) * @see android.database.Cursor#isNull(int) */ @Override public boolean isNull(int columnIndex) {return mCursor.isNull(columnIndex);} /* (non-Javadoc) * @see android.database.Cursor#moveToPrevious() */ @Override public final boolean moveToPrevious() {return moveToPosition(getPosition() - 1);} /* (non-Javadoc) * @see android.database.Cursor#move(int) */ @Override public boolean move(int offset) {return moveToPosition(getPosition() + offset);} /* (non-Javadoc) * @see android.database.Cursor#moveToFirst() */ @Override public boolean moveToFirst() { // TODO Auto-generated method stub return false; } /* (non-Javadoc) * @see android.database.Cursor#moveToLast() */ @Override public boolean moveToLast() { // TODO Auto-generated method stub return false; } /* (non-Javadoc) * @see android.database.Cursor#moveToNext() */ @Override public boolean moveToNext() { // TODO Auto-generated method stub return false; } /* (non-Javadoc) * @see android.database.Cursor#registerContentObserver(android.database.ContentObserver) */ @Override public void registerContentObserver(ContentObserver arg0) {mCursor.registerContentObserver(arg0);} /* (non-Javadoc) * @see android.database.Cursor#registerDataSetObserver(android.database.DataSetObserver) */ @Override public void registerDataSetObserver(DataSetObserver arg0) {mCursor.registerDataSetObserver(arg0);} /* (non-Javadoc) * @see android.database.Cursor#requery() */ @Override @Deprecated public boolean requery() {return mCursor.requery();} /* (non-Javadoc) * @see android.database.Cursor#respond(android.os.Bundle) */ @Override public Bundle respond(Bundle arg0) {return mCursor.respond(arg0);} /* (non-Javadoc) * @see android.database.Cursor#setNotificationUri(android.content.ContentResolver, android.net.Uri) */ @Override public void setNotificationUri(ContentResolver arg0, Uri arg1) {mCursor.setNotificationUri(arg0, arg1);} /* (non-Javadoc) * @see android.database.Cursor#unregisterContentObserver(android.database.ContentObserver) */ @Override public void unregisterContentObserver(ContentObserver arg0) {mCursor.unregisterContentObserver(arg0);} /* (non-Javadoc) * @see android.database.Cursor#unregisterDataSetObserver(android.database.DataSetObserver) */ @Override public void unregisterDataSetObserver(DataSetObserver arg0) {mCursor.unregisterDataSetObserver(arg0);} }