• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.DataSetObserver;
22 import android.database.SQLException;
23 
24 import android.os.Handler;
25 import android.os.Message;
26 import android.os.Process;
27 import android.os.StrictMode;
28 import android.text.TextUtils;
29 import android.util.Config;
30 import android.util.Log;
31 
32 import java.util.HashMap;
33 import java.util.Iterator;
34 import java.util.Map;
35 import java.util.concurrent.locks.ReentrantLock;
36 
37 /**
38  * A Cursor implementation that exposes results from a query on a
39  * {@link SQLiteDatabase}.
40  *
41  * SQLiteCursor is not internally synchronized so code using a SQLiteCursor from multiple
42  * threads should perform its own synchronization when using the SQLiteCursor.
43  */
44 public class SQLiteCursor extends AbstractWindowedCursor {
45     static final String TAG = "Cursor";
46     static final int NO_COUNT = -1;
47 
48     /** The name of the table to edit */
49     private String mEditTable;
50 
51     /** The names of the columns in the rows */
52     private String[] mColumns;
53 
54     /** The query object for the cursor */
55     private SQLiteQuery mQuery;
56 
57     /** The database the cursor was created from */
58     private SQLiteDatabase mDatabase;
59 
60     /** The compiled query this cursor came from */
61     private SQLiteCursorDriver mDriver;
62 
63     /** The number of rows in the cursor */
64     private int mCount = NO_COUNT;
65 
66     /** A mapping of column names to column indices, to speed up lookups */
67     private Map<String, Integer> mColumnNameMap;
68 
69     /** Used to find out where a cursor was allocated in case it never got released. */
70     private Throwable mStackTrace;
71 
72     /**
73      *  mMaxRead is the max items that each cursor window reads
74      *  default to a very high value
75      */
76     private int mMaxRead = Integer.MAX_VALUE;
77     private int mInitialRead = Integer.MAX_VALUE;
78     private int mCursorState = 0;
79     private ReentrantLock mLock = null;
80     private boolean mPendingData = false;
81 
82     /**
83      *  support for a cursor variant that doesn't always read all results
84      *  initialRead is the initial number of items that cursor window reads
85      *  if query contains more than this number of items, a thread will be
86      *  created and handle the left over items so that caller can show
87      *  results as soon as possible
88      * @param initialRead initial number of items that cursor read
89      * @param maxRead leftover items read at maxRead items per time
90      * @hide
91      */
setLoadStyle(int initialRead, int maxRead)92     public void setLoadStyle(int initialRead, int maxRead) {
93         mMaxRead = maxRead;
94         mInitialRead = initialRead;
95         mLock = new ReentrantLock(true);
96     }
97 
queryThreadLock()98     private void queryThreadLock() {
99         if (mLock != null) {
100             mLock.lock();
101         }
102     }
103 
queryThreadUnlock()104     private void queryThreadUnlock() {
105         if (mLock != null) {
106             mLock.unlock();
107         }
108     }
109 
110 
111     /**
112      * @hide
113      */
114     final private class QueryThread implements Runnable {
115         private final int mThreadState;
QueryThread(int version)116         QueryThread(int version) {
117             mThreadState = version;
118         }
sendMessage()119         private void sendMessage() {
120             if (mNotificationHandler != null) {
121                 mNotificationHandler.sendEmptyMessage(1);
122                 mPendingData = false;
123             } else {
124                 mPendingData = true;
125             }
126 
127         }
run()128         public void run() {
129              // use cached mWindow, to avoid get null mWindow
130             CursorWindow cw = mWindow;
131             Process.setThreadPriority(Process.myTid(), Process.THREAD_PRIORITY_BACKGROUND);
132             // the cursor's state doesn't change
133             while (true) {
134                 mLock.lock();
135                 if (mCursorState != mThreadState) {
136                     mLock.unlock();
137                     break;
138                 }
139                 try {
140                     int count = mQuery.fillWindow(cw, mMaxRead, mCount);
141                     // return -1 means not finished
142                     if (count != 0) {
143                         if (count == NO_COUNT){
144                             mCount += mMaxRead;
145                             sendMessage();
146                         } else {
147                             mCount = count;
148                             sendMessage();
149                             break;
150                         }
151                     } else {
152                         break;
153                     }
154                 } catch (Exception e) {
155                     // end the tread when the cursor is close
156                     break;
157                 } finally {
158                     mLock.unlock();
159                 }
160             }
161         }
162     }
163 
164     /**
165      * @hide
166      */
167     protected class MainThreadNotificationHandler extends Handler {
handleMessage(Message msg)168         public void handleMessage(Message msg) {
169             notifyDataSetChange();
170         }
171     }
172 
173     /**
174      * @hide
175      */
176     protected MainThreadNotificationHandler mNotificationHandler;
177 
registerDataSetObserver(DataSetObserver observer)178     public void registerDataSetObserver(DataSetObserver observer) {
179         super.registerDataSetObserver(observer);
180         if ((Integer.MAX_VALUE != mMaxRead || Integer.MAX_VALUE != mInitialRead) &&
181                 mNotificationHandler == null) {
182             queryThreadLock();
183             try {
184                 mNotificationHandler = new MainThreadNotificationHandler();
185                 if (mPendingData) {
186                     notifyDataSetChange();
187                     mPendingData = false;
188                 }
189             } finally {
190                 queryThreadUnlock();
191             }
192         }
193 
194     }
195 
196     /**
197      * Execute a query and provide access to its result set through a Cursor
198      * interface. For a query such as: {@code SELECT name, birth, phone FROM
199      * myTable WHERE ... LIMIT 1,20 ORDER BY...} the column names (name, birth,
200      * phone) would be in the projection argument and everything from
201      * {@code FROM} onward would be in the params argument. This constructor
202      * has package scope.
203      *
204      * @param db a reference to a Database object that is already constructed
205      *     and opened
206      * @param editTable the name of the table used for this query
207      * @param query the rest of the query terms
208      *     cursor is finalized
209      */
SQLiteCursor(SQLiteDatabase db, SQLiteCursorDriver driver, String editTable, SQLiteQuery query)210     public SQLiteCursor(SQLiteDatabase db, SQLiteCursorDriver driver,
211             String editTable, SQLiteQuery query) {
212         // The AbstractCursor constructor needs to do some setup.
213         super();
214         mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace();
215         mDatabase = db;
216         mDriver = driver;
217         mEditTable = editTable;
218         mColumnNameMap = null;
219         mQuery = query;
220 
221         try {
222             db.lock();
223 
224             // Setup the list of columns
225             int columnCount = mQuery.columnCountLocked();
226             mColumns = new String[columnCount];
227 
228             // Read in all column names
229             for (int i = 0; i < columnCount; i++) {
230                 String columnName = mQuery.columnNameLocked(i);
231                 mColumns[i] = columnName;
232                 if (Config.LOGV) {
233                     Log.v("DatabaseWindow", "mColumns[" + i + "] is "
234                             + mColumns[i]);
235                 }
236 
237                 // Make note of the row ID column index for quick access to it
238                 if ("_id".equals(columnName)) {
239                     mRowIdColumnIndex = i;
240                 }
241             }
242         } finally {
243             db.unlock();
244         }
245     }
246 
247     /**
248      * @return the SQLiteDatabase that this cursor is associated with.
249      */
getDatabase()250     public SQLiteDatabase getDatabase() {
251         return mDatabase;
252     }
253 
254     @Override
onMove(int oldPosition, int newPosition)255     public boolean onMove(int oldPosition, int newPosition) {
256         // Make sure the row at newPosition is present in the window
257         if (mWindow == null || newPosition < mWindow.getStartPosition() ||
258                 newPosition >= (mWindow.getStartPosition() + mWindow.getNumRows())) {
259             fillWindow(newPosition);
260         }
261 
262         return true;
263     }
264 
265     @Override
getCount()266     public int getCount() {
267         if (mCount == NO_COUNT) {
268             fillWindow(0);
269         }
270         return mCount;
271     }
272 
fillWindow(int startPos)273     private void fillWindow (int startPos) {
274         if (mWindow == null) {
275             // If there isn't a window set already it will only be accessed locally
276             mWindow = new CursorWindow(true /* the window is local only */);
277         } else {
278             mCursorState++;
279                 queryThreadLock();
280                 try {
281                     mWindow.clear();
282                 } finally {
283                     queryThreadUnlock();
284                 }
285         }
286         mWindow.setStartPosition(startPos);
287         mCount = mQuery.fillWindow(mWindow, mInitialRead, 0);
288         // return -1 means not finished
289         if (mCount == NO_COUNT){
290             mCount = startPos + mInitialRead;
291             Thread t = new Thread(new QueryThread(mCursorState), "query thread");
292             t.start();
293         }
294     }
295 
296     @Override
getColumnIndex(String columnName)297     public int getColumnIndex(String columnName) {
298         // Create mColumnNameMap on demand
299         if (mColumnNameMap == null) {
300             String[] columns = mColumns;
301             int columnCount = columns.length;
302             HashMap<String, Integer> map = new HashMap<String, Integer>(columnCount, 1);
303             for (int i = 0; i < columnCount; i++) {
304                 map.put(columns[i], i);
305             }
306             mColumnNameMap = map;
307         }
308 
309         // Hack according to bug 903852
310         final int periodIndex = columnName.lastIndexOf('.');
311         if (periodIndex != -1) {
312             Exception e = new Exception();
313             Log.e(TAG, "requesting column name with table name -- " + columnName, e);
314             columnName = columnName.substring(periodIndex + 1);
315         }
316 
317         Integer i = mColumnNameMap.get(columnName);
318         if (i != null) {
319             return i.intValue();
320         } else {
321             return -1;
322         }
323     }
324 
325     /**
326      * @hide
327      * @deprecated
328      */
329     @Override
deleteRow()330     public boolean deleteRow() {
331         checkPosition();
332 
333         // Only allow deletes if there is an ID column, and the ID has been read from it
334         if (mRowIdColumnIndex == -1 || mCurrentRowID == null) {
335             Log.e(TAG,
336                     "Could not delete row because either the row ID column is not available or it" +
337                     "has not been read.");
338             return false;
339         }
340 
341         boolean success;
342 
343         /*
344          * Ensure we don't change the state of the database when another
345          * thread is holding the database lock. requery() and moveTo() are also
346          * synchronized here to make sure they get the state of the database
347          * immediately following the DELETE.
348          */
349         mDatabase.lock();
350         try {
351             try {
352                 mDatabase.delete(mEditTable, mColumns[mRowIdColumnIndex] + "=?",
353                         new String[] {mCurrentRowID.toString()});
354                 success = true;
355             } catch (SQLException e) {
356                 success = false;
357             }
358 
359             int pos = mPos;
360             requery();
361 
362             /*
363              * Ensure proper cursor state. Note that mCurrentRowID changes
364              * in this call.
365              */
366             moveToPosition(pos);
367         } finally {
368             mDatabase.unlock();
369         }
370 
371         if (success) {
372             onChange(true);
373             return true;
374         } else {
375             return false;
376         }
377     }
378 
379     @Override
getColumnNames()380     public String[] getColumnNames() {
381         return mColumns;
382     }
383 
384     /**
385      * @hide
386      * @deprecated
387      */
388     @Override
supportsUpdates()389     public boolean supportsUpdates() {
390         return super.supportsUpdates() && !TextUtils.isEmpty(mEditTable);
391     }
392 
393     /**
394      * @hide
395      * @deprecated
396      */
397     @Override
commitUpdates(Map<? extends Long, ? extends Map<String, Object>> additionalValues)398     public boolean commitUpdates(Map<? extends Long,
399             ? extends Map<String, Object>> additionalValues) {
400         if (!supportsUpdates()) {
401             Log.e(TAG, "commitUpdates not supported on this cursor, did you "
402                     + "include the _id column?");
403             return false;
404         }
405 
406         /*
407          * Prevent other threads from changing the updated rows while they're
408          * being processed here.
409          */
410         synchronized (mUpdatedRows) {
411             if (additionalValues != null) {
412                 mUpdatedRows.putAll(additionalValues);
413             }
414 
415             if (mUpdatedRows.size() == 0) {
416                 return true;
417             }
418 
419             /*
420              * Prevent other threads from changing the database state while
421              * we process the updated rows, and prevents us from changing the
422              * database behind the back of another thread.
423              */
424             mDatabase.beginTransaction();
425             try {
426                 StringBuilder sql = new StringBuilder(128);
427 
428                 // For each row that has been updated
429                 for (Map.Entry<Long, Map<String, Object>> rowEntry :
430                         mUpdatedRows.entrySet()) {
431                     Map<String, Object> values = rowEntry.getValue();
432                     Long rowIdObj = rowEntry.getKey();
433 
434                     if (rowIdObj == null || values == null) {
435                         throw new IllegalStateException("null rowId or values found! rowId = "
436                                 + rowIdObj + ", values = " + values);
437                     }
438 
439                     if (values.size() == 0) {
440                         continue;
441                     }
442 
443                     long rowId = rowIdObj.longValue();
444 
445                     Iterator<Map.Entry<String, Object>> valuesIter =
446                             values.entrySet().iterator();
447 
448                     sql.setLength(0);
449                     sql.append("UPDATE " + mEditTable + " SET ");
450 
451                     // For each column value that has been updated
452                     Object[] bindings = new Object[values.size()];
453                     int i = 0;
454                     while (valuesIter.hasNext()) {
455                         Map.Entry<String, Object> entry = valuesIter.next();
456                         sql.append(entry.getKey());
457                         sql.append("=?");
458                         bindings[i] = entry.getValue();
459                         if (valuesIter.hasNext()) {
460                             sql.append(", ");
461                         }
462                         i++;
463                     }
464 
465                     sql.append(" WHERE " + mColumns[mRowIdColumnIndex]
466                             + '=' + rowId);
467                     sql.append(';');
468                     mDatabase.execSQL(sql.toString(), bindings);
469                     mDatabase.rowUpdated(mEditTable, rowId);
470                 }
471                 mDatabase.setTransactionSuccessful();
472             } finally {
473                 mDatabase.endTransaction();
474             }
475 
476             mUpdatedRows.clear();
477         }
478 
479         // Let any change observers know about the update
480         onChange(true);
481 
482         return true;
483     }
484 
deactivateCommon()485     private void deactivateCommon() {
486         if (Config.LOGV) Log.v(TAG, "<<< Releasing cursor " + this);
487         mCursorState = 0;
488         if (mWindow != null) {
489             mWindow.close();
490             mWindow = null;
491         }
492         if (Config.LOGV) Log.v("DatabaseWindow", "closing window in release()");
493     }
494 
495     @Override
deactivate()496     public void deactivate() {
497         super.deactivate();
498         deactivateCommon();
499         mDriver.cursorDeactivated();
500     }
501 
502     @Override
close()503     public void close() {
504         super.close();
505         deactivateCommon();
506         mQuery.close();
507         mDriver.cursorClosed();
508     }
509 
510     @Override
requery()511     public boolean requery() {
512         if (isClosed()) {
513             return false;
514         }
515         long timeStart = 0;
516         if (Config.LOGV) {
517             timeStart = System.currentTimeMillis();
518         }
519         /*
520          * Synchronize on the database lock to ensure that mCount matches the
521          * results of mQuery.requery().
522          */
523         mDatabase.lock();
524         try {
525             if (mWindow != null) {
526                 mWindow.clear();
527             }
528             mPos = -1;
529             // This one will recreate the temp table, and get its count
530             mDriver.cursorRequeried(this);
531             mCount = NO_COUNT;
532             mCursorState++;
533             queryThreadLock();
534             try {
535                 mQuery.requery();
536             } finally {
537                 queryThreadUnlock();
538             }
539         } finally {
540             mDatabase.unlock();
541         }
542 
543         if (Config.LOGV) {
544             Log.v("DatabaseWindow", "closing window in requery()");
545             Log.v(TAG, "--- Requery()ed cursor " + this + ": " + mQuery);
546         }
547 
548         boolean result = super.requery();
549         if (Config.LOGV) {
550             long timeEnd = System.currentTimeMillis();
551             Log.v(TAG, "requery (" + (timeEnd - timeStart) + " ms): " + mDriver.toString());
552         }
553         return result;
554     }
555 
556     @Override
setWindow(CursorWindow window)557     public void setWindow(CursorWindow window) {
558         if (mWindow != null) {
559             mCursorState++;
560             queryThreadLock();
561             try {
562                 mWindow.close();
563             } finally {
564                 queryThreadUnlock();
565             }
566             mCount = NO_COUNT;
567         }
568         mWindow = window;
569     }
570 
571     /**
572      * Changes the selection arguments. The new values take effect after a call to requery().
573      */
setSelectionArguments(String[] selectionArgs)574     public void setSelectionArguments(String[] selectionArgs) {
575         mDriver.setBindArguments(selectionArgs);
576     }
577 
578     /**
579      * Release the native resources, if they haven't been released yet.
580      */
581     @Override
finalize()582     protected void finalize() {
583         try {
584             // if the cursor hasn't been closed yet, close it first
585             if (mWindow != null) {
586                 if (StrictMode.vmSqliteObjectLeaksEnabled()) {
587                     int len = mQuery.mSql.length();
588                     StrictMode.onSqliteObjectLeaked(
589                         "Finalizing a Cursor that has not been deactivated or closed. " +
590                         "database = " + mDatabase.getPath() + ", table = " + mEditTable +
591                         ", query = " + mQuery.mSql.substring(0, (len > 100) ? 100 : len),
592                         mStackTrace);
593                 }
594                 close();
595                 SQLiteDebug.notifyActiveCursorFinalized();
596             } else {
597                 if (Config.LOGV) {
598                     Log.v(TAG, "Finalizing cursor on database = " + mDatabase.getPath() +
599                             ", table = " + mEditTable + ", query = " + mQuery.mSql);
600                 }
601             }
602         } finally {
603             super.finalize();
604         }
605     }
606 }
607