summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/database/sqlite/SQLiteClosable.java22
-rw-r--r--core/java/android/database/sqlite/SQLiteConnectionPool.java7
-rw-r--r--core/java/android/database/sqlite/SQLiteDatabase.java1
-rw-r--r--core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java67
4 files changed, 95 insertions, 2 deletions
diff --git a/core/java/android/database/sqlite/SQLiteClosable.java b/core/java/android/database/sqlite/SQLiteClosable.java
index 8eb512a2cbc6..806c386bf9a7 100644
--- a/core/java/android/database/sqlite/SQLiteClosable.java
+++ b/core/java/android/database/sqlite/SQLiteClosable.java
@@ -31,6 +31,20 @@ public abstract class SQLiteClosable implements Closeable {
private int mReferenceCount = 1;
/**
+ * True if the instance should record when it was closed. Tracking closure can be expensive,
+ * so it is best reserved for subclasses that have long lifetimes.
+ * @hide
+ */
+ protected boolean mTrackClosure = false;
+
+ /**
+ * The caller that finally released this instance. If this is not null, it is supplied as the
+ * cause to the IllegalStateException that is thrown when the object is reopened. Subclasses
+ * are responsible for populating this field, if they wish to use it.
+ */
+ private Throwable mClosedBy = null;
+
+ /**
* Called when the last reference to the object was released by
* a call to {@link #releaseReference()} or {@link #close()}.
*/
@@ -57,7 +71,7 @@ public abstract class SQLiteClosable implements Closeable {
synchronized(this) {
if (mReferenceCount <= 0) {
throw new IllegalStateException(
- "attempt to re-open an already-closed object: " + this);
+ "attempt to re-open an already-closed object: " + this, mClosedBy);
}
mReferenceCount++;
}
@@ -108,5 +122,11 @@ public abstract class SQLiteClosable implements Closeable {
*/
public void close() {
releaseReference();
+ synchronized (this) {
+ if (mTrackClosure && (mClosedBy == null)) {
+ String name = getClass().getName();
+ mClosedBy = new Exception("closed by " + name + ".close()").fillInStackTrace();
+ }
+ }
}
}
diff --git a/core/java/android/database/sqlite/SQLiteConnectionPool.java b/core/java/android/database/sqlite/SQLiteConnectionPool.java
index 15d7d6675c8e..505905f5aa16 100644
--- a/core/java/android/database/sqlite/SQLiteConnectionPool.java
+++ b/core/java/android/database/sqlite/SQLiteConnectionPool.java
@@ -96,6 +96,10 @@ public final class SQLiteConnectionPool implements Closeable {
private boolean mIsOpen;
private int mNextConnectionId;
+ // Record the caller that explicitly closed the database.
+ @GuardedBy("mLock")
+ private Throwable mClosedBy;
+
private ConnectionWaiter mConnectionWaiterPool;
private ConnectionWaiter mConnectionWaiterQueue;
@@ -265,6 +269,7 @@ public final class SQLiteConnectionPool implements Closeable {
throwIfClosedLocked();
mIsOpen = false;
+ mClosedBy = new Exception("SQLiteConnectionPool.close()").fillInStackTrace();
closeAvailableConnectionsAndLogExceptionsLocked();
@@ -1101,7 +1106,7 @@ public final class SQLiteConnectionPool implements Closeable {
private void throwIfClosedLocked() {
if (!mIsOpen) {
throw new IllegalStateException("Cannot perform this operation "
- + "because the connection pool has been closed.");
+ + "because the connection pool has been closed.", mClosedBy);
}
}
diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java
index 60fd0ce59209..8bff624451e7 100644
--- a/core/java/android/database/sqlite/SQLiteDatabase.java
+++ b/core/java/android/database/sqlite/SQLiteDatabase.java
@@ -488,6 +488,7 @@ public final class SQLiteDatabase extends SQLiteClosable {
@Nullable CursorFactory cursorFactory, @Nullable DatabaseErrorHandler errorHandler,
int lookasideSlotSize, int lookasideSlotCount, long idleConnectionTimeoutMs,
@Nullable String journalMode, @Nullable String syncMode) {
+ mTrackClosure = true;
mCursorFactory = cursorFactory;
mErrorHandler = errorHandler != null ? errorHandler : new DefaultDatabaseErrorHandler();
mConfigurationLocked = new SQLiteDatabaseConfiguration(path, openFlags);
diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java
index 00978a099f13..9d477094692a 100644
--- a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java
+++ b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java
@@ -25,6 +25,7 @@ import static org.junit.Assert.fail;
import android.content.Context;
import android.database.Cursor;
import android.database.DatabaseUtils;
+import android.database.DefaultDatabaseErrorHandler;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
@@ -592,4 +593,70 @@ public class SQLiteDatabaseTest {
}
closeAndDeleteDatabase();
}
+
+ @Test
+ public void testCloseCorruptionReport() throws Exception {
+ mDatabase.beginTransaction();
+ try {
+ mDatabase.execSQL("CREATE TABLE t2 (i int, j int);");
+ mDatabase.execSQL("INSERT INTO t2 (i, j) VALUES (2, 20)");
+ mDatabase.execSQL("INSERT INTO t2 (i, j) VALUES (3, 30)");
+ mDatabase.setTransactionSuccessful();
+ } finally {
+ mDatabase.endTransaction();
+ }
+
+ // Start a transaction and announce that the DB is corrupted.
+ DefaultDatabaseErrorHandler errorHandler = new DefaultDatabaseErrorHandler();
+
+ // Do not bother with endTransaction; the database will have been closed in the corruption
+ // handler.
+ mDatabase.beginTransaction();
+ try {
+ errorHandler.onCorruption(mDatabase);
+ mDatabase.execSQL("INSERT INTO t2 (i, j) VALUES (4, 40)");
+ fail("expected an exception");
+ } catch (IllegalStateException e) {
+ final Throwable cause = e.getCause();
+ assertNotNull(cause);
+ boolean found = false;
+ for (StackTraceElement s : cause.getStackTrace()) {
+ if (s.getMethodName().contains("onCorruption")) {
+ found = true;
+ }
+ }
+ assertTrue(found);
+ }
+ }
+
+ @Test
+ public void testCloseReport() throws Exception {
+ mDatabase.beginTransaction();
+ try {
+ mDatabase.execSQL("CREATE TABLE t2 (i int, j int);");
+ mDatabase.execSQL("INSERT INTO t2 (i, j) VALUES (2, 20)");
+ mDatabase.execSQL("INSERT INTO t2 (i, j) VALUES (3, 30)");
+ mDatabase.setTransactionSuccessful();
+ } finally {
+ mDatabase.endTransaction();
+ }
+
+ mDatabase.close();
+ try {
+ // Do not bother with endTransaction; the database has already been close.
+ mDatabase.beginTransaction();
+ fail("expected an exception");
+ } catch (IllegalStateException e) {
+ assertTrue(e.toString().contains("attempt to re-open an already-closed object"));
+ final Throwable cause = e.getCause();
+ assertNotNull(cause);
+ boolean found = false;
+ for (StackTraceElement s : cause.getStackTrace()) {
+ if (s.getMethodName().contains("testCloseReport")) {
+ found = true;
+ }
+ }
+ assertTrue(found);
+ }
+ }
}