Merge "Add SearchContext#setPersistToDiskRecoveryProof." into androidx-main
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
index af20e81..898e680 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
@@ -3877,17 +3877,27 @@
                 Collections.emptyMap());
         assertThat(getResult).isEqualTo(document);
 
-        // That document should be visible even from another instance.
+        // Inialize a new instance of AppSearch to test initialization.
+        InitializeStats.Builder initStatsBuilder = new InitializeStats.Builder();
         AppSearchImpl appSearchImpl2 = AppSearchImpl.create(
                 mAppSearchDir,
                 new AppSearchConfigImpl(
                         new UnlimitedLimitConfig(),
                         new LocalStorageIcingOptionsConfig()
                 ),
-                /*initStatsBuilder=*/ null,
+                /*initStatsBuilder=*/initStatsBuilder,
                 /*visibilityChecker=*/ null,
                 /*revocableFileDescriptorStore=*/ null,
                 ALWAYS_OPTIMIZE);
+
+        // Initialization should trigger a recovery
+        InitializeStats initStats = initStatsBuilder.build();
+        assertThat(initStats.getDocumentStoreRecoveryCause())
+                .isEqualTo(InitializeStats.RECOVERY_CAUSE_IO_ERROR);
+        assertThat(initStats.getIndexRestorationCause())
+                .isEqualTo(InitializeStats.RECOVERY_CAUSE_IO_ERROR);
+
+        // That document should be visible even from another instance.
         getResult = appSearchImpl2.getDocument("package", "database", "namespace1",
                 "id1",
                 Collections.emptyMap());
@@ -3950,17 +3960,27 @@
                 Collections.emptyMap());
         assertThat(getResult).isEqualTo(document2);
 
-        // Only the second document should be retrievable from another instance.
+        // Inialize a new instance of AppSearch to test initialization.
+        InitializeStats.Builder initStatsBuilder = new InitializeStats.Builder();
         AppSearchImpl appSearchImpl2 = AppSearchImpl.create(
                 mAppSearchDir,
                 new AppSearchConfigImpl(
                         new UnlimitedLimitConfig(),
                         new LocalStorageIcingOptionsConfig()
                 ),
-                /*initStatsBuilder=*/ null,
+                /*initStatsBuilder=*/initStatsBuilder,
                 /*visibilityChecker=*/ null,
                 /*revocableFileDescriptorStore=*/ null,
                 ALWAYS_OPTIMIZE);
