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