Home | History | Annotate | Download | only in sqlite
      1 /*
      2  * Copyright (C) 2006 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.database.sqlite;
     18 
     19 import android.database.AbstractWindowedCursor;
     20 import android.database.CursorWindow;
     21 import android.database.DatabaseUtils;
     22 import android.os.StrictMode;
     23 import android.util.Log;
     24 
     25 import java.util.HashMap;
     26 import java.util.Map;
     27 
     28 /**
     29  * A Cursor implementation that exposes results from a query on a
     30  * {@link SQLiteDatabase}.
     31  *
     32  * SQLiteCursor is not internally synchronized so code using a SQLiteCursor from multiple
     33  * threads should perform its own synchronization when using the SQLiteCursor.
     34  */
     35 public class SQLiteCursor extends AbstractWindowedCursor {
     36     static final String TAG = "SQLiteCursor";
     37     static final int NO_COUNT = -1;
     38 
     39     /** The name of the table to edit */
     40     private final String mEditTable;
     41 
     42     /** The names of the columns in the rows */
     43     private final String[] mColumns;
     44 
     45     /** The query object for the cursor */
     46     private final SQLiteQuery mQuery;
     47 
     48     /** The compiled query this cursor came from */
     49     private final SQLiteCursorDriver mDriver;
     50 
     51     /** The number of rows in the cursor */
     52     private int mCount = NO_COUNT;
     53 
     54     /** The number of rows that can fit in the cursor window, 0 if unknown */
     55     private int mCursorWindowCapacity;
     56 
     57     /** A mapping of column names to column indices, to speed up lookups */
     58     private Map<String, Integer> mColumnNameMap;
     59 
     60     /** Used to find out where a cursor was allocated in case it never got released. */
     61     private final Throwable mStackTrace;
     62 
     63     /**
     64      * Execute a query and provide access to its result set through a Cursor
     65      * interface. For a query such as: {@code SELECT name, birth, phone FROM
     66      * myTable WHERE ... LIMIT 1,20 ORDER BY...} the column names (name, birth,
     67      * phone) would be in the projection argument and everything from
     68      * {@code FROM} onward would be in the params argument.
     69      *
     70      * @param db a reference to a Database object that is already constructed
     71      *     and opened. This param is not used any longer
     72      * @param editTable the name of the table used for this query
     73      * @param query the rest of the query terms
     74      *     cursor is finalized
     75      * @deprecated use {@link #SQLiteCursor(SQLiteCursorDriver, String, SQLiteQuery)} instead
     76      */
     77     @Deprecated
     78     public SQLiteCursor(SQLiteDatabase db, SQLiteCursorDriver driver,
     79             String editTable, SQLiteQuery query) {
     80         this(driver, editTable, query);
     81     }
     82 
     83     /**
     84      * Execute a query and provide access to its result set through a Cursor
     85      * interface. For a query such as: {@code SELECT name, birth, phone FROM
     86      * myTable WHERE ... LIMIT 1,20 ORDER BY...} the column names (name, birth,
     87      * phone) would be in the projection argument and everything from
     88      * {@code FROM} onward would be in the params argument.
     89      *
     90      * @param editTable the name of the table used for this query
     91      * @param query the {@link SQLiteQuery} object associated with this cursor object.
     92      */
     93     public SQLiteCursor(SQLiteCursorDriver driver, String editTable, SQLiteQuery query) {
     94         if (query == null) {
     95             throw new IllegalArgumentException("query object cannot be null");
     96         }
     97         if (StrictMode.vmSqliteObjectLeaksEnabled()) {
     98             mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace();
     99         } else {
    100             mStackTrace = null;
    101         }
    102         mDriver = driver;
    103         mEditTable = editTable;
    104         mColumnNameMap = null;
    105         mQuery = query;
    106 
    107         mColumns = query.getColumnNames();
    108     }
    109 
    110     /**
    111      * Get the database that this cursor is associated with.
    112      * @return the SQLiteDatabase that this cursor is associated with.
    113      */
    114     public SQLiteDatabase getDatabase() {
    115         return mQuery.getDatabase();
    116     }
    117 
    118     @Override
    119     public boolean onMove(int oldPosition, int newPosition) {
    120         // Make sure the row at newPosition is present in the window
    121         if (mWindow == null || newPosition < mWindow.getStartPosition() ||
    122                 newPosition >= (mWindow.getStartPosition() + mWindow.getNumRows())) {
    123             fillWindow(newPosition);
    124         }
    125 
    126         return true;
    127     }
    128 
    129     @Override
    130     public int getCount() {
    131         if (mCount == NO_COUNT) {
    132             fillWindow(0);
    133         }
    134         return mCount;
    135     }
    136 
    137     private void fillWindow(int requiredPos) {
    138         clearOrCreateWindow(getDatabase().getPath());
    139 
    140         try {
    141             if (mCount == NO_COUNT) {
    142                 int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, 0);
    143                 mCount = mQuery.fillWindow(mWindow, startPos, requiredPos, true);
    144                 mCursorWindowCapacity = mWindow.getNumRows();
    145                 if (Log.isLoggable(TAG, Log.DEBUG)) {
    146                     Log.d(TAG, "received count(*) from native_fill_window: " + mCount);
    147                 }
    148             } else {
    149                 int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos,
    150                         mCursorWindowCapacity);
    151                 mQuery.fillWindow(mWindow, startPos, requiredPos, false);
    152             }
    153         } catch (RuntimeException ex) {
    154             // Close the cursor window if the query failed and therefore will
    155             // not produce any results.  This helps to avoid accidentally leaking
    156             // the cursor window if the client does not correctly handle exceptions
    157             // and fails to close the cursor.
    158             closeWindow();
    159             throw ex;
    160         }
    161     }
    162 
    163     @Override
    164     public int getColumnIndex(String columnName) {
    165         // Create mColumnNameMap on demand
    166         if (mColumnNameMap == null) {
    167             String[] columns = mColumns;
    168             int columnCount = columns.length;
    169             HashMap<String, Integer> map = new HashMap<String, Integer>(columnCount, 1);
    170             for (int i = 0; i < columnCount; i++) {
    171                 map.put(columns[i], i);
    172             }
    173             mColumnNameMap = map;
    174         }
    175 
    176         // Hack according to bug 903852
    177         final int periodIndex = columnName.lastIndexOf('.');
    178         if (periodIndex != -1) {
    179             Exception e = new Exception();
    180             Log.e(TAG, "requesting column name with table name -- " + columnName, e);
    181             columnName = columnName.substring(periodIndex + 1);
    182         }
    183 
    184         Integer i = mColumnNameMap.get(columnName);
    185         if (i != null) {
    186             return i.intValue();
    187         } else {
    188             return -1;
    189         }
    190     }
    191 
    192     @Override
    193     public String[] getColumnNames() {
    194         return mColumns;
    195     }
    196 
    197     @Override
    198     public void deactivate() {
    199         super.deactivate();
    200         mDriver.cursorDeactivated();
    201     }
    202 
    203     @Override
    204     public void close() {
    205         super.close();
    206         synchronized (this) {
    207             mQuery.close();
    208             mDriver.cursorClosed();
    209         }
    210     }
    211 
    212     @Override
    213     public boolean requery() {
    214         if (isClosed()) {
    215             return false;
    216         }
    217 
    218         synchronized (this) {
    219             if (!mQuery.getDatabase().isOpen()) {
    220                 return false;
    221             }
    222 
    223             if (mWindow != null) {
    224                 mWindow.clear();
    225             }
    226             mPos = -1;
    227             mCount = NO_COUNT;
    228 
    229             mDriver.cursorRequeried(this);
    230         }
    231 
    232         try {
    233             return super.requery();
    234         } catch (IllegalStateException e) {
    235             // for backwards compatibility, just return false
    236             Log.w(TAG, "requery() failed " + e.getMessage(), e);
    237             return false;
    238         }
    239     }
    240 
    241     @Override
    242     public void setWindow(CursorWindow window) {
    243         super.setWindow(window);
    244         mCount = NO_COUNT;
    245     }
    246 
    247     /**
    248      * Changes the selection arguments. The new values take effect after a call to requery().
    249      */
    250     public void setSelectionArguments(String[] selectionArgs) {
    251         mDriver.setBindArguments(selectionArgs);
    252     }
    253 
    254     /**
    255      * Release the native resources, if they haven't been released yet.
    256      */
    257     @Override
    258     protected void finalize() {
    259         try {
    260             // if the cursor hasn't been closed yet, close it first
    261             if (mWindow != null) {
    262                 if (mStackTrace != null) {
    263                     String sql = mQuery.getSql();
    264                     int len = sql.length();
    265                     StrictMode.onSqliteObjectLeaked(
    266                         "Finalizing a Cursor that has not been deactivated or closed. " +
    267                         "database = " + mQuery.getDatabase().getLabel() +
    268                         ", table = " + mEditTable +
    269                         ", query = " + sql.substring(0, (len > 1000) ? 1000 : len),
    270                         mStackTrace);
    271                 }
    272                 close();
    273             }
    274         } finally {
    275             super.finalize();
    276         }
    277     }
    278 }
    279