+
+        // Initialization should trigger a recovery
+        InitializeStats initStats = initStatsBuilder.build();
+        assertThat(initStats.getDocumentStoreRecoveryCause())
+                .isEqualTo(InitializeStats.RECOVERY_CAUSE_IO_ERROR);
+        assertThat(initStats.getIndexRestorationCause())
+                .isEqualTo(InitializeStats.RECOVERY_CAUSE_IO_ERROR);
+
+        // Only the second document should be retrievable from another instance.
         assertThrows(AppSearchException.class, () -> appSearchImpl2.getDocument("package",
                 "database",
                 "namespace1",
@@ -4030,17 +4050,263 @@
                 Collections.emptyMap());
         assertThat(getResult).isEqualTo(document2);
 
-        // Only the second document should be retrievable from another instance.
+        // Inialize a new instance of AppSearch to test initialization.
+        InitializeStats.Builder initStatsBuilder = new InitializeStats.Builder();
         AppSearchImpl appSearchImpl2 = AppSearchImpl.create(
                 mAppSearchDir,
                 new AppSearchConfigImpl(
                         new UnlimitedLimitConfig(),
                         new LocalStorageIcingOptionsConfig()
                 ),
-                /*initStatsBuilder=*/ null,
+                /*initStatsBuilder=*/initStatsBuilder,
                 /*visibilityChecker=*/ null,
                 /*revocableFileDescriptorStore=*/ null,
                 ALWAYS_OPTIMIZE);
+
+        // Initialization should trigger a recovery
+        InitializeStats initStats = initStatsBuilder.build();
+        assertThat(initStats.getDocumentStoreRecoveryCause())
+                .isEqualTo(InitializeStats.RECOVERY_CAUSE_IO_ERROR);
+        assertThat(initStats.getIndexRestorationCause())
+                .isEqualTo(InitializeStats.RECOVERY_CAUSE_IO_ERROR);
+
+        // Only the second document should be retrievable from another instance.
+        assertThrows(AppSearchException.class, () -> appSearchImpl2.getDocument("package",
+                "database",
+                "namespace1",
+                "id1",
+                Collections.emptyMap()));
+        getResult = appSearchImpl2.getDocument("package", "database", "namespace2",
+                "id2",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document2);
+        appSearchImpl2.close();
+    }
+
+    @Test
+    public void testPutPersistsWithoutRecoveryWithRecoveryProofFlush() throws Exception {
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                /*visibilityConfigs=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+        assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
+
+        // Add a document and persist it.
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace1", "id1", "type").build();
+        mAppSearchImpl.putDocument(
+                "package",
+                "database",
+                document,
+                /*sendChangeNotifications=*/ false,
+                /*logger=*/ null);
+        mAppSearchImpl.persistToDisk(PersistType.Code.RECOVERY_PROOF);
+
+        GenericDocument getResult = mAppSearchImpl.getDocument("package", "database", "namespace1",
+                "id1",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document);
+
+        // Inialize a new instance of AppSearch to test initialization.
+        InitializeStats.Builder initStatsBuilder = new InitializeStats.Builder();
+        AppSearchImpl appSearchImpl2 = AppSearchImpl.create(
+                mAppSearchDir,
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new LocalStorageIcingOptionsConfig()
+                ),
+                /*initStatsBuilder=*/initStatsBuilder,
+                /*visibilityChecker=*/ null,
+                /*revocableFileDescriptorStore=*/ null,
+                ALWAYS_OPTIMIZE);
+
+        // Initialization should NOT trigger a recovery
+        InitializeStats initStats = initStatsBuilder.build();
+        assertThat(initStats.getDocumentStoreRecoveryCause())
+                .isEqualTo(InitializeStats.RECOVERY_CAUSE_NONE);
+        assertThat(initStats.getIndexRestorationCause())
+                .isEqualTo(InitializeStats.RECOVERY_CAUSE_NONE);
+
+        // That document should be visible even from another instance.
+        getResult = appSearchImpl2.getDocument("package", "database", "namespace1",
+                "id1",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document);
+        appSearchImpl2.close();
+    }
+
+    @Test
+    public void testDeletePersistsWithoutRecoveryWithRecoveryProofFlush() throws Exception {
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                /*visibilityConfigs=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+        assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
+
+        // Add two documents and persist them.
+        GenericDocument document1 =
+                new GenericDocument.Builder<>("namespace1", "id1", "type").build();
+        mAppSearchImpl.putDocument(
+                "package",
+                "database",
+                document1,
+                /*sendChangeNotifications=*/ false,
+                /*logger=*/ null);
+        GenericDocument document2 =
+                new GenericDocument.Builder<>("namespace1", "id2", "type").build();
+        mAppSearchImpl.putDocument(
+                "package",
+                "database",
+                document2,
+                /*sendChangeNotifications=*/ false,
+                /*logger=*/ null);
+        mAppSearchImpl.persistToDisk(PersistType.Code.RECOVERY_PROOF);
+
+        GenericDocument getResult = mAppSearchImpl.getDocument("package", "database", "namespace1",
+                "id1",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document1);
+        getResult = mAppSearchImpl.getDocument("package", "database", "namespace1",
+                "id2",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document2);
+
+        // Delete the first document
+        mAppSearchImpl.remove("package", "database", "namespace1", "id1", /*statsBuilder=*/ null);
+        mAppSearchImpl.persistToDisk(PersistType.Code.RECOVERY_PROOF);
+        assertThrows(AppSearchException.class, () -> mAppSearchImpl.getDocument("package",
+                "database",
+                "namespace1",
+                "id1",
+                Collections.emptyMap()));
+        getResult = mAppSearchImpl.getDocument("package", "database", "namespace1",
+                "id2",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document2);
+
+        // Inialize a new instance of AppSearch to test initialization.
+        InitializeStats.Builder initStatsBuilder = new InitializeStats.Builder();
+        AppSearchImpl appSearchImpl2 = AppSearchImpl.create(
+                mAppSearchDir,
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new LocalStorageIcingOptionsConfig()
+                ),
+                /*initStatsBuilder=*/initStatsBuilder,
+                /*visibilityChecker=*/ null,
+                /*revocableFileDescriptorStore=*/ null,
+                ALWAYS_OPTIMIZE);
+
+        // Initialization should NOT trigger a recovery.
+        InitializeStats initStats = initStatsBuilder.build();
+        assertThat(initStats.getDocumentStoreRecoveryCause())
+                .isEqualTo(InitializeStats.RECOVERY_CAUSE_NONE);
+        assertThat(initStats.getIndexRestorationCause())
+                .isEqualTo(InitializeStats.RECOVERY_CAUSE_NONE);
+
+        // Only the second document should be retrievable from another instance.
+        assertThrows(AppSearchException.class, () -> appSearchImpl2.getDocument("package",
+                "database",
+                "namespace1",
+                "id1",
+                Collections.emptyMap()));
+        getResult = appSearchImpl2.getDocument("package", "database", "namespace1",
+                "id2",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document2);
+        appSearchImpl2.close();
+    }
+
+    @Test
+    public void testDeleteByQueryPersistsWithoutRecoveryWithRecoveryProofFlush() throws Exception {
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                /*visibilityConfigs=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+        assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
+
+        // Add two documents and persist them.
+        GenericDocument document1 =
+                new GenericDocument.Builder<>("namespace1", "id1", "type").build();
+        mAppSearchImpl.putDocument(
+                "package",
+                "database",
+                document1,
+                /*sendChangeNotifications=*/ false,
+                /*logger=*/ null);
+        GenericDocument document2 =
+                new GenericDocument.Builder<>("namespace2", "id2", "type").build();
+        mAppSearchImpl.putDocument(
+                "package",
+                "database",
+                document2,
+                /*sendChangeNotifications=*/ false,
+                /*logger=*/ null);
+        mAppSearchImpl.persistToDisk(PersistType.Code.RECOVERY_PROOF);
+
+        GenericDocument getResult = mAppSearchImpl.getDocument("package", "database", "namespace1",
+                "id1",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document1);
+        getResult = mAppSearchImpl.getDocument("package", "database", "namespace2",
+                "id2",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document2);
+
+        // Delete the first document
+        mAppSearchImpl.removeByQuery("package", "database", "",
+                new SearchSpec.Builder().addFilterNamespaces("namespace1").setTermMatch(
+                        SearchSpec.TERM_MATCH_EXACT_ONLY).build(), /*statsBuilder=*/ null);
+        mAppSearchImpl.persistToDisk(PersistType.Code.RECOVERY_PROOF);
+        assertThrows(AppSearchException.class, () -> mAppSearchImpl.getDocument("package",
+                "database",
+                "namespace1",
+                "id1",
+                Collections.emptyMap()));
+        getResult = mAppSearchImpl.getDocument("package", "database", "namespace2",
+                "id2",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document2);
+
+        // Initialize a new instance of AppSearch to test initialization.
+        InitializeStats.Builder initStatsBuilder = new InitializeStats.Builder();
+        AppSearchImpl appSearchImpl2 = AppSearchImpl.create(
+                mAppSearchDir,
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new LocalStorageIcingOptionsConfig()
+                ),
+                /*initStatsBuilder=*/initStatsBuilder,
+                /*visibilityChecker=*/ null,
+                /*revocableFileDescriptorStore=*/ null,
+                ALWAYS_OPTIMIZE);
+
+        // Initialization should NOT trigger a recovery.
+        InitializeStats initStats = initStatsBuilder.build();
+        assertThat(initStats.getDocumentStoreRecoveryCause())
+                .isEqualTo(InitializeStats.RECOVERY_CAUSE_NONE);
+        assertThat(initStats.getIndexRestorationCause())
+                .isEqualTo(InitializeStats.RECOVERY_CAUSE_NONE);
+
+        // Only the second document should be retrievable from another instance.
         assertThrows(AppSearchException.class, () -> appSearchImpl2.getDocument("package",
                 "database",
                 "namespace1",
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/LocalStorageTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/LocalStorageTest.java
index 333cea3..18f8abe 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/LocalStorageTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/LocalStorageTest.java
@@ -20,8 +20,11 @@
 
 import static org.junit.Assert.assertThrows;
 
+import androidx.appsearch.exceptions.AppSearchException;
 import androidx.test.core.app.ApplicationProvider;
 
+import com.google.android.icing.proto.PersistType;
+
 import org.junit.Test;
 
 import java.util.concurrent.Executor;
@@ -30,11 +33,15 @@
 public class LocalStorageTest {
     @Test
     public void testSameInstance() throws Exception {
+        LocalStorage.resetInstance();
+
         Executor executor = Executors.newCachedThreadPool();
         LocalStorage b1 = LocalStorage.getOrCreateInstance(
-                ApplicationProvider.getApplicationContext(), executor, /*logger=*/ null);
+                ApplicationProvider.getApplicationContext(), executor, /*logger=*/ null,
+                /* persistToDiskRecoveryProof=*/false);
         LocalStorage b2 = LocalStorage.getOrCreateInstance(
-                ApplicationProvider.getApplicationContext(), executor, /*logger=*/ null);
+                ApplicationProvider.getApplicationContext(), executor, /*logger=*/ null,
+                /* persistToDiskRecoveryProof=*/false);
         assertThat(b1).isSameInstanceAs(b2);
     }
 
@@ -88,4 +95,31 @@
                         "/testDatabaseNameStartWith").build());
         assertThat(e).hasMessageThat().isEqualTo("Database name cannot contain '/'");
     }
