public final class

DefaultDownloadIndex

extends java.lang.Object

implements WritableDownloadIndex

 java.lang.Object

↳androidx.media3.exoplayer.offline.DefaultDownloadIndex

Gradle dependencies

compile group: 'androidx.media3', name: 'media3-exoplayer', version: '1.0.0-alpha03'

  • groupId: androidx.media3
  • artifactId: media3-exoplayer
  • version: 1.0.0-alpha03

Artifact androidx.media3:media3-exoplayer:1.0.0-alpha03 it located at Google repository (https://maven.google.com/)

Overview

A DownloadIndex that uses SQLite to persist Downloads.

Summary

Constructors
publicDefaultDownloadIndex(DatabaseProvider databaseProvider)

Creates an instance that stores the Downloads in an SQLite database provided by a DatabaseProvider.

publicDefaultDownloadIndex(DatabaseProvider databaseProvider, java.lang.String name)

Creates an instance that stores the Downloads in an SQLite database provided by a DatabaseProvider.

Methods
public DownloadgetDownload(java.lang.String id)

public DownloadCursorgetDownloads(int[] states[])

public voidputDownload(Download download)

public voidremoveDownload(java.lang.String id)

public voidsetDownloadingStatesToQueued()

public voidsetStatesToRemoving()

public voidsetStopReason(int stopReason)

public voidsetStopReason(java.lang.String id, int stopReason)

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public DefaultDownloadIndex(DatabaseProvider databaseProvider)

Creates an instance that stores the Downloads in an SQLite database provided by a DatabaseProvider.

Equivalent to calling DefaultDownloadIndex.DefaultDownloadIndex(DatabaseProvider, String) with name="".

Applications that only have one download index may use this constructor. Applications that have multiple download indices should call DefaultDownloadIndex.DefaultDownloadIndex(DatabaseProvider, String) to specify a unique name for each index.

Parameters:

databaseProvider: Provides the SQLite database in which downloads are persisted.

public DefaultDownloadIndex(DatabaseProvider databaseProvider, java.lang.String name)

Creates an instance that stores the Downloads in an SQLite database provided by a DatabaseProvider.

Parameters:

databaseProvider: Provides the SQLite database in which downloads are persisted.
name: The name of the index. This name is incorporated into the names of the SQLite tables in which downloads are persisted.

Methods

public Download getDownload(java.lang.String id)

public DownloadCursor getDownloads(int[] states[])

public void putDownload(Download download)

public void removeDownload(java.lang.String id)

public void setDownloadingStatesToQueued()

public void setStatesToRemoving()

public void setStopReason(int stopReason)

public void setStopReason(java.lang.String id, int stopReason)

Source

/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * 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 androidx.media3.exoplayer.offline;

import static androidx.media3.common.util.Assertions.checkNotNull;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.StreamKey;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.database.DatabaseIOException;
import androidx.media3.database.DatabaseProvider;
import androidx.media3.database.VersionTable;
import androidx.media3.exoplayer.offline.Download.FailureReason;
import androidx.media3.exoplayer.offline.Download.State;
import java.util.ArrayList;
import java.util.List;

/** A {@link DownloadIndex} that uses SQLite to persist {@link Download Downloads}. */
@UnstableApi
public final class DefaultDownloadIndex implements WritableDownloadIndex {

  private static final String TABLE_PREFIX = DatabaseProvider.TABLE_PREFIX + "Downloads";

  @VisibleForTesting /* package */ static final int TABLE_VERSION = 3;

  private static final String COLUMN_ID = "id";
  private static final String COLUMN_MIME_TYPE = "mime_type";
  private static final String COLUMN_URI = "uri";
  private static final String COLUMN_STREAM_KEYS = "stream_keys";
  private static final String COLUMN_CUSTOM_CACHE_KEY = "custom_cache_key";
  private static final String COLUMN_DATA = "data";
  private static final String COLUMN_STATE = "state";
  private static final String COLUMN_START_TIME_MS = "start_time_ms";
  private static final String COLUMN_UPDATE_TIME_MS = "update_time_ms";
  private static final String COLUMN_CONTENT_LENGTH = "content_length";
  private static final String COLUMN_STOP_REASON = "stop_reason";
  private static final String COLUMN_FAILURE_REASON = "failure_reason";
  private static final String COLUMN_PERCENT_DOWNLOADED = "percent_downloaded";
  private static final String COLUMN_BYTES_DOWNLOADED = "bytes_downloaded";
  private static final String COLUMN_KEY_SET_ID = "key_set_id";

  private static final int COLUMN_INDEX_ID = 0;
  private static final int COLUMN_INDEX_MIME_TYPE = 1;
  private static final int COLUMN_INDEX_URI = 2;
  private static final int COLUMN_INDEX_STREAM_KEYS = 3;
  private static final int COLUMN_INDEX_CUSTOM_CACHE_KEY = 4;
  private static final int COLUMN_INDEX_DATA = 5;
  private static final int COLUMN_INDEX_STATE = 6;
  private static final int COLUMN_INDEX_START_TIME_MS = 7;
  private static final int COLUMN_INDEX_UPDATE_TIME_MS = 8;
  private static final int COLUMN_INDEX_CONTENT_LENGTH = 9;
  private static final int COLUMN_INDEX_STOP_REASON = 10;
  private static final int COLUMN_INDEX_FAILURE_REASON = 11;
  private static final int COLUMN_INDEX_PERCENT_DOWNLOADED = 12;
  private static final int COLUMN_INDEX_BYTES_DOWNLOADED = 13;
  private static final int COLUMN_INDEX_KEY_SET_ID = 14;

  private static final String WHERE_ID_EQUALS = COLUMN_ID + " = ?";
  private static final String WHERE_STATE_IS_DOWNLOADING =
      COLUMN_STATE + " = " + Download.STATE_DOWNLOADING;
  private static final String WHERE_STATE_IS_TERMINAL =
      getStateQuery(Download.STATE_COMPLETED, Download.STATE_FAILED);

  private static final String[] COLUMNS =
      new String[] {
        COLUMN_ID,
        COLUMN_MIME_TYPE,
        COLUMN_URI,
        COLUMN_STREAM_KEYS,
        COLUMN_CUSTOM_CACHE_KEY,
        COLUMN_DATA,
        COLUMN_STATE,
        COLUMN_START_TIME_MS,
        COLUMN_UPDATE_TIME_MS,
        COLUMN_CONTENT_LENGTH,
        COLUMN_STOP_REASON,
        COLUMN_FAILURE_REASON,
        COLUMN_PERCENT_DOWNLOADED,
        COLUMN_BYTES_DOWNLOADED,
        COLUMN_KEY_SET_ID
      };

  private static final String TABLE_SCHEMA =
      "("
          + COLUMN_ID
          + " TEXT PRIMARY KEY NOT NULL,"
          + COLUMN_MIME_TYPE
          + " TEXT,"
          + COLUMN_URI
          + " TEXT NOT NULL,"
          + COLUMN_STREAM_KEYS
          + " TEXT NOT NULL,"
          + COLUMN_CUSTOM_CACHE_KEY
          + " TEXT,"
          + COLUMN_DATA
          + " BLOB NOT NULL,"
          + COLUMN_STATE
          + " INTEGER NOT NULL,"
          + COLUMN_START_TIME_MS
          + " INTEGER NOT NULL,"
          + COLUMN_UPDATE_TIME_MS
          + " INTEGER NOT NULL,"
          + COLUMN_CONTENT_LENGTH
          + " INTEGER NOT NULL,"
          + COLUMN_STOP_REASON
          + " INTEGER NOT NULL,"
          + COLUMN_FAILURE_REASON
          + " INTEGER NOT NULL,"
          + COLUMN_PERCENT_DOWNLOADED
          + " REAL NOT NULL,"
          + COLUMN_BYTES_DOWNLOADED
          + " INTEGER NOT NULL,"
          + COLUMN_KEY_SET_ID
          + " BLOB NOT NULL)";

  private static final String TRUE = "1";

  private final String name;
  private final String tableName;
  private final DatabaseProvider databaseProvider;
  private final Object initializationLock;

  @GuardedBy("initializationLock")
  private boolean initialized;

  /**
   * Creates an instance that stores the {@link Download Downloads} in an SQLite database provided
   * by a {@link DatabaseProvider}.
   *
   * <p>Equivalent to calling {@link #DefaultDownloadIndex(DatabaseProvider, String)} with {@code
   * name=""}.
   *
   * <p>Applications that only have one download index may use this constructor. Applications that
   * have multiple download indices should call {@link #DefaultDownloadIndex(DatabaseProvider,
   * String)} to specify a unique name for each index.
   *
   * @param databaseProvider Provides the SQLite database in which downloads are persisted.
   */
  public DefaultDownloadIndex(DatabaseProvider databaseProvider) {
    this(databaseProvider, "");
  }

  /**
   * Creates an instance that stores the {@link Download Downloads} in an SQLite database provided
   * by a {@link DatabaseProvider}.
   *
   * @param databaseProvider Provides the SQLite database in which downloads are persisted.
   * @param name The name of the index. This name is incorporated into the names of the SQLite
   *     tables in which downloads are persisted.
   */
  public DefaultDownloadIndex(DatabaseProvider databaseProvider, String name) {
    this.name = name;
    this.databaseProvider = databaseProvider;
    tableName = TABLE_PREFIX + name;
    initializationLock = new Object();
  }

  @Override
  @Nullable
  public Download getDownload(String id) throws DatabaseIOException {
    ensureInitialized();
    try (Cursor cursor = getCursor(WHERE_ID_EQUALS, new String[] {id})) {
      if (cursor.getCount() == 0) {
        return null;
      }
      cursor.moveToNext();
      return getDownloadForCurrentRow(cursor);
    } catch (SQLiteException e) {
      throw new DatabaseIOException(e);
    }
  }

  @Override
  public DownloadCursor getDownloads(@Download.State int... states) throws DatabaseIOException {
    ensureInitialized();
    Cursor cursor = getCursor(getStateQuery(states), /* selectionArgs= */ null);
    return new DownloadCursorImpl(cursor);
  }

  @Override
  public void putDownload(Download download) throws DatabaseIOException {
    ensureInitialized();
    try {
      SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
      putDownloadInternal(download, writableDatabase);
    } catch (SQLiteException e) {
      throw new DatabaseIOException(e);
    }
  }

  @Override
  public void removeDownload(String id) throws DatabaseIOException {
    ensureInitialized();
    try {
      databaseProvider.getWritableDatabase().delete(tableName, WHERE_ID_EQUALS, new String[] {id});
    } catch (SQLiteException e) {
      throw new DatabaseIOException(e);
    }
  }

  @Override
  public void setDownloadingStatesToQueued() throws DatabaseIOException {
    ensureInitialized();
    try {
      ContentValues values = new ContentValues();
      values.put(COLUMN_STATE, Download.STATE_QUEUED);
      SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
      writableDatabase.update(tableName, values, WHERE_STATE_IS_DOWNLOADING, /* whereArgs= */ null);
    } catch (SQLException e) {
      throw new DatabaseIOException(e);
    }
  }

  @Override
  public void setStatesToRemoving() throws DatabaseIOException {
    ensureInitialized();
    try {
      ContentValues values = new ContentValues();
      values.put(COLUMN_STATE, Download.STATE_REMOVING);
      // Only downloads in STATE_FAILED are allowed a failure reason, so we need to clear it here in
      // case we're moving downloads from STATE_FAILED to STATE_REMOVING.
      values.put(COLUMN_FAILURE_REASON, Download.FAILURE_REASON_NONE);
      SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
      writableDatabase.update(tableName, values, /* whereClause= */ null, /* whereArgs= */ null);
    } catch (SQLException e) {
      throw new DatabaseIOException(e);
    }
  }

  @Override
  public void setStopReason(int stopReason) throws DatabaseIOException {
    ensureInitialized();
    try {
      ContentValues values = new ContentValues();
      values.put(COLUMN_STOP_REASON, stopReason);
      SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
      writableDatabase.update(tableName, values, WHERE_STATE_IS_TERMINAL, /* whereArgs= */ null);
    } catch (SQLException e) {
      throw new DatabaseIOException(e);
    }
  }

  @Override
  public void setStopReason(String id, int stopReason) throws DatabaseIOException {
    ensureInitialized();
    try {
      ContentValues values = new ContentValues();
      values.put(COLUMN_STOP_REASON, stopReason);
      SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
      writableDatabase.update(
          tableName,
          values,
          WHERE_STATE_IS_TERMINAL + " AND " + WHERE_ID_EQUALS,
          new String[] {id});
    } catch (SQLException e) {
      throw new DatabaseIOException(e);
    }
  }

  private void ensureInitialized() throws DatabaseIOException {
    synchronized (initializationLock) {
      if (initialized) {
        return;
      }
      try {
        SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase();
        int version = VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_OFFLINE, name);
        if (version != TABLE_VERSION) {
          SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
          writableDatabase.beginTransactionNonExclusive();
          try {
            VersionTable.setVersion(
                writableDatabase, VersionTable.FEATURE_OFFLINE, name, TABLE_VERSION);
            List<Download> upgradedDownloads =
                version == 2 ? loadDownloadsFromVersion2(writableDatabase) : new ArrayList<>();
            writableDatabase.execSQL("DROP TABLE IF EXISTS " + tableName);
            writableDatabase.execSQL("CREATE TABLE " + tableName + " " + TABLE_SCHEMA);
            for (Download download : upgradedDownloads) {
              putDownloadInternal(download, writableDatabase);
            }
            writableDatabase.setTransactionSuccessful();
          } finally {
            writableDatabase.endTransaction();
          }
        }
        initialized = true;
      } catch (SQLException e) {
        throw new DatabaseIOException(e);
      }
    }
  }

  private void putDownloadInternal(Download download, SQLiteDatabase database) {
    byte[] keySetId =
        download.request.keySetId == null ? Util.EMPTY_BYTE_ARRAY : download.request.keySetId;
    ContentValues values = new ContentValues();
    values.put(COLUMN_ID, download.request.id);
    values.put(COLUMN_MIME_TYPE, download.request.mimeType);
    values.put(COLUMN_URI, download.request.uri.toString());
    values.put(COLUMN_STREAM_KEYS, encodeStreamKeys(download.request.streamKeys));
    values.put(COLUMN_CUSTOM_CACHE_KEY, download.request.customCacheKey);
    values.put(COLUMN_DATA, download.request.data);
    values.put(COLUMN_STATE, download.state);
    values.put(COLUMN_START_TIME_MS, download.startTimeMs);
    values.put(COLUMN_UPDATE_TIME_MS, download.updateTimeMs);
    values.put(COLUMN_CONTENT_LENGTH, download.contentLength);
    values.put(COLUMN_STOP_REASON, download.stopReason);
    values.put(COLUMN_FAILURE_REASON, download.failureReason);
    values.put(COLUMN_PERCENT_DOWNLOADED, download.getPercentDownloaded());
    values.put(COLUMN_BYTES_DOWNLOADED, download.getBytesDownloaded());
    values.put(COLUMN_KEY_SET_ID, keySetId);
    database.replaceOrThrow(tableName, /* nullColumnHack= */ null, values);
  }

  private List<Download> loadDownloadsFromVersion2(SQLiteDatabase database) {
    List<Download> downloads = new ArrayList<>();
    if (!Util.tableExists(database, tableName)) {
      return downloads;
    }

    String[] columnsV2 =
        new String[] {
          "id",
          "title",
          "uri",
          "stream_keys",
          "custom_cache_key",
          "data",
          "state",
          "start_time_ms",
          "update_time_ms",
          "content_length",
          "stop_reason",
          "failure_reason",
          "percent_downloaded",
          "bytes_downloaded"
        };
    try (Cursor cursor =
        database.query(
            tableName,
            columnsV2,
            /* selection= */ null,
            /* selectionArgs= */ null,
            /* groupBy= */ null,
            /* having= */ null,
            /* orderBy= */ null); ) {
      while (cursor.moveToNext()) {
        downloads.add(getDownloadForCurrentRowV2(cursor));
      }
      return downloads;
    }
  }

  /** Infers the MIME type from a v2 table row. */
  private static String inferMimeType(@Nullable String downloadType) {
    if ("dash".equals(downloadType)) {
      return MimeTypes.APPLICATION_MPD;
    } else if ("hls".equals(downloadType)) {
      return MimeTypes.APPLICATION_M3U8;
    } else if ("ss".equals(downloadType)) {
      return MimeTypes.APPLICATION_SS;
    } else {
      return MimeTypes.VIDEO_UNKNOWN;
    }
  }

  private Cursor getCursor(String selection, @Nullable String[] selectionArgs)
      throws DatabaseIOException {
    try {
      String sortOrder = COLUMN_START_TIME_MS + " ASC";
      return databaseProvider
          .getReadableDatabase()
          .query(
              tableName,
              COLUMNS,
              selection,
              selectionArgs,
              /* groupBy= */ null,
              /* having= */ null,
              sortOrder);
    } catch (SQLiteException e) {
      throw new DatabaseIOException(e);
    }
  }

  @VisibleForTesting
  /* package */ static String encodeStreamKeys(List<StreamKey> streamKeys) {
    StringBuilder stringBuilder = new StringBuilder();
    for (int i = 0; i < streamKeys.size(); i++) {
      StreamKey streamKey = streamKeys.get(i);
      stringBuilder
          .append(streamKey.periodIndex)
          .append('.')
          .append(streamKey.groupIndex)
          .append('.')
          .append(streamKey.streamIndex)
          .append(',');
    }
    if (stringBuilder.length() > 0) {
      stringBuilder.setLength(stringBuilder.length() - 1);
    }
    return stringBuilder.toString();
  }

  private static String getStateQuery(@Download.State int... states) {
    if (states.length == 0) {
      return TRUE;
    }
    StringBuilder selectionBuilder = new StringBuilder();
    selectionBuilder.append(COLUMN_STATE).append(" IN (");
    for (int i = 0; i < states.length; i++) {
      if (i > 0) {
        selectionBuilder.append(',');
      }
      selectionBuilder.append(states[i]);
    }
    selectionBuilder.append(')');
    return selectionBuilder.toString();
  }

  private static Download getDownloadForCurrentRow(Cursor cursor) {
    byte[] keySetId = cursor.getBlob(COLUMN_INDEX_KEY_SET_ID);
    DownloadRequest request =
        new DownloadRequest.Builder(
                /* id= */ checkNotNull(cursor.getString(COLUMN_INDEX_ID)),
                /* uri= */ Uri.parse(checkNotNull(cursor.getString(COLUMN_INDEX_URI))))
            .setMimeType(cursor.getString(COLUMN_INDEX_MIME_TYPE))
            .setStreamKeys(decodeStreamKeys(cursor.getString(COLUMN_INDEX_STREAM_KEYS)))
            .setKeySetId(keySetId.length > 0 ? keySetId : null)
            .setCustomCacheKey(cursor.getString(COLUMN_INDEX_CUSTOM_CACHE_KEY))
            .setData(cursor.getBlob(COLUMN_INDEX_DATA))
            .build();
    DownloadProgress downloadProgress = new DownloadProgress();
    downloadProgress.bytesDownloaded = cursor.getLong(COLUMN_INDEX_BYTES_DOWNLOADED);
    downloadProgress.percentDownloaded = cursor.getFloat(COLUMN_INDEX_PERCENT_DOWNLOADED);
    @State int state = cursor.getInt(COLUMN_INDEX_STATE);
    // It's possible the database contains failure reasons for non-failed downloads, which is
    // invalid. Clear them here. See https://github.com/google/ExoPlayer/issues/6785.
    @FailureReason
    int failureReason =
        state == Download.STATE_FAILED
            ? cursor.getInt(COLUMN_INDEX_FAILURE_REASON)
            : Download.FAILURE_REASON_NONE;
    return new Download(
        request,
        state,
        /* startTimeMs= */ cursor.getLong(COLUMN_INDEX_START_TIME_MS),
        /* updateTimeMs= */ cursor.getLong(COLUMN_INDEX_UPDATE_TIME_MS),
        /* contentLength= */ cursor.getLong(COLUMN_INDEX_CONTENT_LENGTH),
        /* stopReason= */ cursor.getInt(COLUMN_INDEX_STOP_REASON),
        failureReason,
        downloadProgress);
  }

  /** Read a {@link Download} from a table row of version 2. */
  private static Download getDownloadForCurrentRowV2(Cursor cursor) {
    /*
     * Version 2 schema
     * Index  Column              Type
     * 0      id                  string
     * 1      type                string
     * 2      uri                 string
     * 3      stream_keys         string
     * 4      custom_cache_key    string
     * 5      data                blob
     * 6      state               integer
     * 7      start_time_ms       integer
     * 8      update_time_ms      integer
     * 9      content_length      integer
     * 10     stop_reason         integer
     * 11     failure_reason      integer
     * 12     percent_downloaded  real
     * 13     bytes_downloaded    integer
     */
    DownloadRequest request =
        new DownloadRequest.Builder(
                /* id= */ checkNotNull(cursor.getString(0)),
                /* uri= */ Uri.parse(checkNotNull(cursor.getString(2))))
            .setMimeType(inferMimeType(cursor.getString(1)))
            .setStreamKeys(decodeStreamKeys(cursor.getString(3)))
            .setCustomCacheKey(cursor.getString(4))
            .setData(cursor.getBlob(5))
            .build();
    DownloadProgress downloadProgress = new DownloadProgress();
    downloadProgress.bytesDownloaded = cursor.getLong(13);
    downloadProgress.percentDownloaded = cursor.getFloat(12);
    @State int state = cursor.getInt(6);
    // It's possible the database contains failure reasons for non-failed downloads, which is
    // invalid. Clear them here. See https://github.com/google/ExoPlayer/issues/6785.
    @FailureReason
    int failureReason =
        state == Download.STATE_FAILED ? cursor.getInt(11) : Download.FAILURE_REASON_NONE;
    return new Download(
        request,
        state,
        /* startTimeMs= */ cursor.getLong(7),
        /* updateTimeMs= */ cursor.getLong(8),
        /* contentLength= */ cursor.getLong(9),
        /* stopReason= */ cursor.getInt(10),
        failureReason,
        downloadProgress);
  }

  private static List<StreamKey> decodeStreamKeys(@Nullable String encodedStreamKeys) {
    ArrayList<StreamKey> streamKeys = new ArrayList<>();
    if (TextUtils.isEmpty(encodedStreamKeys)) {
      return streamKeys;
    }
    String[] streamKeysStrings = Util.split(encodedStreamKeys, ",");
    for (String streamKeysString : streamKeysStrings) {
      String[] indices = Util.split(streamKeysString, "\\.");
      Assertions.checkState(indices.length == 3);
      streamKeys.add(
          new StreamKey(
              Integer.parseInt(indices[0]),
              Integer.parseInt(indices[1]),
              Integer.parseInt(indices[2])));
    }
    return streamKeys;
  }

  private static final class DownloadCursorImpl implements DownloadCursor {

    private final Cursor cursor;

    private DownloadCursorImpl(Cursor cursor) {
      this.cursor = cursor;
    }

    @Override
    public Download getDownload() {
      return getDownloadForCurrentRow(cursor);
    }

    @Override
    public int getCount() {
      return cursor.getCount();
    }

    @Override
    public int getPosition() {
      return cursor.getPosition();
    }

    @Override
    public boolean moveToPosition(int position) {
      return cursor.moveToPosition(position);
    }

    @Override
    public void close() {
      cursor.close();
    }

    @Override
    public boolean isClosed() {
      return cursor.isClosed();
    }
  }
}