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         mRowIdColumnIndex = DatabaseUtils.findRowIdColumnIndex(mColumns);
    109     }
    110 
    111     /**
    112      * Get the database that this cursor is associated with.
    113      * @return the SQLiteDatabase that this cursor is associated with.
    114      */
    115     public SQLiteDatabase getDatabase() {
    116         return mQuery.getDatabase();
    117     }
    118 
    119     @Override
    120     public boolean onMove(int oldPosition, int newPosition) {
    121         // Make sure the row at newPosition is present in the window
    122         if (mWindow == null || newPosition < mWindow.getStartPosition() ||
    123                 newPosition >= (mWindow.getStartPosition() + mWindow.getNumRows())) {
    124             fillWindow(newPosition);
    125         }
    126 
    127         return true;
    128     }
    129 
    130     @Override
    131     public int getCount() {
    132         if (mCount == NO_COUNT) {
    133             fillWindow(0);
    134         }
    135         return mCount;
    136     }
    137 
    138     private void fillWindow(int requiredPos) {
    139         clearOrCreateWindow(getDatabase().getPath());
    140 
    141         try {
    142             if (mCount == NO_COUNT) {
    143                 int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, 0);
    144                 mCount = mQuery.fillWindow(mWindow, startPos, requiredPos, true);
    145                 mCursorWindowCapacity = mWindow.getNumRows();
    146                 if (Log.isLoggable(TAG, Log.DEBUG)) {
    147                     Log.d(TAG, "received count(*) from native_fill_window: " + mCount);
    148                 }
    149             } else {
    150                 int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos,
    151                         mCursorWindowCapacity);
    152                 mQuery.fillWindow(mWindow, startPos, requiredPos, false);
    153             }
    154         } catch (RuntimeException ex) {
    155             // Close the cursor window if the query failed and therefore will
    156             // not produce any results.  This helps to avoid accidentally leaking
    157             // the cursor window if the client does not correctly handle exceptions
    158             // and fails to close the cursor.
    159             closeWindow();
    160             throw ex;
    161         }
    162     }
    163 
    164     @Override
    165     public int getColumnIndex(String columnName) {
    166         // Create mColumnNameMap on demand
    167         if (mColumnNameMap == null) {
    168             String[] columns = mColumns;
    169             int columnCount = columns.length;
    170             HashMap<String, Integer> map = new HashMap<String, Integer>(columnCount, 1);
    171             for (int i = 0; i < columnCount; i++) {
    172                 map.put(columns[i], i);
    173             }
    174             mColumnNameMap = map;
    175         }
    176 
    177         // Hack according to bug 903852
    178         final int periodIndex = columnName.lastIndexOf('.');
    179         if (periodIndex != -1) {
    180             Exception e = new Exception();
    181             Log.e(TAG, "requesting column name with table name -- " + columnName, e);
    182             columnName = columnName.substring(periodIndex + 1);
    183         }
    184 
    185         Integer i = mColumnNameMap.get(columnName);
    186         if (i != null) {
    187             return i.intValue();
    188         } else {
    189             return -1;
    190         }
    191     }
    192 
    193     @Override
    194     public String[] getColumnNames() {
    195         return mColumns;
    196     }
    197 
    198     @Override
    199     public void deactivate() {
    200         super.deactivate();
    201         mDriver.cursorDeactivated();
    202     }
    203 
    204     @Override
    205     public void close() {
    206         super.close();
    207         synchronized (this) {
    208             mQuery.close();
    209             mDriver.cursorClosed();
    210         }
    211     }
    212 
    213     @Override
    214     public boolean requery() {
    215         if (isClosed()) {
    216             return false;
    217         }
    218 
    219         synchronized (this) {
    220             if (!mQuery.getDatabase().isOpen()) {
    221                 return false;
    222             }
    223 
    224             if (mWindow != null) {
    225                 mWindow.clear();
    226             }
    227             mPos = -1;
    228             mCount = NO_COUNT;
    229 
    230             mDriver.cursorRequeried(this);
    231         }
    232 
    233         try {
    234             return super.requery();
    235         } catch (IllegalStateException e) {
    236             // for backwards compatibility, just return false
    237             Log.w(TAG, "requery() failed " + e.getMessage(), e);
    238             return false;
    239         }
    240     }
    241 
    242     @Override
    243     public void setWindow(CursorWindow window) {
    244         super.setWindow(window);
    245         mCount = NO_COUNT;
    246     }
    247 
    248     /**
    249      * Changes the selection arguments. The new values take effect after a call to requery().
    250      */
    251     public void setSelectionArguments(String[] selectionArgs) {
    252         mDriver.setBindArguments(selectionArgs);
    253     }
    254 
    255     /**
    256      * Release the native resources, if they haven't been released yet.
    257      */
    258     @Override
    259     protected void finalize() {
    260         try {
    261             // if the cursor hasn't been closed yet, close it first
    262             if (mWindow != null) {
    263                 if (mStackTrace != null) {
    264                     String sql = mQuery.getSql();
    265                     int len = sql.length();
    266                     StrictMode.onSqliteObjectLeaked(
    267                         "Finalizing a Cursor that has not been deactivated or closed. " +
    268                         "database = " + mQuery.getDatabase().getLabel() +
    269                         ", table = " + mEditTable +
    270                         ", query = " + sql.substring(0, (len > 1000) ? 1000 : len),
    271                         mStackTrace);
    272                 }
    273                 close();
    274             }
    275         } finally {
    276             super.finalize();
    277         }
    278     }
    279 }
    280