+
+    @Test
+    public void testLocalStorage_persistToDiskRecoveryProofTrue() throws AppSearchException {
+        LocalStorage.resetInstance();
+
+        LocalStorage.SearchContext searchContext =
+                new LocalStorage.SearchContext.Builder(
+                        ApplicationProvider.getApplicationContext(),
+                        /*databaseName=*/"dbName").setPersistToDiskRecoveryProof(true).build();
+
+        AppSearchConfig config = LocalStorage.getConfig(searchContext);
+        assertThat(config.getLightweightPersistType()).isEqualTo(PersistType.Code.RECOVERY_PROOF);
+    }
+
+    @Test
+    public void testLocalStorage_persistToDiskRecoveryProofFalse() throws AppSearchException {
+        LocalStorage.resetInstance();
+
+        // persistToDiskRecoveryProof is false by default.
+        LocalStorage.SearchContext searchContext =
+                new LocalStorage.SearchContext.Builder(
+                        ApplicationProvider.getApplicationContext(),
+                        /*databaseName=*/"dbName").build();
+
+        AppSearchConfig config = LocalStorage.getConfig(searchContext);
+        assertThat(config.getLightweightPersistType()).isEqualTo(PersistType.Code.LITE);
+    }
 }
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
index 94d80452..cfb30c8 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
@@ -436,14 +436,16 @@
                         new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                                 new LocalStorageIcingOptionsConfig(),
                                 /* storeParentInfoAsSyntheticProperty= */ false,
