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