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