-                                /* shouldRetrieveParentInfo= */ true));
+                                /* shouldRetrieveParentInfo= */ true,
+                                /* persistToDiskRecoveryProof=*/false));
         GenericDocument actualDocWithParentAsSyntheticProperty =
                 GenericDocumentToProtoConverter.toGenericDocument(documentProto, PREFIX,
                         new SchemaCache(schemaMap),
                         new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                                 new LocalStorageIcingOptionsConfig(),
                                 /* storeParentInfoAsSyntheticProperty= */ true,
-                                /* shouldRetrieveParentInfo= */ true));
+                                /* shouldRetrieveParentInfo= */ true,
+                                /* persistToDiskRecoveryProof=*/false));
 
         assertThat(actualDocWithParentAsMetaField).isEqualTo(expectedDocWithParentAsMetaField);
         assertThat(actualDocWithParentAsMetaField).isNotEqualTo(
@@ -505,14 +507,16 @@
                         new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                                 new LocalStorageIcingOptionsConfig(),
                                 /* storeParentInfoAsSyntheticProperty= */ false,
-                                /* shouldRetrieveParentInfo= */ true));
+                                /* shouldRetrieveParentInfo= */ true,
+                                /* persistToDiskRecoveryProof=*/false));
         GenericDocument actualDoc2 =
                 GenericDocumentToProtoConverter.toGenericDocument(documentProto, PREFIX,
                         new SchemaCache(schemaMap),
                         new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                                 new LocalStorageIcingOptionsConfig(),
                                 /* storeParentInfoAsSyntheticProperty= */ true,
-                                /* shouldRetrieveParentInfo= */ true));
+                                /* shouldRetrieveParentInfo= */ true,
+                                /* persistToDiskRecoveryProof=*/false));
         assertThat(actualDoc1).isEqualTo(expectedDoc);
         assertThat(actualDoc2).isEqualTo(expectedDoc);
     }
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java
index 1576821..fb950a0 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java
@@ -59,7 +59,8 @@
                 new UnlimitedLimitConfig(),
                 new LocalStorageIcingOptionsConfig(),
                 /* storeParentInfoAsSyntheticProperty= */ false,
