SQLiteConnection.java revision 47847f3f4dcf2a0dbea0bc0e4f02528e21d37a88
1/*
2 * Copyright (C) 2011 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
17package android.database.sqlite;
18
19import dalvik.system.BlockGuard;
20import dalvik.system.CloseGuard;
21
22import android.content.CancellationSignal;
23import android.content.OperationCanceledException;
24import android.database.Cursor;
25import android.database.CursorWindow;
26import android.database.DatabaseUtils;
27import android.database.sqlite.SQLiteDebug.DbStats;
28import android.os.ParcelFileDescriptor;
29import android.util.Log;
30import android.util.LruCache;
31import android.util.Printer;
32
33import java.sql.Date;
34import java.text.SimpleDateFormat;
35import java.util.ArrayList;
36import java.util.Map;
37import java.util.regex.Pattern;
38
39/**
40 * Represents a SQLite database connection.
41 * Each connection wraps an instance of a native <code>sqlite3</code> object.
42 * <p>
43 * When database connection pooling is enabled, there can be multiple active
44 * connections to the same database.  Otherwise there is typically only one
45 * connection per database.
46 * </p><p>
47 * When the SQLite WAL feature is enabled, multiple readers and one writer
48 * can concurrently access the database.  Without WAL, readers and writers
49 * are mutually exclusive.
50 * </p>
51 *
52 * <h2>Ownership and concurrency guarantees</h2>
53 * <p>
54 * Connection objects are not thread-safe.  They are acquired as needed to
55 * perform a database operation and are then returned to the pool.  At any
56 * given time, a connection is either owned and used by a {@link SQLiteSession}
57 * object or the {@link SQLiteConnectionPool}.  Those classes are
58 * responsible for serializing operations to guard against concurrent
59 * use of a connection.
60 * </p><p>
61 * The guarantee of having a single owner allows this class to be implemented
62 * without locks and greatly simplifies resource management.
63 * </p>
64 *
65 * <h2>Encapsulation guarantees</h2>
66 * <p>
67 * The connection object object owns *all* of the SQLite related native
68 * objects that are associated with the connection.  What's more, there are
69 * no other objects in the system that are capable of obtaining handles to
70 * those native objects.  Consequently, when the connection is closed, we do
71 * not have to worry about what other components might have references to
72 * its associated SQLite state -- there are none.
73 * </p><p>
74 * Encapsulation is what ensures that the connection object's
75 * lifecycle does not become a tortured mess of finalizers and reference
76 * queues.
77 * </p>
78 *
79 * <h2>Reentrance</h2>
80 * <p>
81 * This class must tolerate reentrant execution of SQLite operations because
82 * triggers may call custom SQLite functions that perform additional queries.
83 * </p>
84 *
85 * @hide
86 */
87public final class SQLiteConnection implements CancellationSignal.OnCancelListener {
88    private static final String TAG = "SQLiteConnection";
89    private static final boolean DEBUG = false;
90
91    private static final String[] EMPTY_STRING_ARRAY = new String[0];
92    private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
93
94    private static final Pattern TRIM_SQL_PATTERN = Pattern.compile("[\\s]*\\n+[\\s]*");
95
96    private final CloseGuard mCloseGuard = CloseGuard.get();
97
98    private final SQLiteConnectionPool mPool;
99    private final SQLiteDatabaseConfiguration mConfiguration;
100    private final int mConnectionId;
101    private final boolean mIsPrimaryConnection;
102    private final boolean mIsReadOnlyConnection;
103    private final PreparedStatementCache mPreparedStatementCache;
104    private PreparedStatement mPreparedStatementPool;
105
106    // The recent operations log.
107    private final OperationLog mRecentOperations = new OperationLog();
108
109    // The native SQLiteConnection pointer.  (FOR INTERNAL USE ONLY)
110    private int mConnectionPtr;
111
112    private boolean mOnlyAllowReadOnlyOperations;
113
114    // The number of times attachCancellationSignal has been called.
115    // Because SQLite statement execution can be reentrant, we keep track of how many
116    // times we have attempted to attach a cancellation signal to the connection so that
117    // we can ensure that we detach the signal at the right time.
118    private int mCancellationSignalAttachCount;
119
120    private static native int nativeOpen(String path, int openFlags, String label,
121            boolean enableTrace, boolean enableProfile);
122    private static native void nativeClose(int connectionPtr);
123    private static native void nativeRegisterCustomFunction(int connectionPtr,
124            SQLiteCustomFunction function);
125    private static native void nativeRegisterLocalizedCollators(int connectionPtr, String locale);
126    private static native int nativePrepareStatement(int connectionPtr, String sql);
127    private static native void nativeFinalizeStatement(int connectionPtr, int statementPtr);
128    private static native int nativeGetParameterCount(int connectionPtr, int statementPtr);
129    private static native boolean nativeIsReadOnly(int connectionPtr, int statementPtr);
130    private static native int nativeGetColumnCount(int connectionPtr, int statementPtr);
131    private static native String nativeGetColumnName(int connectionPtr, int statementPtr,
132            int index);
133    private static native void nativeBindNull(int connectionPtr, int statementPtr,
134            int index);
135    private static native void nativeBindLong(int connectionPtr, int statementPtr,
136            int index, long value);
137    private static native void nativeBindDouble(int connectionPtr, int statementPtr,
138            int index, double value);
139    private static native void nativeBindString(int connectionPtr, int statementPtr,
140            int index, String value);
141    private static native void nativeBindBlob(int connectionPtr, int statementPtr,
142            int index, byte[] value);
143    private static native void nativeResetStatementAndClearBindings(
144            int connectionPtr, int statementPtr);
145    private static native void nativeExecute(int connectionPtr, int statementPtr);
146    private static native long nativeExecuteForLong(int connectionPtr, int statementPtr);
147    private static native String nativeExecuteForString(int connectionPtr, int statementPtr);
148    private static native int nativeExecuteForBlobFileDescriptor(
149            int connectionPtr, int statementPtr);
150    private static native int nativeExecuteForChangedRowCount(int connectionPtr, int statementPtr);
151    private static native long nativeExecuteForLastInsertedRowId(
152            int connectionPtr, int statementPtr);
153    private static native long nativeExecuteForCursorWindow(
154            int connectionPtr, int statementPtr, int windowPtr,
155            int startPos, int requiredPos, boolean countAllRows);
156    private static native int nativeGetDbLookaside(int connectionPtr);
157    private static native void nativeCancel(int connectionPtr);
158    private static native void nativeResetCancel(int connectionPtr, boolean cancelable);
159
160    private SQLiteConnection(SQLiteConnectionPool pool,
161            SQLiteDatabaseConfiguration configuration,
162            int connectionId, boolean primaryConnection) {
163        mPool = pool;
164        mConfiguration = new SQLiteDatabaseConfiguration(configuration);
165        mConnectionId = connectionId;
166        mIsPrimaryConnection = primaryConnection;
167        mIsReadOnlyConnection = (configuration.openFlags & SQLiteDatabase.OPEN_READONLY) != 0;
168        mPreparedStatementCache = new PreparedStatementCache(
169                mConfiguration.maxSqlCacheSize);
170        mCloseGuard.open("close");
171    }
172
173    @Override
174    protected void finalize() throws Throwable {
175        try {
176            if (mPool != null && mConnectionPtr != 0) {
177                mPool.onConnectionLeaked();
178            }
179
180            dispose(true);
181        } finally {
182            super.finalize();
183        }
184    }
185
186    // Called by SQLiteConnectionPool only.
187    static SQLiteConnection open(SQLiteConnectionPool pool,
188            SQLiteDatabaseConfiguration configuration,
189            int connectionId, boolean primaryConnection) {
190        SQLiteConnection connection = new SQLiteConnection(pool, configuration,
191                connectionId, primaryConnection);
192        try {
193            connection.open();
194            return connection;
195        } catch (SQLiteException ex) {
196            connection.dispose(false);
197            throw ex;
198        }
199    }
200
201    // Called by SQLiteConnectionPool only.
202    // Closes the database closes and releases all of its associated resources.
203    // Do not call methods on the connection after it is closed.  It will probably crash.
204    void close() {
205        dispose(false);
206    }
207
208    private void open() {
209        mConnectionPtr = nativeOpen(mConfiguration.path, mConfiguration.openFlags,
210                mConfiguration.label,
211                SQLiteDebug.DEBUG_SQL_STATEMENTS, SQLiteDebug.DEBUG_SQL_TIME);
212
213        setPageSize();
214        setWalModeFromConfiguration();
215        setJournalSizeLimit();
216        setAutoCheckpointInterval();
217        setLocaleFromConfiguration();
218    }
219
220    private void dispose(boolean finalized) {
221        if (mCloseGuard != null) {
222            if (finalized) {
223                mCloseGuard.warnIfOpen();
224            }
225            mCloseGuard.close();
226        }
227
228        if (mConnectionPtr != 0) {
229            final int cookie = mRecentOperations.beginOperation("close", null, null);
230            try {
231                mPreparedStatementCache.evictAll();
232                nativeClose(mConnectionPtr);
233                mConnectionPtr = 0;
234            } finally {
235                mRecentOperations.endOperation(cookie);
236            }
237        }
238    }
239
240    private void setPageSize() {
241        if (!mConfiguration.isInMemoryDb() && !mIsReadOnlyConnection) {
242            final long newValue = SQLiteGlobal.getDefaultPageSize();
243            long value = executeForLong("PRAGMA page_size", null, null);
244            if (value != newValue) {
245                execute("PRAGMA page_size=" + newValue, null, null);
246            }
247        }
248    }
249
250    private void setAutoCheckpointInterval() {
251        if (!mConfiguration.isInMemoryDb() && !mIsReadOnlyConnection) {
252            final long newValue = SQLiteGlobal.getWALAutoCheckpoint();
253            long value = executeForLong("PRAGMA wal_autocheckpoint", null, null);
254            if (value != newValue) {
255                executeForLong("PRAGMA wal_autocheckpoint=" + newValue, null, null);
256            }
257        }
258    }
259
260    private void setJournalSizeLimit() {
261        if (!mConfiguration.isInMemoryDb() && !mIsReadOnlyConnection) {
262            final long newValue = SQLiteGlobal.getJournalSizeLimit();
263            long value = executeForLong("PRAGMA journal_size_limit", null, null);
264            if (value != newValue) {
265                executeForLong("PRAGMA journal_size_limit=" + newValue, null, null);
266            }
267        }
268    }
269
270    private void setWalModeFromConfiguration() {
271        if (!mConfiguration.isInMemoryDb() && !mIsReadOnlyConnection) {
272            if ((mConfiguration.openFlags & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0) {
273                setJournalMode("WAL");
274                setSyncMode(SQLiteGlobal.getWALSyncMode());
275            } else {
276                setJournalMode(SQLiteGlobal.getDefaultJournalMode());
277                setSyncMode(SQLiteGlobal.getDefaultSyncMode());
278            }
279        }
280    }
281
282    private void setSyncMode(String newValue) {
283        String value = executeForString("PRAGMA synchronous", null, null);
284        if (!canonicalizeSyncMode(value).equalsIgnoreCase(
285                canonicalizeSyncMode(newValue))) {
286            execute("PRAGMA synchronous=" + newValue, null, null);
287        }
288    }
289
290    private static String canonicalizeSyncMode(String value) {
291        if (value.equals("0")) {
292            return "OFF";
293        } else if (value.equals("1")) {
294            return "NORMAL";
295        } else if (value.equals("2")) {
296            return "FULL";
297        }
298        return value;
299    }
300
301    private void setJournalMode(String newValue) {
302        String value = executeForString("PRAGMA journal_mode", null, null);
303        if (!value.equalsIgnoreCase(newValue)) {
304            try {
305                String result = executeForString("PRAGMA journal_mode=" + newValue, null, null);
306                if (result.equalsIgnoreCase(newValue)) {
307                    return;
308                }
309                // PRAGMA journal_mode silently fails and returns the original journal
310                // mode in some cases if the journal mode could not be changed.
311            } catch (SQLiteDatabaseLockedException ex) {
312                // This error (SQLITE_BUSY) occurs if one connection has the database
313                // open in WAL mode and another tries to change it to non-WAL.
314            }
315            // Because we always disable WAL mode when a database is first opened
316            // (even if we intend to re-enable it), we can encounter problems if
317            // there is another open connection to the database somewhere.
318            // This can happen for a variety of reasons such as an application opening
319            // the same database in multiple processes at the same time or if there is a
320            // crashing content provider service that the ActivityManager has
321            // removed from its registry but whose process hasn't quite died yet
322            // by the time it is restarted in a new process.
323            //
324            // If we don't change the journal mode, nothing really bad happens.
325            // In the worst case, an application that enables WAL might not actually
326            // get it, although it can still use connection pooling.
327            Log.w(TAG, "Could not change the database journal mode of '"
328                    + mConfiguration.label + "' from '" + value + "' to '" + newValue
329                    + "' because the database is locked.  This usually means that "
330                    + "there are other open connections to the database which prevents "
331                    + "the database from enabling or disabling write-ahead logging mode.  "
332                    + "Proceeding without changing the journal mode.");
333        }
334    }
335
336    private void setLocaleFromConfiguration() {
337        if ((mConfiguration.openFlags & SQLiteDatabase.NO_LOCALIZED_COLLATORS) != 0) {
338            return;
339        }
340
341        // Register the localized collators.
342        final String newLocale = mConfiguration.locale.toString();
343        nativeRegisterLocalizedCollators(mConnectionPtr, newLocale);
344
345        // If the database is read-only, we cannot modify the android metadata table
346        // or existing indexes.
347        if (mIsReadOnlyConnection) {
348            return;
349        }
350
351        try {
352            // Ensure the android metadata table exists.
353            execute("CREATE TABLE IF NOT EXISTS android_metadata (locale TEXT)", null, null);
354
355            // Check whether the locale was actually changed.
356            final String oldLocale = executeForString("SELECT locale FROM android_metadata "
357                    + "UNION SELECT NULL ORDER BY locale DESC LIMIT 1", null, null);
358            if (oldLocale != null && oldLocale.equals(newLocale)) {
359                return;
360            }
361
362            // Go ahead and update the indexes using the new locale.
363            execute("BEGIN", null, null);
364            boolean success = false;
365            try {
366                execute("DELETE FROM android_metadata", null, null);
367                execute("INSERT INTO android_metadata (locale) VALUES(?)",
368                        new Object[] { newLocale }, null);
369                execute("REINDEX LOCALIZED", null, null);
370                success = true;
371            } finally {
372                execute(success ? "COMMIT" : "ROLLBACK", null, null);
373            }
374        } catch (RuntimeException ex) {
375            throw new SQLiteException("Failed to change locale for db '" + mConfiguration.label
376                    + "' to '" + newLocale + "'.", ex);
377        }
378    }
379
380    // Called by SQLiteConnectionPool only.
381    void reconfigure(SQLiteDatabaseConfiguration configuration) {
382        // Register custom functions.
383        final int functionCount = configuration.customFunctions.size();
384        for (int i = 0; i < functionCount; i++) {
385            SQLiteCustomFunction function = configuration.customFunctions.get(i);
386            if (!mConfiguration.customFunctions.contains(function)) {
387                nativeRegisterCustomFunction(mConnectionPtr, function);
388            }
389        }
390
391        // Remember what changed.
392        boolean walModeChanged = ((configuration.openFlags ^ mConfiguration.openFlags)
393                & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0;
394        boolean localeChanged = !configuration.locale.equals(mConfiguration.locale);
395
396        // Update configuration parameters.
397        mConfiguration.updateParametersFrom(configuration);
398
399        // Update prepared statement cache size.
400        mPreparedStatementCache.resize(configuration.maxSqlCacheSize);
401
402        // Update WAL.
403        if (walModeChanged) {
404            setWalModeFromConfiguration();
405        }
406
407        // Update locale.
408        if (localeChanged) {
409            setLocaleFromConfiguration();
410        }
411    }
412
413    // Called by SQLiteConnectionPool only.
414    // When set to true, executing write operations will throw SQLiteException.
415    // Preparing statements that might write is ok, just don't execute them.
416    void setOnlyAllowReadOnlyOperations(boolean readOnly) {
417        mOnlyAllowReadOnlyOperations = readOnly;
418    }
419
420    // Called by SQLiteConnectionPool only.
421    // Returns true if the prepared statement cache contains the specified SQL.
422    boolean isPreparedStatementInCache(String sql) {
423        return mPreparedStatementCache.get(sql) != null;
424    }
425
426    /**
427     * Gets the unique id of this connection.
428     * @return The connection id.
429     */
430    public int getConnectionId() {
431        return mConnectionId;
432    }
433
434    /**
435     * Returns true if this is the primary database connection.
436     * @return True if this is the primary database connection.
437     */
438    public boolean isPrimaryConnection() {
439        return mIsPrimaryConnection;
440    }
441
442    /**
443     * Prepares a statement for execution but does not bind its parameters or execute it.
444     * <p>
445     * This method can be used to check for syntax errors during compilation
446     * prior to execution of the statement.  If the {@code outStatementInfo} argument
447     * is not null, the provided {@link SQLiteStatementInfo} object is populated
448     * with information about the statement.
449     * </p><p>
450     * A prepared statement makes no reference to the arguments that may eventually
451     * be bound to it, consequently it it possible to cache certain prepared statements
452     * such as SELECT or INSERT/UPDATE statements.  If the statement is cacheable,
453     * then it will be stored in the cache for later.
454     * </p><p>
455     * To take advantage of this behavior as an optimization, the connection pool
456     * provides a method to acquire a connection that already has a given SQL statement
457     * in its prepared statement cache so that it is ready for execution.
458     * </p>
459     *
460     * @param sql The SQL statement to prepare.
461     * @param outStatementInfo The {@link SQLiteStatementInfo} object to populate
462     * with information about the statement, or null if none.
463     *
464     * @throws SQLiteException if an error occurs, such as a syntax error.
465     */
466    public void prepare(String sql, SQLiteStatementInfo outStatementInfo) {
467        if (sql == null) {
468            throw new IllegalArgumentException("sql must not be null.");
469        }
470
471        final int cookie = mRecentOperations.beginOperation("prepare", sql, null);
472        try {
473            final PreparedStatement statement = acquirePreparedStatement(sql);
474            try {
475                if (outStatementInfo != null) {
476                    outStatementInfo.numParameters = statement.mNumParameters;
477                    outStatementInfo.readOnly = statement.mReadOnly;
478
479                    final int columnCount = nativeGetColumnCount(
480                            mConnectionPtr, statement.mStatementPtr);
481                    if (columnCount == 0) {
482                        outStatementInfo.columnNames = EMPTY_STRING_ARRAY;
483                    } else {
484                        outStatementInfo.columnNames = new String[columnCount];
485                        for (int i = 0; i < columnCount; i++) {
486                            outStatementInfo.columnNames[i] = nativeGetColumnName(
487                                    mConnectionPtr, statement.mStatementPtr, i);
488                        }
489                    }
490                }
491            } finally {
492                releasePreparedStatement(statement);
493            }
494        } catch (RuntimeException ex) {
495            mRecentOperations.failOperation(cookie, ex);
496            throw ex;
497        } finally {
498            mRecentOperations.endOperation(cookie);
499        }
500    }
501
502    /**
503     * Executes a statement that does not return a result.
504     *
505     * @param sql The SQL statement to execute.
506     * @param bindArgs The arguments to bind, or null if none.
507     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
508     *
509     * @throws SQLiteException if an error occurs, such as a syntax error
510     * or invalid number of bind arguments.
511     * @throws OperationCanceledException if the operation was canceled.
512     */
513    public void execute(String sql, Object[] bindArgs,
514            CancellationSignal cancellationSignal) {
515        if (sql == null) {
516            throw new IllegalArgumentException("sql must not be null.");
517        }
518
519        final int cookie = mRecentOperations.beginOperation("execute", sql, bindArgs);
520        try {
521            final PreparedStatement statement = acquirePreparedStatement(sql);
522            try {
523                throwIfStatementForbidden(statement);
524                bindArguments(statement, bindArgs);
525                applyBlockGuardPolicy(statement);
526                attachCancellationSignal(cancellationSignal);
527                try {
528                    nativeExecute(mConnectionPtr, statement.mStatementPtr);
529                } finally {
530                    detachCancellationSignal(cancellationSignal);
531                }
532            } finally {
533                releasePreparedStatement(statement);
534            }
535        } catch (RuntimeException ex) {
536            mRecentOperations.failOperation(cookie, ex);
537            throw ex;
538        } finally {
539            mRecentOperations.endOperation(cookie);
540        }
541    }
542
543    /**
544     * Executes a statement that returns a single <code>long</code> result.
545     *
546     * @param sql The SQL statement to execute.
547     * @param bindArgs The arguments to bind, or null if none.
548     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
549     * @return The value of the first column in the first row of the result set
550     * as a <code>long</code>, or zero if none.
551     *
552     * @throws SQLiteException if an error occurs, such as a syntax error
553     * or invalid number of bind arguments.
554     * @throws OperationCanceledException if the operation was canceled.
555     */
556    public long executeForLong(String sql, Object[] bindArgs,
557            CancellationSignal cancellationSignal) {
558        if (sql == null) {
559            throw new IllegalArgumentException("sql must not be null.");
560        }
561
562        final int cookie = mRecentOperations.beginOperation("executeForLong", sql, bindArgs);
563        try {
564            final PreparedStatement statement = acquirePreparedStatement(sql);
565            try {
566                throwIfStatementForbidden(statement);
567                bindArguments(statement, bindArgs);
568                applyBlockGuardPolicy(statement);
569                attachCancellationSignal(cancellationSignal);
570                try {
571                    return nativeExecuteForLong(mConnectionPtr, statement.mStatementPtr);
572                } finally {
573                    detachCancellationSignal(cancellationSignal);
574                }
575            } finally {
576                releasePreparedStatement(statement);
577            }
578        } catch (RuntimeException ex) {
579            mRecentOperations.failOperation(cookie, ex);
580            throw ex;
581        } finally {
582            mRecentOperations.endOperation(cookie);
583        }
584    }
585
586    /**
587     * Executes a statement that returns a single {@link String} result.
588     *
589     * @param sql The SQL statement to execute.
590     * @param bindArgs The arguments to bind, or null if none.
591     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
592     * @return The value of the first column in the first row of the result set
593     * as a <code>String</code>, or null if none.
594     *
595     * @throws SQLiteException if an error occurs, such as a syntax error
596     * or invalid number of bind arguments.
597     * @throws OperationCanceledException if the operation was canceled.
598     */
599    public String executeForString(String sql, Object[] bindArgs,
600            CancellationSignal cancellationSignal) {
601        if (sql == null) {
602            throw new IllegalArgumentException("sql must not be null.");
603        }
604
605        final int cookie = mRecentOperations.beginOperation("executeForString", sql, bindArgs);
606        try {
607            final PreparedStatement statement = acquirePreparedStatement(sql);
608            try {
609                throwIfStatementForbidden(statement);
610                bindArguments(statement, bindArgs);
611                applyBlockGuardPolicy(statement);
612                attachCancellationSignal(cancellationSignal);
613                try {
614                    return nativeExecuteForString(mConnectionPtr, statement.mStatementPtr);
615                } finally {
616                    detachCancellationSignal(cancellationSignal);
617                }
618            } finally {
619                releasePreparedStatement(statement);
620            }
621        } catch (RuntimeException ex) {
622            mRecentOperations.failOperation(cookie, ex);
623            throw ex;
624        } finally {
625            mRecentOperations.endOperation(cookie);
626        }
627    }
628
629    /**
630     * Executes a statement that returns a single BLOB result as a
631     * file descriptor to a shared memory region.
632     *
633     * @param sql The SQL statement to execute.
634     * @param bindArgs The arguments to bind, or null if none.
635     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
636     * @return The file descriptor for a shared memory region that contains
637     * the value of the first column in the first row of the result set as a BLOB,
638     * or null if none.
639     *
640     * @throws SQLiteException if an error occurs, such as a syntax error
641     * or invalid number of bind arguments.
642     * @throws OperationCanceledException if the operation was canceled.
643     */
644    public ParcelFileDescriptor executeForBlobFileDescriptor(String sql, Object[] bindArgs,
645            CancellationSignal cancellationSignal) {
646        if (sql == null) {
647            throw new IllegalArgumentException("sql must not be null.");
648        }
649
650        final int cookie = mRecentOperations.beginOperation("executeForBlobFileDescriptor",
651                sql, bindArgs);
652        try {
653            final PreparedStatement statement = acquirePreparedStatement(sql);
654            try {
655                throwIfStatementForbidden(statement);
656                bindArguments(statement, bindArgs);
657                applyBlockGuardPolicy(statement);
658                attachCancellationSignal(cancellationSignal);
659                try {
660                    int fd = nativeExecuteForBlobFileDescriptor(
661                            mConnectionPtr, statement.mStatementPtr);
662                    return fd >= 0 ? ParcelFileDescriptor.adoptFd(fd) : null;
663                } finally {
664                    detachCancellationSignal(cancellationSignal);
665                }
666            } finally {
667                releasePreparedStatement(statement);
668            }
669        } catch (RuntimeException ex) {
670            mRecentOperations.failOperation(cookie, ex);
671            throw ex;
672        } finally {
673            mRecentOperations.endOperation(cookie);
674        }
675    }
676
677    /**
678     * Executes a statement that returns a count of the number of rows
679     * that were changed.  Use for UPDATE or DELETE SQL statements.
680     *
681     * @param sql The SQL statement to execute.
682     * @param bindArgs The arguments to bind, or null if none.
683     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
684     * @return The number of rows that were changed.
685     *
686     * @throws SQLiteException if an error occurs, such as a syntax error
687     * or invalid number of bind arguments.
688     * @throws OperationCanceledException if the operation was canceled.
689     */
690    public int executeForChangedRowCount(String sql, Object[] bindArgs,
691            CancellationSignal cancellationSignal) {
692        if (sql == null) {
693            throw new IllegalArgumentException("sql must not be null.");
694        }
695
696        final int cookie = mRecentOperations.beginOperation("executeForChangedRowCount",
697                sql, bindArgs);
698        try {
699            final PreparedStatement statement = acquirePreparedStatement(sql);
700            try {
701                throwIfStatementForbidden(statement);
702                bindArguments(statement, bindArgs);
703                applyBlockGuardPolicy(statement);
704                attachCancellationSignal(cancellationSignal);
705                try {
706                    return nativeExecuteForChangedRowCount(
707                            mConnectionPtr, statement.mStatementPtr);
708                } finally {
709                    detachCancellationSignal(cancellationSignal);
710                }
711            } finally {
712                releasePreparedStatement(statement);
713            }
714        } catch (RuntimeException ex) {
715            mRecentOperations.failOperation(cookie, ex);
716            throw ex;
717        } finally {
718            mRecentOperations.endOperation(cookie);
719        }
720    }
721
722    /**
723     * Executes a statement that returns the row id of the last row inserted
724     * by the statement.  Use for INSERT SQL statements.
725     *
726     * @param sql The SQL statement to execute.
727     * @param bindArgs The arguments to bind, or null if none.
728     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
729     * @return The row id of the last row that was inserted, or 0 if none.
730     *
731     * @throws SQLiteException if an error occurs, such as a syntax error
732     * or invalid number of bind arguments.
733     * @throws OperationCanceledException if the operation was canceled.
734     */
735    public long executeForLastInsertedRowId(String sql, Object[] bindArgs,
736            CancellationSignal cancellationSignal) {
737        if (sql == null) {
738            throw new IllegalArgumentException("sql must not be null.");
739        }
740
741        final int cookie = mRecentOperations.beginOperation("executeForLastInsertedRowId",
742                sql, bindArgs);
743        try {
744            final PreparedStatement statement = acquirePreparedStatement(sql);
745            try {
746                throwIfStatementForbidden(statement);
747                bindArguments(statement, bindArgs);
748                applyBlockGuardPolicy(statement);
749                attachCancellationSignal(cancellationSignal);
750                try {
751                    return nativeExecuteForLastInsertedRowId(
752                            mConnectionPtr, statement.mStatementPtr);
753                } finally {
754                    detachCancellationSignal(cancellationSignal);
755                }
756            } finally {
757                releasePreparedStatement(statement);
758            }
759        } catch (RuntimeException ex) {
760            mRecentOperations.failOperation(cookie, ex);
761            throw ex;
762        } finally {
763            mRecentOperations.endOperation(cookie);
764        }
765    }
766
767    /**
768     * Executes a statement and populates the specified {@link CursorWindow}
769     * with a range of results.  Returns the number of rows that were counted
770     * during query execution.
771     *
772     * @param sql The SQL statement to execute.
773     * @param bindArgs The arguments to bind, or null if none.
774     * @param window The cursor window to clear and fill.
775     * @param startPos The start position for filling the window.
776     * @param requiredPos The position of a row that MUST be in the window.
777     * If it won't fit, then the query should discard part of what it filled
778     * so that it does.  Must be greater than or equal to <code>startPos</code>.
779     * @param countAllRows True to count all rows that the query would return
780     * regagless of whether they fit in the window.
781     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
782     * @return The number of rows that were counted during query execution.  Might
783     * not be all rows in the result set unless <code>countAllRows</code> is true.
784     *
785     * @throws SQLiteException if an error occurs, such as a syntax error
786     * or invalid number of bind arguments.
787     * @throws OperationCanceledException if the operation was canceled.
788     */
789    public int executeForCursorWindow(String sql, Object[] bindArgs,
790            CursorWindow window, int startPos, int requiredPos, boolean countAllRows,
791            CancellationSignal cancellationSignal) {
792        if (sql == null) {
793            throw new IllegalArgumentException("sql must not be null.");
794        }
795        if (window == null) {
796            throw new IllegalArgumentException("window must not be null.");
797        }
798
799        window.acquireReference();
800        try {
801            int actualPos = -1;
802            int countedRows = -1;
803            int filledRows = -1;
804            final int cookie = mRecentOperations.beginOperation("executeForCursorWindow",
805                    sql, bindArgs);
806            try {
807                final PreparedStatement statement = acquirePreparedStatement(sql);
808                try {
809                    throwIfStatementForbidden(statement);
810                    bindArguments(statement, bindArgs);
811                    applyBlockGuardPolicy(statement);
812                    attachCancellationSignal(cancellationSignal);
813                    try {
814                        final long result = nativeExecuteForCursorWindow(
815                                mConnectionPtr, statement.mStatementPtr, window.mWindowPtr,
816                                startPos, requiredPos, countAllRows);
817                        actualPos = (int)(result >> 32);
818                        countedRows = (int)result;
819                        filledRows = window.getNumRows();
820                        window.setStartPosition(actualPos);
821                        return countedRows;
822                    } finally {
823                        detachCancellationSignal(cancellationSignal);
824                    }
825                } finally {
826                    releasePreparedStatement(statement);
827                }
828            } catch (RuntimeException ex) {
829                mRecentOperations.failOperation(cookie, ex);
830                throw ex;
831            } finally {
832                if (mRecentOperations.endOperationDeferLog(cookie)) {
833                    mRecentOperations.logOperation(cookie, "window='" + window
834                            + "', startPos=" + startPos
835                            + ", actualPos=" + actualPos
836                            + ", filledRows=" + filledRows
837                            + ", countedRows=" + countedRows);
838                }
839            }
840        } finally {
841            window.releaseReference();
842        }
843    }
844
845    private PreparedStatement acquirePreparedStatement(String sql) {
846        PreparedStatement statement = mPreparedStatementCache.get(sql);
847        boolean skipCache = false;
848        if (statement != null) {
849            if (!statement.mInUse) {
850                return statement;
851            }
852            // The statement is already in the cache but is in use (this statement appears
853            // to be not only re-entrant but recursive!).  So prepare a new copy of the
854            // statement but do not cache it.
855            skipCache = true;
856        }
857
858        final int statementPtr = nativePrepareStatement(mConnectionPtr, sql);
859        try {
860            final int numParameters = nativeGetParameterCount(mConnectionPtr, statementPtr);
861            final int type = DatabaseUtils.getSqlStatementType(sql);
862            final boolean readOnly = nativeIsReadOnly(mConnectionPtr, statementPtr);
863            statement = obtainPreparedStatement(sql, statementPtr, numParameters, type, readOnly);
864            if (!skipCache && isCacheable(type)) {
865                mPreparedStatementCache.put(sql, statement);
866                statement.mInCache = true;
867            }
868        } catch (RuntimeException ex) {
869            // Finalize the statement if an exception occurred and we did not add
870            // it to the cache.  If it is already in the cache, then leave it there.
871            if (statement == null || !statement.mInCache) {
872                nativeFinalizeStatement(mConnectionPtr, statementPtr);
873            }
874            throw ex;
875        }
876        statement.mInUse = true;
877        return statement;
878    }
879
880    private void releasePreparedStatement(PreparedStatement statement) {
881        statement.mInUse = false;
882        if (statement.mInCache) {
883            try {
884                nativeResetStatementAndClearBindings(mConnectionPtr, statement.mStatementPtr);
885            } catch (SQLiteException ex) {
886                // The statement could not be reset due to an error.  Remove it from the cache.
887                // When remove() is called, the cache will invoke its entryRemoved() callback,
888                // which will in turn call finalizePreparedStatement() to finalize and
889                // recycle the statement.
890                if (DEBUG) {
891                    Log.d(TAG, "Could not reset prepared statement due to an exception.  "
892                            + "Removing it from the cache.  SQL: "
893                            + trimSqlForDisplay(statement.mSql), ex);
894                }
895
896                mPreparedStatementCache.remove(statement.mSql);
897            }
898        } else {
899            finalizePreparedStatement(statement);
900        }
901    }
902
903    private void finalizePreparedStatement(PreparedStatement statement) {
904        nativeFinalizeStatement(mConnectionPtr, statement.mStatementPtr);
905        recyclePreparedStatement(statement);
906    }
907
908    private void attachCancellationSignal(CancellationSignal cancellationSignal) {
909        if (cancellationSignal != null) {
910            cancellationSignal.throwIfCanceled();
911
912            mCancellationSignalAttachCount += 1;
913            if (mCancellationSignalAttachCount == 1) {
914                // Reset cancellation flag before executing the statement.
915                nativeResetCancel(mConnectionPtr, true /*cancelable*/);
916
917                // After this point, onCancel() may be called concurrently.
918                cancellationSignal.setOnCancelListener(this);
919            }
920        }
921    }
922
923    private void detachCancellationSignal(CancellationSignal cancellationSignal) {
924        if (cancellationSignal != null) {
925            assert mCancellationSignalAttachCount > 0;
926
927            mCancellationSignalAttachCount -= 1;
928            if (mCancellationSignalAttachCount == 0) {
929                // After this point, onCancel() cannot be called concurrently.
930                cancellationSignal.setOnCancelListener(null);
931
932                // Reset cancellation flag after executing the statement.
933                nativeResetCancel(mConnectionPtr, false /*cancelable*/);
934            }
935        }
936    }
937
938    // CancellationSignal.OnCancelListener callback.
939    // This method may be called on a different thread than the executing statement.
940    // However, it will only be called between calls to attachCancellationSignal and
941    // detachCancellationSignal, while a statement is executing.  We can safely assume
942    // that the SQLite connection is still alive.
943    @Override
944    public void onCancel() {
945        nativeCancel(mConnectionPtr);
946    }
947
948    private void bindArguments(PreparedStatement statement, Object[] bindArgs) {
949        final int count = bindArgs != null ? bindArgs.length : 0;
950        if (count != statement.mNumParameters) {
951            throw new SQLiteBindOrColumnIndexOutOfRangeException(
952                    "Expected " + statement.mNumParameters + " bind arguments but "
953                    + bindArgs.length + " were provided.");
954        }
955        if (count == 0) {
956            return;
957        }
958
959        final int statementPtr = statement.mStatementPtr;
960        for (int i = 0; i < count; i++) {
961            final Object arg = bindArgs[i];
962            switch (DatabaseUtils.getTypeOfObject(arg)) {
963                case Cursor.FIELD_TYPE_NULL:
964                    nativeBindNull(mConnectionPtr, statementPtr, i + 1);
965                    break;
966                case Cursor.FIELD_TYPE_INTEGER:
967                    nativeBindLong(mConnectionPtr, statementPtr, i + 1,
968                            ((Number)arg).longValue());
969                    break;
970                case Cursor.FIELD_TYPE_FLOAT:
971                    nativeBindDouble(mConnectionPtr, statementPtr, i + 1,
972                            ((Number)arg).doubleValue());
973                    break;
974                case Cursor.FIELD_TYPE_BLOB:
975                    nativeBindBlob(mConnectionPtr, statementPtr, i + 1, (byte[])arg);
976                    break;
977                case Cursor.FIELD_TYPE_STRING:
978                default:
979                    if (arg instanceof Boolean) {
980                        // Provide compatibility with legacy applications which may pass
981                        // Boolean values in bind args.
982                        nativeBindLong(mConnectionPtr, statementPtr, i + 1,
983                                ((Boolean)arg).booleanValue() ? 1 : 0);
984                    } else {
985                        nativeBindString(mConnectionPtr, statementPtr, i + 1, arg.toString());
986                    }
987                    break;
988            }
989        }
990    }
991
992    private void throwIfStatementForbidden(PreparedStatement statement) {
993        if (mOnlyAllowReadOnlyOperations && !statement.mReadOnly) {
994            throw new SQLiteException("Cannot execute this statement because it "
995                    + "might modify the database but the connection is read-only.");
996        }
997    }
998
999    private static boolean isCacheable(int statementType) {
1000        if (statementType == DatabaseUtils.STATEMENT_UPDATE
1001                || statementType == DatabaseUtils.STATEMENT_SELECT) {
1002            return true;
1003        }
1004        return false;
1005    }
1006
1007    private void applyBlockGuardPolicy(PreparedStatement statement) {
1008        if (!mConfiguration.isInMemoryDb()) {
1009            if (statement.mReadOnly) {
1010                BlockGuard.getThreadPolicy().onReadFromDisk();
1011            } else {
1012                BlockGuard.getThreadPolicy().onWriteToDisk();
1013            }
1014        }
1015    }
1016
1017    /**
1018     * Dumps debugging information about this connection.
1019     *
1020     * @param printer The printer to receive the dump, not null.
1021     * @param verbose True to dump more verbose information.
1022     */
1023    public void dump(Printer printer, boolean verbose) {
1024        dumpUnsafe(printer, verbose);
1025    }
1026
1027    /**
1028     * Dumps debugging information about this connection, in the case where the
1029     * caller might not actually own the connection.
1030     *
1031     * This function is written so that it may be called by a thread that does not
1032     * own the connection.  We need to be very careful because the connection state is
1033     * not synchronized.
1034     *
1035     * At worst, the method may return stale or slightly wrong data, however
1036     * it should not crash.  This is ok as it is only used for diagnostic purposes.
1037     *
1038     * @param printer The printer to receive the dump, not null.
1039     * @param verbose True to dump more verbose information.
1040     */
1041    void dumpUnsafe(Printer printer, boolean verbose) {
1042        printer.println("Connection #" + mConnectionId + ":");
1043        if (verbose) {
1044            printer.println("  connectionPtr: 0x" + Integer.toHexString(mConnectionPtr));
1045        }
1046        printer.println("  isPrimaryConnection: " + mIsPrimaryConnection);
1047        printer.println("  onlyAllowReadOnlyOperations: " + mOnlyAllowReadOnlyOperations);
1048
1049        mRecentOperations.dump(printer);
1050
1051        if (verbose) {
1052            mPreparedStatementCache.dump(printer);
1053        }
1054    }
1055
1056    /**
1057     * Describes the currently executing operation, in the case where the
1058     * caller might not actually own the connection.
1059     *
1060     * This function is written so that it may be called by a thread that does not
1061     * own the connection.  We need to be very careful because the connection state is
1062     * not synchronized.
1063     *
1064     * At worst, the method may return stale or slightly wrong data, however
1065     * it should not crash.  This is ok as it is only used for diagnostic purposes.
1066     *
1067     * @return A description of the current operation including how long it has been running,
1068     * or null if none.
1069     */
1070    String describeCurrentOperationUnsafe() {
1071        return mRecentOperations.describeCurrentOperation();
1072    }
1073
1074    /**
1075     * Collects statistics about database connection memory usage.
1076     *
1077     * @param dbStatsList The list to populate.
1078     */
1079    void collectDbStats(ArrayList<DbStats> dbStatsList) {
1080        // Get information about the main database.
1081        int lookaside = nativeGetDbLookaside(mConnectionPtr);
1082        long pageCount = 0;
1083        long pageSize = 0;
1084        try {
1085            pageCount = executeForLong("PRAGMA page_count;", null, null);
1086            pageSize = executeForLong("PRAGMA page_size;", null, null);
1087        } catch (SQLiteException ex) {
1088            // Ignore.
1089        }
1090        dbStatsList.add(getMainDbStatsUnsafe(lookaside, pageCount, pageSize));
1091
1092        // Get information about attached databases.
1093        // We ignore the first row in the database list because it corresponds to
1094        // the main database which we have already described.
1095        CursorWindow window = new CursorWindow("collectDbStats");
1096        try {
1097            executeForCursorWindow("PRAGMA database_list;", null, window, 0, 0, false, null);
1098            for (int i = 1; i < window.getNumRows(); i++) {
1099                String name = window.getString(i, 1);
1100                String path = window.getString(i, 2);
1101                pageCount = 0;
1102                pageSize = 0;
1103                try {
1104                    pageCount = executeForLong("PRAGMA " + name + ".page_count;", null, null);
1105                    pageSize = executeForLong("PRAGMA " + name + ".page_size;", null, null);
1106                } catch (SQLiteException ex) {
1107                    // Ignore.
1108                }
1109                String label = "  (attached) " + name;
1110                if (!path.isEmpty()) {
1111                    label += ": " + path;
1112                }
1113                dbStatsList.add(new DbStats(label, pageCount, pageSize, 0, 0, 0, 0));
1114            }
1115        } catch (SQLiteException ex) {
1116            // Ignore.
1117        } finally {
1118            window.close();
1119        }
1120    }
1121
1122    /**
1123     * Collects statistics about database connection memory usage, in the case where the
1124     * caller might not actually own the connection.
1125     *
1126     * @return The statistics object, never null.
1127     */
1128    void collectDbStatsUnsafe(ArrayList<DbStats> dbStatsList) {
1129        dbStatsList.add(getMainDbStatsUnsafe(0, 0, 0));
1130    }
1131
1132    private DbStats getMainDbStatsUnsafe(int lookaside, long pageCount, long pageSize) {
1133        // The prepared statement cache is thread-safe so we can access its statistics
1134        // even if we do not own the database connection.
1135        String label = mConfiguration.path;
1136        if (!mIsPrimaryConnection) {
1137            label += " (" + mConnectionId + ")";
1138        }
1139        return new DbStats(label, pageCount, pageSize, lookaside,
1140                mPreparedStatementCache.hitCount(),
1141                mPreparedStatementCache.missCount(),
1142                mPreparedStatementCache.size());
1143    }
1144
1145    @Override
1146    public String toString() {
1147        return "SQLiteConnection: " + mConfiguration.path + " (" + mConnectionId + ")";
1148    }
1149
1150    private PreparedStatement obtainPreparedStatement(String sql, int statementPtr,
1151            int numParameters, int type, boolean readOnly) {
1152        PreparedStatement statement = mPreparedStatementPool;
1153        if (statement != null) {
1154            mPreparedStatementPool = statement.mPoolNext;
1155            statement.mPoolNext = null;
1156            statement.mInCache = false;
1157        } else {
1158            statement = new PreparedStatement();
1159        }
1160        statement.mSql = sql;
1161        statement.mStatementPtr = statementPtr;
1162        statement.mNumParameters = numParameters;
1163        statement.mType = type;
1164        statement.mReadOnly = readOnly;
1165        return statement;
1166    }
1167
1168    private void recyclePreparedStatement(PreparedStatement statement) {
1169        statement.mSql = null;
1170        statement.mPoolNext = mPreparedStatementPool;
1171        mPreparedStatementPool = statement;
1172    }
1173
1174    private static String trimSqlForDisplay(String sql) {
1175        return TRIM_SQL_PATTERN.matcher(sql).replaceAll(" ");
1176    }
1177
1178    /**
1179     * Holder type for a prepared statement.
1180     *
1181     * Although this object holds a pointer to a native statement object, it
1182     * does not have a finalizer.  This is deliberate.  The {@link SQLiteConnection}
1183     * owns the statement object and will take care of freeing it when needed.
1184     * In particular, closing the connection requires a guarantee of deterministic
1185     * resource disposal because all native statement objects must be freed before
1186     * the native database object can be closed.  So no finalizers here.
1187     */
1188    private static final class PreparedStatement {
1189        // Next item in pool.
1190        public PreparedStatement mPoolNext;
1191
1192        // The SQL from which the statement was prepared.
1193        public String mSql;
1194
1195        // The native sqlite3_stmt object pointer.
1196        // Lifetime is managed explicitly by the connection.
1197        public int mStatementPtr;
1198
1199        // The number of parameters that the prepared statement has.
1200        public int mNumParameters;
1201
1202        // The statement type.
1203        public int mType;
1204
1205        // True if the statement is read-only.
1206        public boolean mReadOnly;
1207
1208        // True if the statement is in the cache.
1209        public boolean mInCache;
1210
1211        // True if the statement is in use (currently executing).
1212        // We need this flag because due to the use of custom functions in triggers, it's
1213        // possible for SQLite calls to be re-entrant.  Consequently we need to prevent
1214        // in use statements from being finalized until they are no longer in use.
1215        public boolean mInUse;
1216    }
1217
1218    private final class PreparedStatementCache
1219            extends LruCache<String, PreparedStatement> {
1220        public PreparedStatementCache(int size) {
1221            super(size);
1222        }
1223
1224        @Override
1225        protected void entryRemoved(boolean evicted, String key,
1226                PreparedStatement oldValue, PreparedStatement newValue) {
1227            oldValue.mInCache = false;
1228            if (!oldValue.mInUse) {
1229                finalizePreparedStatement(oldValue);
1230            }
1231        }
1232
1233        public void dump(Printer printer) {
1234            printer.println("  Prepared statement cache:");
1235            Map<String, PreparedStatement> cache = snapshot();
1236            if (!cache.isEmpty()) {
1237                int i = 0;
1238                for (Map.Entry<String, PreparedStatement> entry : cache.entrySet()) {
1239                    PreparedStatement statement = entry.getValue();
1240                    if (statement.mInCache) { // might be false due to a race with entryRemoved
1241                        String sql = entry.getKey();
1242                        printer.println("    " + i + ": statementPtr=0x"
1243                                + Integer.toHexString(statement.mStatementPtr)
1244                                + ", numParameters=" + statement.mNumParameters
1245                                + ", type=" + statement.mType
1246                                + ", readOnly=" + statement.mReadOnly
1247                                + ", sql=\"" + trimSqlForDisplay(sql) + "\"");
1248                    }
1249                    i += 1;
1250                }
1251            } else {
1252                printer.println("    <none>");
1253            }
1254        }
1255    }
1256
1257    private static final class OperationLog {
1258        private static final int MAX_RECENT_OPERATIONS = 20;
1259        private static final int COOKIE_GENERATION_SHIFT = 8;
1260        private static final int COOKIE_INDEX_MASK = 0xff;
1261
1262        private final Operation[] mOperations = new Operation[MAX_RECENT_OPERATIONS];
1263        private int mIndex;
1264        private int mGeneration;
1265
1266        public int beginOperation(String kind, String sql, Object[] bindArgs) {
1267            synchronized (mOperations) {
1268                final int index = (mIndex + 1) % MAX_RECENT_OPERATIONS;
1269                Operation operation = mOperations[index];
1270                if (operation == null) {
1271                    operation = new Operation();
1272                    mOperations[index] = operation;
1273                } else {
1274                    operation.mFinished = false;
1275                    operation.mException = null;
1276                    if (operation.mBindArgs != null) {
1277                        operation.mBindArgs.clear();
1278                    }
1279                }
1280                operation.mStartTime = System.currentTimeMillis();
1281                operation.mKind = kind;
1282                operation.mSql = sql;
1283                if (bindArgs != null) {
1284                    if (operation.mBindArgs == null) {
1285                        operation.mBindArgs = new ArrayList<Object>();
1286                    } else {
1287                        operation.mBindArgs.clear();
1288                    }
1289                    for (int i = 0; i < bindArgs.length; i++) {
1290                        final Object arg = bindArgs[i];
1291                        if (arg != null && arg instanceof byte[]) {
1292                            // Don't hold onto the real byte array longer than necessary.
1293                            operation.mBindArgs.add(EMPTY_BYTE_ARRAY);
1294                        } else {
1295                            operation.mBindArgs.add(arg);
1296                        }
1297                    }
1298                }
1299                operation.mCookie = newOperationCookieLocked(index);
1300                mIndex = index;
1301                return operation.mCookie;
1302            }
1303        }
1304
1305        public void failOperation(int cookie, Exception ex) {
1306            synchronized (mOperations) {
1307                final Operation operation = getOperationLocked(cookie);
1308                if (operation != null) {
1309                    operation.mException = ex;
1310                }
1311            }
1312        }
1313
1314        public void endOperation(int cookie) {
1315            synchronized (mOperations) {
1316                if (endOperationDeferLogLocked(cookie)) {
1317                    logOperationLocked(cookie, null);
1318                }
1319            }
1320        }
1321
1322        public boolean endOperationDeferLog(int cookie) {
1323            synchronized (mOperations) {
1324                return endOperationDeferLogLocked(cookie);
1325            }
1326        }
1327
1328        public void logOperation(int cookie, String detail) {
1329            synchronized (mOperations) {
1330                logOperationLocked(cookie, detail);
1331            }
1332        }
1333
1334        private boolean endOperationDeferLogLocked(int cookie) {
1335            final Operation operation = getOperationLocked(cookie);
1336            if (operation != null) {
1337                operation.mEndTime = System.currentTimeMillis();
1338                operation.mFinished = true;
1339                return SQLiteDebug.DEBUG_LOG_SLOW_QUERIES && SQLiteDebug.shouldLogSlowQuery(
1340                                operation.mEndTime - operation.mStartTime);
1341            }
1342            return false;
1343        }
1344
1345        private void logOperationLocked(int cookie, String detail) {
1346            final Operation operation = getOperationLocked(cookie);
1347            StringBuilder msg = new StringBuilder();
1348            operation.describe(msg);
1349            if (detail != null) {
1350                msg.append(", ").append(detail);
1351            }
1352            Log.d(TAG, msg.toString());
1353        }
1354
1355        private int newOperationCookieLocked(int index) {
1356            final int generation = mGeneration++;
1357            return generation << COOKIE_GENERATION_SHIFT | index;
1358        }
1359
1360        private Operation getOperationLocked(int cookie) {
1361            final int index = cookie & COOKIE_INDEX_MASK;
1362            final Operation operation = mOperations[index];
1363            return operation.mCookie == cookie ? operation : null;
1364        }
1365
1366        public String describeCurrentOperation() {
1367            synchronized (mOperations) {
1368                final Operation operation = mOperations[mIndex];
1369                if (operation != null && !operation.mFinished) {
1370                    StringBuilder msg = new StringBuilder();
1371                    operation.describe(msg);
1372                    return msg.toString();
1373                }
1374                return null;
1375            }
1376        }
1377
1378        public void dump(Printer printer) {
1379            synchronized (mOperations) {
1380                printer.println("  Most recently executed operations:");
1381                int index = mIndex;
1382                Operation operation = mOperations[index];
1383                if (operation != null) {
1384                    int n = 0;
1385                    do {
1386                        StringBuilder msg = new StringBuilder();
1387                        msg.append("    ").append(n).append(": [");
1388                        msg.append(operation.getFormattedStartTime());
1389                        msg.append("] ");
1390                        operation.describe(msg);
1391                        printer.println(msg.toString());
1392
1393                        if (index > 0) {
1394                            index -= 1;
1395                        } else {
1396                            index = MAX_RECENT_OPERATIONS - 1;
1397                        }
1398                        n += 1;
1399                        operation = mOperations[index];
1400                    } while (operation != null && n < MAX_RECENT_OPERATIONS);
1401                } else {
1402                    printer.println("    <none>");
1403                }
1404            }
1405        }
1406    }
1407
1408    private static final class Operation {
1409        private static final SimpleDateFormat sDateFormat =
1410                new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
1411
1412        public long mStartTime;
1413        public long mEndTime;
1414        public String mKind;
1415        public String mSql;
1416        public ArrayList<Object> mBindArgs;
1417        public boolean mFinished;
1418        public Exception mException;
1419        public int mCookie;
1420
1421        public void describe(StringBuilder msg) {
1422            msg.append(mKind);
1423            if (mFinished) {
1424                msg.append(" took ").append(mEndTime - mStartTime).append("ms");
1425            } else {
1426                msg.append(" started ").append(System.currentTimeMillis() - mStartTime)
1427                        .append("ms ago");
1428            }
1429            msg.append(" - ").append(getStatus());
1430            if (mSql != null) {
1431                msg.append(", sql=\"").append(trimSqlForDisplay(mSql)).append("\"");
1432            }
1433            if (mBindArgs != null && mBindArgs.size() != 0) {
1434                msg.append(", bindArgs=[");
1435                final int count = mBindArgs.size();
1436                for (int i = 0; i < count; i++) {
1437                    final Object arg = mBindArgs.get(i);
1438                    if (i != 0) {
1439                        msg.append(", ");
1440                    }
1441                    if (arg == null) {
1442                        msg.append("null");
1443                    } else if (arg instanceof byte[]) {
1444                        msg.append("<byte[]>");
1445                    } else if (arg instanceof String) {
1446                        msg.append("\"").append((String)arg).append("\"");
1447                    } else {
1448                        msg.append(arg);
1449                    }
1450                }
1451                msg.append("]");
1452            }
1453            if (mException != null) {
1454                msg.append(", exception=\"").append(mException.getMessage()).append("\"");
1455            }
1456        }
1457
1458        private String getStatus() {
1459            if (!mFinished) {
1460                return "running";
1461            }
1462            return mException != null ? "failed" : "succeeded";
1463        }
1464
1465        private String getFormattedStartTime() {
1466            return sDateFormat.format(new Date(mStartTime));
1467        }
1468    }
1469}
1470