gecko-dev/mobile/android/base/db/TopSitesCursorWrapper.java
2014-10-12 00:28:29 +01:00

568 lines
15 KiB
Java

package org.mozilla.gecko.db;
import java.util.HashMap;
import java.util.Map;
import org.mozilla.gecko.db.BrowserContract.Bookmarks;
import org.mozilla.gecko.db.BrowserContract.TopSites;
import android.content.ContentResolver;
import android.database.CharArrayBuffer;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.net.Uri;
import android.os.Bundle;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
/**
* {@TopSitesCursorWrapper} is a cursor wrapper that merges
* the top and pinned sites cursors into one. It ensures the
* cursor will contain at least a given minimum number of
* entries.
*/
public class TopSitesCursorWrapper implements Cursor {
private enum RowType {
UNKNOWN,
BLANK,
TOP,
PINNED,
SUGGESTED
}
private static final String[] columnNames = new String[] {
TopSites._ID,
TopSites.URL,
TopSites.TITLE,
TopSites.BOOKMARK_ID,
TopSites.HISTORY_ID,
TopSites.TYPE,
};
private static final Map<String, Integer> columnIndexes =
new HashMap<String, Integer>(columnNames.length);
static {
for (int i = 0; i < columnNames.length; i++) {
columnIndexes.put(columnNames[i], i);
}
}
// Maps column indexes from the wrapper to the cursor's.
private SparseIntArray topIndexes;
private SparseIntArray pinnedIndexes;
private SparseIntArray suggestedIndexes;
// Type of content in the current position
private RowType currentRowType;
// Currently active cursor
private Cursor currentCursor;
// The cursor for the top sites query
private final Cursor topCursor;
// The cursor for the pinned sites query
private final Cursor pinnedCursor;
// The cursor for the suggested sites query
private final Cursor suggestedCursor;
// Associates pinned sites and their respective positions
private SparseBooleanArray pinnedPositions;
// Current position of the cursor
private int currentPosition = -1;
// Number of pinned sites before the current position
private int pinnedBefore;
// The size of the cursor wrapper
private int count;
// The minimum size of the cursor wrapper
private final int minSize;
public TopSitesCursorWrapper(Cursor pinnedCursor, Cursor topCursor, int minSize) {
this(pinnedCursor, topCursor, null, minSize);
}
public TopSitesCursorWrapper(Cursor pinnedCursor, Cursor topCursor, Cursor suggestedCursor, int minSize) {
currentRowType = RowType.UNKNOWN;
this.minSize = minSize;
this.topCursor = topCursor;
this.pinnedCursor = pinnedCursor;
this.suggestedCursor = suggestedCursor;
updateIndexMaps();
updatePinnedPositions();
updateCount();
}
private void updateIndexMaps() {
topIndexes = new SparseIntArray(topCursor.getColumnCount());
updateIndexMapFromCursor(topIndexes, topCursor);
pinnedIndexes = new SparseIntArray(pinnedCursor.getColumnCount());
updateIndexMapFromCursor(pinnedIndexes, pinnedCursor);
if (suggestedCursor != null) {
suggestedIndexes = new SparseIntArray(suggestedCursor.getColumnCount());
updateIndexMapFromCursor(suggestedIndexes, suggestedCursor);
}
}
private static void updateIndexMapFromCursor(SparseIntArray indexMap, Cursor c) {
final int columnCount = c.getColumnCount();
for (int i = 0; i < columnCount; i++) {
final Integer index = columnIndexes.get(c.getColumnName(i));
if (index != null) {
indexMap.put(index, i);
}
}
}
private void updatePinnedPositions() {
if (pinnedPositions == null) {
pinnedPositions = new SparseBooleanArray();
} else {
pinnedPositions.clear();
}
pinnedCursor.moveToPosition(-1);
while (pinnedCursor.moveToNext()) {
int pos = pinnedCursor.getInt(pinnedCursor.getColumnIndex(Bookmarks.POSITION));
pinnedPositions.put(pos, true);
};
}
private void updateCount() {
int sum = pinnedCursor.getCount() + topCursor.getCount();
if (suggestedCursor != null) {
sum += suggestedCursor.getCount();
}
count = Math.max(minSize, sum);
}
private static boolean cursorHasValidPosition(Cursor c) {
return (c != null && !c.isBeforeFirst() && !c.isAfterLast());
}
private void updatePinnedBefore(int position) {
pinnedBefore = 0;
for (int i = 0; i < position; i++) {
if (pinnedPositions.get(i)) {
pinnedBefore++;
}
}
}
private void setCurrentCursor(Cursor cursor, int position, RowType rowType) {
cursor.moveToPosition(position);
currentRowType = rowType;
currentCursor = cursor;
}
private boolean updateTopCursorPosition(int position) {
final int index = position - pinnedBefore;
if (index >= 0 && index < topCursor.getCount()) {
setCurrentCursor(topCursor, index, RowType.TOP);
return true;
}
return false;
}
private boolean updatePinnedCursorPosition(int position) {
if (position >= minSize) {
return false;
}
if (pinnedPositions.get(position)) {
setCurrentCursor(pinnedCursor, pinnedPositions.indexOfKey(position), RowType.PINNED);
return true;
}
return false;
}
private boolean updateSuggestedCursorPosition(int position) {
if (suggestedCursor == null) {
return false;
}
if (position >= minSize) {
return false;
}
final int index = position - pinnedBefore - topCursor.getCount();
if (index >= 0 && index < suggestedCursor.getCount()) {
setCurrentCursor(suggestedCursor, index, RowType.SUGGESTED);
return true;
}
return false;
}
private void assertValidColumnIndex(int columnIndex) {
if (columnIndex < 0 || columnIndex > columnNames.length - 1) {
throw new IllegalArgumentException("Column index is out of bounds: " + columnIndex);
}
}
private void assertValidRowType() {
if (currentRowType == RowType.UNKNOWN) {
throw new IllegalStateException("No provided cursor holds data at this position");
}
}
private int getColumnIndexForCurrentRowType(int columnIndex) {
assertValidRowType();
assertValidColumnIndex(columnIndex);
SparseIntArray map = null;
switch (currentRowType) {
case TOP:
map = topIndexes;
break;
case PINNED:
map = pinnedIndexes;
break;
case SUGGESTED:
map = suggestedIndexes;
break;
}
if (map != null) {
return map.get(columnIndex, -1);
}
return -1;
}
@Override
public int getPosition() {
return currentPosition;
}
@Override
public int getCount() {
return count;
}
@Override
public boolean isAfterLast() {
return (currentPosition >= count);
}
@Override
public boolean isBeforeFirst() {
return (currentPosition < 0);
}
@Override
public boolean isFirst() {
return (currentPosition == 0);
}
@Override
public boolean isLast() {
return (currentPosition == count - 1);
}
@Override
public boolean moveToNext() {
return moveToPosition(currentPosition + 1);
}
@Override
public boolean moveToPrevious() {
return moveToPosition(currentPosition - 1);
}
@Override
public boolean moveToFirst() {
return moveToPosition(0);
}
@Override
public boolean moveToLast() {
return moveToPosition(count - 1);
}
@Override
public boolean move(int offset) {
return moveToPosition(currentPosition + offset);
}
@Override
public boolean moveToPosition(int position) {
currentPosition = position;
updatePinnedBefore(position);
if (!updatePinnedCursorPosition(position) &&
!updateTopCursorPosition(position) &&
!updateSuggestedCursorPosition(position)) {
if (position >= 0 && position < minSize) {
currentRowType = RowType.BLANK;
} else {
currentRowType = RowType.UNKNOWN;
}
currentCursor = null;
}
return cursorHasValidPosition(this);
}
@Override
public long getLong(int columnIndex) {
final int index = getColumnIndexForCurrentRowType(columnIndex);
if (index >= 0) {
return currentCursor.getLong(index);
}
return 0;
}
@Override
public int getInt(int columnIndex) {
assertValidRowType();
assertValidColumnIndex(columnIndex);
if (columnNames[columnIndex].equals(TopSites.TYPE)) {
switch (currentRowType) {
case BLANK:
return TopSites.TYPE_BLANK;
case TOP:
return TopSites.TYPE_TOP;
case PINNED:
return TopSites.TYPE_PINNED;
case SUGGESTED:
return TopSites.TYPE_SUGGESTED;
default:
return -1;
}
}
final int index = getColumnIndexForCurrentRowType(columnIndex);
if (index >= 0) {
return currentCursor.getInt(index);
}
return 0;
}
@Override
public String getString(int columnIndex) {
final int index = getColumnIndexForCurrentRowType(columnIndex);
if (index >= 0) {
return currentCursor.getString(index);
}
return "";
}
@Override
public float getFloat(int columnIndex) {
throw new UnsupportedOperationException();
}
@Override
public double getDouble(int columnIndex) {
throw new UnsupportedOperationException();
}
@Override
public short getShort(int columnIndex) {
throw new UnsupportedOperationException();
}
@Override
public byte[] getBlob(int columnIndex) {
throw new UnsupportedOperationException();
}
@Override
public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
throw new UnsupportedOperationException();
}
@Override
public boolean isNull(int columnIndex) {
final int index = getColumnIndexForCurrentRowType(columnIndex);
if (index >= 0) {
return currentCursor.isNull(index);
}
return true;
}
@Override
public int getType(int columnIndex) {
throw new UnsupportedOperationException();
}
@Override
public int getColumnCount() {
return columnNames.length;
}
@Override
public int getColumnIndex(String columnName) {
final Integer index = columnIndexes.get(columnName);
if (index == null) {
return -1;
}
return index;
}
@Override
public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
final int index = getColumnIndex(columnName);
if (index < 0) {
throw new IllegalArgumentException("Column index not found: " + columnName);
}
return index;
}
@Override
public String getColumnName(int columnIndex) {
return columnNames[columnIndex];
}
@Override
public String[] getColumnNames() {
return columnNames;
}
@Override
public boolean requery() {
boolean result = topCursor.requery() && pinnedCursor.requery();
if (suggestedCursor != null) {
result &= suggestedCursor.requery();
}
updatePinnedPositions();
updateCount();
return result;
}
@Override
public Bundle respond(Bundle extras) {
throw new UnsupportedOperationException();
}
@Override
public Bundle getExtras() {
throw new UnsupportedOperationException();
}
@Override
public boolean getWantsAllOnMoveCalls() {
return false;
}
@Override
public Uri getNotificationUri() {
// There's no single notification URI for the wrapper
return null;
}
@Override
public void setNotificationUri(ContentResolver cr, Uri uri) {
// Keep the original notification URI for the
// wrapped cursors so that we get proper change
// notifications from the ContentResolver.
}
@Override
public void registerContentObserver(ContentObserver observer) {
topCursor.registerContentObserver(observer);
pinnedCursor.registerContentObserver(observer);
if (suggestedCursor != null) {
suggestedCursor.registerContentObserver(observer);
}
}
@Override
public void unregisterContentObserver(ContentObserver observer) {
topCursor.unregisterContentObserver(observer);
pinnedCursor.unregisterContentObserver(observer);
if (suggestedCursor != null) {
suggestedCursor.unregisterContentObserver(observer);
}
}
@Override
public void registerDataSetObserver(DataSetObserver observer) {
topCursor.registerDataSetObserver(observer);
pinnedCursor.registerDataSetObserver(observer);
if (suggestedCursor != null) {
suggestedCursor.registerDataSetObserver(observer);
}
}
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
topCursor.unregisterDataSetObserver(observer);
pinnedCursor.unregisterDataSetObserver(observer);
if (suggestedCursor != null) {
suggestedCursor.unregisterDataSetObserver(observer);
}
}
@Override
public void deactivate() {
topCursor.deactivate();
pinnedCursor.deactivate();
if (suggestedCursor != null) {
suggestedCursor.deactivate();
}
}
@Override
public boolean isClosed() {
boolean result = topCursor.isClosed() && pinnedCursor.isClosed();
if (suggestedCursor != null) {
result &= suggestedCursor.isClosed();
}
return result;
}
@Override
public void close() {
topCursor.close();
topIndexes = null;
pinnedCursor.close();
pinnedIndexes = null;
pinnedPositions = null;
if (suggestedCursor != null) {
suggestedCursor.close();
suggestedIndexes = null;
}
}
}