-                /* shouldRetrieveParentInfo= */ true);
+                /* shouldRetrieveParentInfo= */ true,
+                /* persistToDiskRecoveryProof=*/false);
 
         // Building the SearchResult received from query.
         DocumentProto.Builder documentProtoBuilder = DocumentProto.newBuilder()
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchConfig.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchConfig.java
index 0932377..3b53f7e 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchConfig.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchConfig.java
@@ -19,6 +19,10 @@
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.app.GenericDocument;
 
+import com.google.android.icing.proto.PersistType;
+
+import org.jspecify.annotations.NonNull;
+
 /**
  * An interface that wraps AppSearch configurations required to create {@link AppSearchImpl}.
  */
@@ -38,4 +42,10 @@
      * {@link androidx.appsearch.flags.Flags#FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES} in on.
      */
     boolean shouldRetrieveParentInfo();
+
+    /**
+     * Returns the {@code PersistType.Code} that should be used to persist common mutations such as
+     * PUTs or DELETEs.
+     */
+    PersistType. @NonNull Code getLightweightPersistType();
 }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchConfigImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchConfigImpl.java
index 21142e0..1479d19 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchConfigImpl.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchConfigImpl.java
@@ -18,6 +18,8 @@
 
 import androidx.annotation.RestrictTo;
 
+import com.google.android.icing.proto.PersistType;
+
 import org.jspecify.annotations.NonNull;
 
 /**
@@ -30,23 +32,27 @@
     private final IcingOptionsConfig mIcingOptionsConfig;
     private final boolean mStoreParentInfoAsSyntheticProperty;
     private final boolean mShouldRetrieveParentInfo;
+    private final boolean mPersistToDiskRecoveryProof;
 
     public AppSearchConfigImpl(@NonNull LimitConfig limitConfig,
             @NonNull IcingOptionsConfig icingOptionsConfig) {
         this(limitConfig,
                 icingOptionsConfig,
                 /* storeParentInfoAsSyntheticProperty= */ false,
-                /* shouldRetrieveParentInfo= */ false);
+                /* shouldRetrieveParentInfo= */ false,
+                /* persistToDiskRecoveryProof= */false);
     }
 
     public AppSearchConfigImpl(@NonNull LimitConfig limitConfig,
             @NonNull IcingOptionsConfig icingOptionsConfig,
             boolean storeParentInfoAsSyntheticProperty,
-            boolean shouldRetrieveParentInfo) {
+            boolean shouldRetrieveParentInfo,
+            boolean persistToDiskRecoveryProof) {
         mLimitConfig = limitConfig;
         mIcingOptionsConfig = icingOptionsConfig;
         mStoreParentInfoAsSyntheticProperty = storeParentInfoAsSyntheticProperty;
         mShouldRetrieveParentInfo = shouldRetrieveParentInfo;
+        mPersistToDiskRecoveryProof = persistToDiskRecoveryProof;
     }
 
     @Override
