Expose SQLite deferred transactions as public API

Bug: 274020993

New public APIs are added to SQLiteDatabase:
beginTransactionDeferred() and beginTransactionWithListenerDeferred().
The listener parameter is nullable, so
beginTransactionWithListenerDeferred(nul) is strictly identical to
beginTransactionDeferred().

To maintain symmetry, existing beginTransactionWithListener*() APIs
have their listener parameter also marked nullable.  See the Anroid
API guidelines for this situation:

go/androidx-api-guidelines#extending-apis-that-are-missing-annotations

A CTS test will be added in a follow-on commit.

Test: atest
 * SQLiteDatabaseTest (from CtsDatabaseTestCases)

Change-Id: Icc94bf5bb058936cfa32d92a91379ddad3cea4b4
diff --git a/core/api/current.txt b/core/api/current.txt
index 3b34f3a..393576b 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -14244,9 +14244,11 @@
 
   public final class SQLiteDatabase extends android.database.sqlite.SQLiteClosable {
     method public void beginTransaction();
+    method public void beginTransactionDeferred();
     method public void beginTransactionNonExclusive();
-    method public void beginTransactionWithListener(android.database.sqlite.SQLiteTransactionListener);
-    method public void beginTransactionWithListenerNonExclusive(android.database.sqlite.SQLiteTransactionListener);
+    method public void beginTransactionWithListener(@Nullable android.database.sqlite.SQLiteTransactionListener);
+    method public void beginTransactionWithListenerDeferred(@Nullable android.database.sqlite.SQLiteTransactionListener);
+    method public void beginTransactionWithListenerNonExclusive(@Nullable android.database.sqlite.SQLiteTransactionListener);
     method public android.database.sqlite.SQLiteStatement compileStatement(String) throws android.database.SQLException;
     method @NonNull public static android.database.sqlite.SQLiteDatabase create(@Nullable android.database.sqlite.SQLiteDatabase.CursorFactory);
     method @NonNull public static android.database.sqlite.SQLiteDatabase createInMemory(@NonNull android.database.sqlite.SQLiteDatabase.OpenParams);
diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java
index c08294f..87fc8c4 100644
--- a/core/java/android/database/sqlite/SQLiteDatabase.java
+++ b/core/java/android/database/sqlite/SQLiteDatabase.java
@@ -675,6 +675,30 @@
     }
 
     /**
+     * Begins a transaction in DEFERRED mode.
+     * <p>
+     * Transactions can be nested. When the outer transaction is ended all of the work done in
+     * that transaction and all of the nested transactions will be committed or rolled back. The
+     * changes will be rolled back if any transaction is ended without being marked as clean (by
+     * calling setTransactionSuccessful). Otherwise they will be committed.
+     * <p>
+     * Here is the standard idiom for transactions:
+     *
+     * <pre>
+     *   db.beginTransactionDeferred();
+     *   try {
+     *     ...
+     *     db.setTransactionSuccessful();
+     *   } finally {
+     *     db.endTransaction();
+     *   }
+     * </pre>
+     */
+    public void beginTransactionDeferred() {
+        beginTransactionWithListenerDeferred(null);
+    }
+
+    /**
      * Begins a transaction in EXCLUSIVE mode.
      * <p>
      * Transactions can be nested.
@@ -699,7 +723,8 @@
      * commits, or is rolled back, either explicitly or by a call to
      * {@link #yieldIfContendedSafely}.
      */
-    public void beginTransactionWithListener(SQLiteTransactionListener transactionListener) {
+    public void beginTransactionWithListener(
+            @Nullable SQLiteTransactionListener transactionListener) {
         beginTransaction(transactionListener, true);
     }
 
@@ -728,19 +753,53 @@
      *            explicitly or by a call to {@link #yieldIfContendedSafely}.
      */
     public void beginTransactionWithListenerNonExclusive(
-            SQLiteTransactionListener transactionListener) {
+            @Nullable SQLiteTransactionListener transactionListener) {
         beginTransaction(transactionListener, false);
     }
 
+    /**
+     * Begins a transaction in DEFERRED mode.
+     * <p>
+     * Transactions can be nested. When the outer transaction is ended all of the work done in
+     * that transaction and all of the nested transactions will be committed or rolled back. The
+     * changes will be rolled back if any transaction is ended without being marked as clean (by
+     * calling setTransactionSuccessful). Otherwise they will be committed.
+     * <p>
+     * Here is the standard idiom for transactions:
+     *
+     * <pre>
+     *   db.beginTransactionDeferred();
+     *   try {
+     *     ...
+     *     db.setTransactionSuccessful();
+     *   } finally {
+     *     db.endTransaction();
+     *   }
+     * </pre>
+     */
+    public void beginTransactionWithListenerDeferred(
+            @Nullable SQLiteTransactionListener transactionListener) {
+        beginTransaction(transactionListener, SQLiteSession.TRANSACTION_MODE_DEFERRED);
+    }
+
     @UnsupportedAppUsage
     private void beginTransaction(SQLiteTransactionListener transactionListener,
             boolean exclusive) {
+        beginTransaction(transactionListener,
+                exclusive ? SQLiteSession.TRANSACTION_MODE_EXCLUSIVE :
+                SQLiteSession.TRANSACTION_MODE_IMMEDIATE);
+    }
+
+    /**
+     * Begin a transaction with the specified mode.  Valid modes are
+     * {@link SquLiteSession.TRANSACTION_MODE_DEFERRED},
+     * {@link SquLiteSession.TRANSACTION_MODE_IMMEDIATE}, and
+     * {@link SquLiteSession.TRANSACTION_MODE_EXCLUSIVE}.
+     */
+    private void beginTransaction(@Nullable SQLiteTransactionListener listener, int mode) {
         acquireReference();
         try {
-            getThreadSession().beginTransaction(
-                    exclusive ? SQLiteSession.TRANSACTION_MODE_EXCLUSIVE :
-                            SQLiteSession.TRANSACTION_MODE_IMMEDIATE,
-                    transactionListener,
+            getThreadSession().beginTransaction(mode, listener,
                     getThreadDefaultConnectionFlags(false /*readOnly*/), null);
         } finally {
             releaseReference();
@@ -3113,4 +3172,3 @@
         ContentResolver.onDbCorruption(tag, message, stacktrace);
     }
 }
-
diff --git a/core/java/android/database/sqlite/SQLiteSession.java b/core/java/android/database/sqlite/SQLiteSession.java
index 24b62b8..8811dd4 100644
--- a/core/java/android/database/sqlite/SQLiteSession.java
+++ b/core/java/android/database/sqlite/SQLiteSession.java
@@ -325,7 +325,12 @@
                         mConnection.execute("BEGIN EXCLUSIVE;", null,
                                 cancellationSignal); // might throw
                         break;
+                    case TRANSACTION_MODE_DEFERRED:
+                        mConnection.execute("BEGIN DEFERRED;", null,
+                                cancellationSignal); // might throw
+                        break;
                     default:
+                        // Per SQLite documentation, this executes in DEFERRED mode.
                         mConnection.execute("BEGIN;", null, cancellationSignal); // might throw
                         break;
                 }