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