@@ -163,4 +169,10 @@
     public long getOrphanBlobTimeToLiveMs() {
         return mIcingOptionsConfig.getOrphanBlobTimeToLiveMs();
     }
+
+    @Override
+    public PersistType. @NonNull Code getLightweightPersistType() {
+        return mPersistToDiskRecoveryProof ?
+                PersistType.Code.RECOVERY_PROOF : PersistType.Code.LITE;
+    }
 }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
index 6cb8f15..1dfb53a 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
@@ -503,6 +503,13 @@
     }
 
     /**
+     * Returns the instance of AppSearchConfig used by this instance of AppSearchImpl.
+     */
+    public @NonNull AppSearchConfig getConfig() {
+        return mConfig;
+    }
+
+    /**
      * Updates the AppSearch schema for this app.
      *
      * <p>This method belongs to mutate group.
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
index 5938407..d3377ee 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
@@ -64,13 +64,16 @@
         final String mDatabaseName;
         final Executor mExecutor;
         final @Nullable AppSearchLogger mLogger;
+        final boolean mPersistToDiskRecoveryProof;
 
         SearchContext(@NonNull Context context, @NonNull String databaseName,
-                @NonNull Executor executor, @Nullable AppSearchLogger logger) {
+                @NonNull Executor executor, @Nullable AppSearchLogger logger,
+                boolean persistToDiskRecoveryProof) {
             mContext = Preconditions.checkNotNull(context);
             mDatabaseName = Preconditions.checkNotNull(databaseName);
             mExecutor = Preconditions.checkNotNull(executor);
             mLogger = logger;
+            mPersistToDiskRecoveryProof = persistToDiskRecoveryProof;
         }
 
         /**
@@ -107,6 +110,7 @@
             private final String mDatabaseName;
             private Executor mExecutor;
             private @Nullable AppSearchLogger mLogger;
+            private boolean mPersistToDiskRecoveryProof;
 
             /**
              * Creates a {@link SearchContext.Builder} instance.
@@ -158,12 +162,32 @@
                 return this;
             }
 
+            /**
+             * Sets whether AppSearch should call persistToDisk LITE or persistToDisk RECOVERY_PROOF
+             * after mutations ({@link AppSearchSession#putAsync} and
+             * {@link AppSearchSession#removeAsync}). LITE guarantees no data loss on initialization
+             * but with a recovery. RECOVERY_PROOF will guarantee no data loss and no recovery.
+             *
+             * <p>Note: This api is only added to facilitate early opt-ins by clients. It will be
+             * deprecated and then deleted (with the new 'true' behavior enabled) once this change
+             * has had sufficient time to soak.
+             *
+             * @exportToFramework:hide
+             */
+            @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+            @NonNull
+            public Builder setPersistToDiskRecoveryProof(boolean persistToDiskRecoveryProof) {
+                mPersistToDiskRecoveryProof = persistToDiskRecoveryProof;
+                return this;
+            }
+
             /** Builds a {@link SearchContext} instance. */
             public @NonNull SearchContext build() {
                 if (mExecutor == null) {
                     mExecutor = EXECUTOR;
                 }
-                return new SearchContext(mContext, mDatabaseName, mExecutor, mLogger);
+                return new SearchContext(
+                        mContext, mDatabaseName, mExecutor, mLogger, mPersistToDiskRecoveryProof);
             }
         }
     }
@@ -272,7 +296,7 @@
         Preconditions.checkNotNull(context);
         return FutureUtil.execute(context.mExecutor, () -> {
             LocalStorage instance = getOrCreateInstance(context.mContext, context.mExecutor,
-                    context.mLogger);
+                    context.mLogger, context.mPersistToDiskRecoveryProof);
             return instance.doCreateSearchSession(context);
         });
     }
