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