OSDN Git Service

Added setIdleConnectionTimeout method
authorFyodor Kupolov <fkupolov@google.com>
Tue, 25 Jul 2017 21:17:33 +0000 (14:17 -0700)
committerFyodor Kupolov <fkupolov@google.com>
Mon, 31 Jul 2017 18:05:57 +0000 (11:05 -0700)
It allows apps to set time SQLite connection is allowed to be idle
before it is closed and removed from the pool.

Test: manual + DatabaseGeneralTest
Bug: 63398887
Change-Id: Ie09eeb4dc2b9e52ba67d9355b1f9bd869b148613

api/current.txt
api/system-current.txt
api/test-current.txt
core/java/android/database/sqlite/SQLiteConnectionPool.java
core/java/android/database/sqlite/SQLiteDatabase.java
core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java
core/java/android/database/sqlite/SQLiteGlobal.java
core/java/android/database/sqlite/SQLiteOpenHelper.java
core/res/res/values/config.xml
core/res/res/values/symbols.xml
core/tests/coretests/src/android/database/DatabaseGeneralTest.java

index 7a2cecd..e186b1f 100644 (file)
@@ -11956,6 +11956,7 @@ package android.database.sqlite {
   public static final class SQLiteDatabase.OpenParams {
     method public android.database.sqlite.SQLiteDatabase.CursorFactory getCursorFactory();
     method public android.database.DatabaseErrorHandler getErrorHandler();
+    method public long getIdleConnectionTimeout();
     method public int getLookasideSlotCount();
     method public int getLookasideSlotSize();
     method public int getOpenFlags();
@@ -11969,6 +11970,7 @@ package android.database.sqlite {
     method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder removeOpenFlags(int);
     method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder setCursorFactory(android.database.sqlite.SQLiteDatabase.CursorFactory);
     method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder setErrorHandler(android.database.DatabaseErrorHandler);
+    method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder setIdleConnectionTimeout(long);
     method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder setLookasideConfig(int, int);
     method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder setOpenFlags(int);
   }
@@ -12026,6 +12028,7 @@ package android.database.sqlite {
     method public void onDowngrade(android.database.sqlite.SQLiteDatabase, int, int);
     method public void onOpen(android.database.sqlite.SQLiteDatabase);
     method public abstract void onUpgrade(android.database.sqlite.SQLiteDatabase, int, int);
+    method public void setIdleConnectionTimeout(long);
     method public void setLookasideConfig(int, int);
     method public void setWriteAheadLoggingEnabled(boolean);
   }
index 27d3ee4..755bc98 100644 (file)
@@ -12751,6 +12751,7 @@ package android.database.sqlite {
   public static final class SQLiteDatabase.OpenParams {
     method public android.database.sqlite.SQLiteDatabase.CursorFactory getCursorFactory();
     method public android.database.DatabaseErrorHandler getErrorHandler();
+    method public long getIdleConnectionTimeout();
     method public int getLookasideSlotCount();
     method public int getLookasideSlotSize();
     method public int getOpenFlags();
@@ -12764,6 +12765,7 @@ package android.database.sqlite {
     method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder removeOpenFlags(int);
     method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder setCursorFactory(android.database.sqlite.SQLiteDatabase.CursorFactory);
     method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder setErrorHandler(android.database.DatabaseErrorHandler);
+    method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder setIdleConnectionTimeout(long);
     method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder setLookasideConfig(int, int);
     method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder setOpenFlags(int);
   }
@@ -12821,6 +12823,7 @@ package android.database.sqlite {
     method public void onDowngrade(android.database.sqlite.SQLiteDatabase, int, int);
     method public void onOpen(android.database.sqlite.SQLiteDatabase);
     method public abstract void onUpgrade(android.database.sqlite.SQLiteDatabase, int, int);
+    method public void setIdleConnectionTimeout(long);
     method public void setLookasideConfig(int, int);
     method public void setWriteAheadLoggingEnabled(boolean);
   }
index 2478004..1109476 100644 (file)
@@ -12000,6 +12000,7 @@ package android.database.sqlite {
   public static final class SQLiteDatabase.OpenParams {
     method public android.database.sqlite.SQLiteDatabase.CursorFactory getCursorFactory();
     method public android.database.DatabaseErrorHandler getErrorHandler();
+    method public long getIdleConnectionTimeout();
     method public int getLookasideSlotCount();
     method public int getLookasideSlotSize();
     method public int getOpenFlags();
@@ -12013,6 +12014,7 @@ package android.database.sqlite {
     method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder removeOpenFlags(int);
     method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder setCursorFactory(android.database.sqlite.SQLiteDatabase.CursorFactory);
     method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder setErrorHandler(android.database.DatabaseErrorHandler);
+    method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder setIdleConnectionTimeout(long);
     method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder setLookasideConfig(int, int);
     method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder setOpenFlags(int);
   }
@@ -12095,6 +12097,7 @@ package android.database.sqlite {
     method public void onDowngrade(android.database.sqlite.SQLiteDatabase, int, int);
     method public void onOpen(android.database.sqlite.SQLiteDatabase);
     method public abstract void onUpgrade(android.database.sqlite.SQLiteDatabase, int, int);
+    method public void setIdleConnectionTimeout(long);
     method public void setLookasideConfig(int, int);
     method public void setWriteAheadLoggingEnabled(boolean);
   }
index 765f27e..fc4c5f2 100644 (file)
@@ -16,7 +16,6 @@
 
 package android.database.sqlite;
 
-import android.app.ActivityManager;
 import android.database.sqlite.SQLiteDebug.DbStats;
 import android.os.CancellationSignal;
 import android.os.Handler;
@@ -24,7 +23,6 @@ import android.os.Looper;
 import android.os.Message;
 import android.os.OperationCanceledException;
 import android.os.SystemClock;
-import android.os.SystemProperties;
 import android.util.Log;
 import android.util.PrefixPrinter;
 import android.util.Printer;
@@ -84,15 +82,6 @@ public final class SQLiteConnectionPool implements Closeable {
     // and logging a message about the connection pool being busy.
     private static final long CONNECTION_POOL_BUSY_MILLIS = 30 * 1000; // 30 seconds
 
-    // TODO b/63398887 Move to SQLiteGlobal
-    private static final long IDLE_CONNECTION_CLOSE_DELAY_MILLIS = SystemProperties
-            .getInt("persist.debug.sqlite.idle_connection_close_delay", 30000);
-
-    // TODO b/63398887 STOPSHIP.
-    // Temporarily enabled for testing across a broader set of dogfood devices.
-    private static final boolean CLOSE_IDLE_CONNECTIONS = SystemProperties
-            .getBoolean("persist.debug.sqlite.close_idle_connections", true);
-
     private final CloseGuard mCloseGuard = CloseGuard.get();
 
     private final Object mLock = new Object();
@@ -167,16 +156,12 @@ public final class SQLiteConnectionPool implements Closeable {
 
     private SQLiteConnectionPool(SQLiteDatabaseConfiguration configuration) {
         mConfiguration = new SQLiteDatabaseConfiguration(configuration);
-        // Disable lookaside allocator on low-RAM devices
-        if (ActivityManager.isLowRamDeviceStatic()) {
-            mConfiguration.lookasideSlotCount = 0;
-            mConfiguration.lookasideSlotSize = 0;
-        }
         setMaxConnectionPoolSizeLocked();
-
-        // Do not close idle connections for in-memory databases
-        if (CLOSE_IDLE_CONNECTIONS && !configuration.isInMemoryDb()) {
-            setupIdleConnectionHandler(Looper.getMainLooper(), IDLE_CONNECTION_CLOSE_DELAY_MILLIS);
+        // If timeout is set, setup idle connection handler
+        // In case of MAX_VALUE - idle connections are never closed
+        if (mConfiguration.idleConnectionTimeoutMs != Long.MAX_VALUE) {
+            setupIdleConnectionHandler(Looper.getMainLooper(),
+                    mConfiguration.idleConnectionTimeoutMs);
         }
     }
 
@@ -214,6 +199,12 @@ public final class SQLiteConnectionPool implements Closeable {
         // This might throw if the database is corrupt.
         mAvailablePrimaryConnection = openConnectionLocked(mConfiguration,
                 true /*primaryConnection*/); // might throw
+        // Mark it released so it can be closed after idle timeout
+        synchronized (mLock) {
+            if (mIdleConnectionHandler != null) {
+                mIdleConnectionHandler.connectionReleased(mAvailablePrimaryConnection);
+            }
+        }
 
         // Mark the pool as being open for business.
         mIsOpen = true;
@@ -1021,12 +1012,12 @@ public final class SQLiteConnectionPool implements Closeable {
     }
 
     /**
-     * Set up the handler based on the provided looper and delay.
+     * Set up the handler based on the provided looper and timeout.
      */
     @VisibleForTesting
-    public void setupIdleConnectionHandler(Looper looper, long delayMs) {
+    public void setupIdleConnectionHandler(Looper looper, long timeoutMs) {
         synchronized (mLock) {
-            mIdleConnectionHandler = new IdleConnectionHandler(looper, delayMs);
+            mIdleConnectionHandler = new IdleConnectionHandler(looper, timeoutMs);
         }
     }
 
@@ -1087,6 +1078,10 @@ public final class SQLiteConnectionPool implements Closeable {
                 printer.println("  Lookaside config: sz=" + mConfiguration.lookasideSlotSize
                         + " cnt=" + mConfiguration.lookasideSlotCount);
             }
+            if (mConfiguration.idleConnectionTimeoutMs != Long.MAX_VALUE) {
+                printer.println(
+                        "  Idle connection timeout: " + mConfiguration.idleConnectionTimeoutMs);
+            }
             printer.println("  Available primary connection:");
             if (mAvailablePrimaryConnection != null) {
                 mAvailablePrimaryConnection.dump(indentedPrinter, verbose);
@@ -1153,11 +1148,11 @@ public final class SQLiteConnectionPool implements Closeable {
     }
 
     private class IdleConnectionHandler extends Handler {
-        private final long mDelay;
+        private final long mTimeout;
 
-        IdleConnectionHandler(Looper looper, long delay) {
+        IdleConnectionHandler(Looper looper, long timeout) {
             super(looper);
-            mDelay = delay;
+            mTimeout = timeout;
         }
 
         @Override
@@ -1170,14 +1165,14 @@ public final class SQLiteConnectionPool implements Closeable {
                 if (closeAvailableConnectionLocked(msg.what)) {
                     if (Log.isLoggable(TAG, Log.DEBUG)) {
                         Log.d(TAG, "Closed idle connection " + mConfiguration.label + " " + msg.what
-                                + " after " + mDelay);
+                                + " after " + mTimeout);
                     }
                 }
             }
         }
 
         void connectionReleased(SQLiteConnection con) {
-            sendEmptyMessageDelayed(con.getConnectionId(), mDelay);
+            sendEmptyMessageDelayed(con.getConnectionId(), mTimeout);
         }
 
         void connectionAcquired(SQLiteConnection con) {
index af6df15..5b6efd4 100644 (file)
@@ -20,6 +20,7 @@ import android.annotation.IntDef;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.ActivityManager;
 import android.content.ContentValues;
 import android.database.Cursor;
 import android.database.DatabaseErrorHandler;
@@ -30,6 +31,7 @@ import android.database.sqlite.SQLiteDebug.DbStats;
 import android.os.CancellationSignal;
 import android.os.Looper;
 import android.os.OperationCanceledException;
+import android.os.SystemProperties;
 import android.text.TextUtils;
 import android.util.EventLog;
 import android.util.Log;
@@ -77,21 +79,21 @@ public final class SQLiteDatabase extends SQLiteClosable {
 
     private static final int EVENT_DB_CORRUPT = 75004;
 
+    // TODO b/63398887 STOPSHIP.
+    // Temporarily enabled for testing across a broader set of dogfood devices.
+    private static final boolean DEBUG_CLOSE_IDLE_CONNECTIONS = SystemProperties
+            .getBoolean("persist.debug.sqlite.close_idle_connections", true);
+
     // Stores reference to all databases opened in the current process.
     // (The referent Object is not used at this time.)
     // INVARIANT: Guarded by sActiveDatabases.
-    private static WeakHashMap<SQLiteDatabase, Object> sActiveDatabases =
-            new WeakHashMap<SQLiteDatabase, Object>();
+    private static WeakHashMap<SQLiteDatabase, Object> sActiveDatabases = new WeakHashMap<>();
 
     // Thread-local for database sessions that belong to this database.
     // Each thread has its own database session.
     // INVARIANT: Immutable.
-    private final ThreadLocal<SQLiteSession> mThreadSession = new ThreadLocal<SQLiteSession>() {
-        @Override
-        protected SQLiteSession initialValue() {
-            return createSession();
-        }
-    };
+    private final ThreadLocal<SQLiteSession> mThreadSession = ThreadLocal
+            .withInitial(this::createSession);
 
     // The optional factory to use when creating new Cursors.  May be null.
     // INVARIANT: Immutable.
@@ -261,12 +263,29 @@ public final class SQLiteDatabase extends SQLiteClosable {
 
     private SQLiteDatabase(final String path, final int openFlags,
             CursorFactory cursorFactory, DatabaseErrorHandler errorHandler,
-            int lookasideSlotSize, int lookasideSlotCount) {
+            int lookasideSlotSize, int lookasideSlotCount, long idleConnectionTimeoutMs) {
         mCursorFactory = cursorFactory;
         mErrorHandler = errorHandler != null ? errorHandler : new DefaultDatabaseErrorHandler();
         mConfigurationLocked = new SQLiteDatabaseConfiguration(path, openFlags);
         mConfigurationLocked.lookasideSlotSize = lookasideSlotSize;
         mConfigurationLocked.lookasideSlotCount = lookasideSlotCount;
+        // Disable lookaside allocator on low-RAM devices
+        if (ActivityManager.isLowRamDeviceStatic()) {
+            mConfigurationLocked.lookasideSlotCount = 0;
+            mConfigurationLocked.lookasideSlotSize = 0;
+        }
+        long effectiveTimeoutMs = Long.MAX_VALUE;
+        // Never close idle connections for in-memory databases
+        if (!mConfigurationLocked.isInMemoryDb()) {
+            // First, check app-specific value. Otherwise use defaults
+            // -1 in idleConnectionTimeoutMs indicates unset value
+            if (idleConnectionTimeoutMs >= 0) {
+                effectiveTimeoutMs = idleConnectionTimeoutMs;
+            } else if (DEBUG_CLOSE_IDLE_CONNECTIONS) {
+                effectiveTimeoutMs = SQLiteGlobal.getIdleConnectionTimeout();
+            }
+        }
+        mConfigurationLocked.idleConnectionTimeoutMs = effectiveTimeoutMs;
     }
 
     @Override
@@ -694,7 +713,8 @@ public final class SQLiteDatabase extends SQLiteClosable {
         Preconditions.checkArgument(openParams != null, "OpenParams cannot be null");
         SQLiteDatabase db = new SQLiteDatabase(path, openParams.mOpenFlags,
                 openParams.mCursorFactory, openParams.mErrorHandler,
-                openParams.mLookasideSlotSize, openParams.mLookasideSlotCount);
+                openParams.mLookasideSlotSize, openParams.mLookasideSlotCount,
+                openParams.mIdleConnectionTimeout);
         db.open();
         return db;
     }
@@ -720,7 +740,7 @@ public final class SQLiteDatabase extends SQLiteClosable {
      */
     public static SQLiteDatabase openDatabase(@NonNull String path, @Nullable CursorFactory factory,
             @DatabaseOpenFlags int flags, @Nullable DatabaseErrorHandler errorHandler) {
-        SQLiteDatabase db = new SQLiteDatabase(path, flags, factory, errorHandler, -1, -1);
+        SQLiteDatabase db = new SQLiteDatabase(path, flags, factory, errorHandler, -1, -1, -1);
         db.open();
         return db;
     }
@@ -2267,14 +2287,17 @@ public final class SQLiteDatabase extends SQLiteClosable {
         private final DatabaseErrorHandler mErrorHandler;
         private final int mLookasideSlotSize;
         private final int mLookasideSlotCount;
+        private long mIdleConnectionTimeout;
 
         private OpenParams(int openFlags, CursorFactory cursorFactory,
-                DatabaseErrorHandler errorHandler, int lookasideSlotSize, int lookasideSlotCount) {
+                DatabaseErrorHandler errorHandler, int lookasideSlotSize, int lookasideSlotCount,
+                long idleConnectionTimeout) {
             mOpenFlags = openFlags;
             mCursorFactory = cursorFactory;
             mErrorHandler = errorHandler;
             mLookasideSlotSize = lookasideSlotSize;
             mLookasideSlotCount = lookasideSlotCount;
+            mIdleConnectionTimeout = idleConnectionTimeout;
         }
 
         /**
@@ -2330,6 +2353,17 @@ public final class SQLiteDatabase extends SQLiteClosable {
         }
 
         /**
+         * Returns maximum number of milliseconds that SQLite connection is allowed to be idle
+         * before it is closed and removed from the pool.
+         * <p>If the value isn't set, the timeout defaults to the system wide timeout
+         *
+         * @return timeout in milliseconds or -1 if the value wasn't set.
+         */
+        public long getIdleConnectionTimeout() {
+            return mIdleConnectionTimeout;
+        }
+
+        /**
          * Creates a new instance of builder {@link Builder#Builder(OpenParams) initialized} with
          * {@code this} parameters.
          * @hide
@@ -2345,6 +2379,7 @@ public final class SQLiteDatabase extends SQLiteClosable {
         public static final class Builder {
             private int mLookasideSlotSize = -1;
             private int mLookasideSlotCount = -1;
+            private long mIdleConnectionTimeout = -1;
             private int mOpenFlags;
             private CursorFactory mCursorFactory;
             private DatabaseErrorHandler mErrorHandler;
@@ -2474,13 +2509,29 @@ public final class SQLiteDatabase extends SQLiteClosable {
             }
 
             /**
+             * Sets the maximum number of milliseconds that SQLite connection is allowed to be idle
+             * before it is closed and removed from the pool.
+             *
+             * @param idleConnectionTimeoutMs timeout in milliseconds. Use {@link Long#MAX_VALUE}
+             * to allow unlimited idle connections.
+             */
+            @NonNull
+            public Builder setIdleConnectionTimeout(
+                    @IntRange(from = 0) long idleConnectionTimeoutMs) {
+                Preconditions.checkArgument(idleConnectionTimeoutMs >= 0,
+                        "idle connection timeout cannot be negative");
+                mIdleConnectionTimeout = idleConnectionTimeoutMs;
+                return this;
+            }
+
+            /**
              * Creates an instance of {@link OpenParams} with the options that were previously set
              * on this builder
              */
             @NonNull
             public OpenParams build() {
                 return new OpenParams(mOpenFlags, mCursorFactory, mErrorHandler, mLookasideSlotSize,
-                        mLookasideSlotCount);
+                        mLookasideSlotCount, mIdleConnectionTimeout);
             }
         }
     }
index 7f09b73..34c9b33 100644 (file)
@@ -94,14 +94,21 @@ public final class SQLiteDatabaseConfiguration {
      *
      * <p>If negative, the default lookaside configuration will be used
      */
-    public int lookasideSlotSize;
+    public int lookasideSlotSize = -1;
 
     /**
      * The total number of lookaside memory slots per database connection
      *
      * <p>If negative, the default lookaside configuration will be used
      */
-    public int lookasideSlotCount;
+    public int lookasideSlotCount = -1;
+
+    /**
+     * The number of milliseconds that SQLite connection is allowed to be idle before it
+     * is closed and removed from the pool.
+     * <p>By default, idle connections are not closed
+     */
+    public long idleConnectionTimeoutMs = Long.MAX_VALUE;
 
     /**
      * Creates a database configuration with the required parameters for opening a
@@ -122,8 +129,6 @@ public final class SQLiteDatabaseConfiguration {
         // Set default values for optional parameters.
         maxSqlCacheSize = 25;
         locale = Locale.getDefault();
-        lookasideSlotSize = -1;
-        lookasideSlotCount = -1;
     }
 
     /**
@@ -164,6 +169,7 @@ public final class SQLiteDatabaseConfiguration {
         customFunctions.addAll(other.customFunctions);
         lookasideSlotSize = other.lookasideSlotSize;
         lookasideSlotCount = other.lookasideSlotCount;
+        idleConnectionTimeoutMs = other.idleConnectionTimeoutMs;
     }
 
     /**
index 922d11b..571656a 100644 (file)
@@ -124,4 +124,15 @@ public final class SQLiteGlobal {
                 com.android.internal.R.integer.db_connection_pool_size));
         return Math.max(2, value);
     }
+
+    /**
+     * The default number of milliseconds that SQLite connection is allowed to be idle before it
+     * is closed and removed from the pool.
+     */
+    public static int getIdleConnectionTimeout() {
+        return SystemProperties.getInt("debug.sqlite.idle_connection_timeout",
+                Resources.getSystem().getInteger(
+                        com.android.internal.R.integer.db_default_idle_connection_timeout));
+    }
+
 }
index c19db82..dfaf714 100644 (file)
@@ -195,6 +195,26 @@ public abstract class SQLiteOpenHelper {
     }
 
     /**
+     * Sets the maximum number of milliseconds that SQLite connection is allowed to be idle
+     * before it is closed and removed from the pool.
+     *
+     * <p>This method should be called from the constructor of the subclass,
+     * before opening the database
+     *
+     * @param idleConnectionTimeoutMs timeout in milliseconds. Use {@link Long#MAX_VALUE} value
+     * to allow unlimited idle connections.
+     */
+    public void setIdleConnectionTimeout(@IntRange(from = 0) final long idleConnectionTimeoutMs) {
+        synchronized (this) {
+            if (mDatabase != null && mDatabase.isOpen()) {
+                throw new IllegalStateException(
+                        "Connection timeout setting cannot be changed after opening the database");
+            }
+            mOpenParamsBuilder.setIdleConnectionTimeout(idleConnectionTimeoutMs);
+        }
+    }
+
+    /**
      * Create and/or open a database that will be used for reading and writing.
      * The first time this is called, the database will be opened and
      * {@link #onCreate}, {@link #onUpgrade} and/or {@link #onOpen} will be
index 27d55ba..81ab29b 100644 (file)
          The size of the WAL file is also constrained by 'db_journal_size_limit'. -->
     <integer name="db_wal_autocheckpoint">100</integer>
 
+    <!-- The number of milliseconds that SQLite connection is allowed to be idle before it
+         is closed and removed from the pool -->
+    <integer name="db_default_idle_connection_timeout">30000</integer>
+
     <!-- Max space (in MB) allocated to DownloadManager to store the downloaded
          files if they are to be stored in DownloadManager's data dir,
          which typically is /data/data/com.android.providers.downloads/files -->
index a8aba0b..b6603f3 100644 (file)
   <java-symbol type="integer" name="db_connection_pool_size" />
   <java-symbol type="integer" name="db_journal_size_limit" />
   <java-symbol type="integer" name="db_wal_autocheckpoint" />
+  <java-symbol type="integer" name="db_default_idle_connection_timeout" />
   <java-symbol type="integer" name="config_soundEffectVolumeDb" />
   <java-symbol type="integer" name="config_lockSoundVolumeDb" />
   <java-symbol type="integer" name="config_multiuserMaximumUsers" />
index 7800f4a..f97d51d 100644 (file)
@@ -25,6 +25,8 @@ import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteDebug;
 import android.database.sqlite.SQLiteException;
 import android.os.Parcel;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.UiDevice;
 import android.test.AndroidTestCase;
 import android.test.PerformanceTestCase;
 import android.test.suitebuilder.annotation.LargeTest;
@@ -1185,4 +1187,38 @@ public class DatabaseGeneralTest extends AndroidTestCase implements PerformanceT
             fail("unexpected");
         }
     }
+
+    @MediumTest
+    public void testCloseIdleConnection() throws Exception {
+        mDatabase.close();
+        SQLiteDatabase.OpenParams params = new SQLiteDatabase.OpenParams.Builder()
+                .setIdleConnectionTimeout(1000).build();
+        mDatabase = SQLiteDatabase.openDatabase(mDatabaseFile.getPath(), params);
+        // Wait a bit and check that connection is still open
+        Thread.sleep(100);
+        String output = executeShellCommand("dumpsys dbinfo " + getContext().getPackageName());
+        assertTrue("Connection #0 should be open. Output: " + output,
+                output.contains("Connection #0:"));
+
+        // Now cause idle timeout and check that connection is closed
+        Thread.sleep(1000);
+        output = executeShellCommand("dumpsys dbinfo " + getContext().getPackageName());
+        assertFalse("Connection #0 should be closed. Output: " + output,
+                output.contains("Connection #0:"));
+    }
+
+    @SmallTest
+    public void testSetIdleConnectionTimeoutValidation() throws Exception {
+        try {
+            new SQLiteDatabase.OpenParams.Builder().setIdleConnectionTimeout(-1).build();
+            fail("Negative timeout should be rejected");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    private String executeShellCommand(String cmd) throws Exception {
+        return UiDevice.getInstance(
+                InstrumentationRegistry.getInstrumentation()).executeShellCommand(cmd);
+    }
+
 }