@@ -292,7 +316,7 @@
         Preconditions.checkNotNull(context);
         return FutureUtil.execute(context.mExecutor, () -> {
             LocalStorage instance = getOrCreateInstance(context.mContext, context.mExecutor,
-                    context.mLogger);
+                    context.mLogger, /*persistToDiskRecoveryProof=*/false);
             return instance.doCreateGlobalSearchSession(context);
         });
     }
@@ -305,25 +329,41 @@
      */
     @WorkerThread
     @VisibleForTesting
-    static @NonNull LocalStorage getOrCreateInstance(@NonNull Context context,
-            @NonNull Executor executor, @Nullable AppSearchLogger logger)
+    static @NonNull LocalStorage getOrCreateInstance(
+            @NonNull Context context, @NonNull Executor executor,
+            @Nullable AppSearchLogger logger, boolean persistToDiskRecoveryProof)
             throws AppSearchException {
         Preconditions.checkNotNull(context);
         if (sInstance == null) {
             synchronized (LocalStorage.class) {
                 if (sInstance == null) {
-                    sInstance = new LocalStorage(context, executor, logger);
+                    sInstance =
+                            new LocalStorage(context, executor, logger, persistToDiskRecoveryProof);
                 }
             }
         }
         return sInstance;
     }
 
+    @VisibleForTesting
+    static @NonNull AppSearchConfig getConfig(@NonNull SearchContext context)
+            throws AppSearchException {
+        LocalStorage instance = getOrCreateInstance(context.mContext, context.mExecutor,
+                context.mLogger, context.mPersistToDiskRecoveryProof);
+        return instance.mAppSearchImpl.getConfig();
+    }
+
+    @VisibleForTesting
+    static void resetInstance() {
+        sInstance = null;
+    }
+
     @WorkerThread
     private LocalStorage(
             @NonNull Context context,
             @NonNull Executor executor,
-            @Nullable AppSearchLogger logger)
+            @Nullable AppSearchLogger logger,
+            boolean persistToDiskRecoveryProof)
             throws AppSearchException {
         Preconditions.checkNotNull(context);
         File icingDir = AppSearchEnvironmentFactory.getEnvironmentInstance()
@@ -342,7 +382,8 @@
                 new UnlimitedLimitConfig(),
                 new LocalStorageIcingOptionsConfig(),
                 /* storeParentInfoAsSyntheticProperty= */ false,
-                /* shouldRetrieveParentInfo= */ true
+                /* shouldRetrieveParentInfo= */ true,
+                persistToDiskRecoveryProof
         );
         RevocableFileDescriptorStore revocableFileDescriptorStore = null;
         if (Flags.enableBlobStore()) {
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
index 3d820bd..30d8161 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
@@ -376,7 +376,7 @@
             }
 
             // Now that the batch has been written. Persist the newly written data.
-            mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
+            mAppSearchImpl.persistToDisk(mAppSearchImpl.getConfig().getLightweightPersistType());
             mIsMutated = true;
 
             // Schedule a task to dispatch change notifications. See requirements for where the
@@ -605,7 +605,7 @@
                 }
             }
             // Now that the batch has been written. Persist the newly written data.
-            mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
+            mAppSearchImpl.persistToDisk(mAppSearchImpl.getConfig().getLightweightPersistType());
             mIsMutated = true;
             // Schedule a task to dispatch change notifications. See requirements for where the
             // method is called documented in the method description.
@@ -636,7 +636,7 @@
             mAppSearchImpl.removeByQuery(mPackageName, mDatabaseName, queryExpression,
                     searchSpec, removeStatsBuilder);
             // Now that the batch has been written. Persist the newly written data.
-            mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
+            mAppSearchImpl.persistToDisk(mAppSearchImpl.getConfig().getLightweightPersistType());
             mIsMutated = true;
             // Schedule a task to dispatch change notifications. See requirements for where the
             // method is called documented in the method description.
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStore.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStore.java
index 6d06eb5c..39e07df 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStore.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStore.java
@@ -234,7 +234,7 @@
                     prefixedVisibilityConfig);
         }
         // Now that the visibility document has been written. Persist the newly written data.
-        mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
+        mAppSearchImpl.persistToDisk(mAppSearchImpl.getConfig().getLightweightPersistType());
     }
 
     /**