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