11/*
2- * Copyright 2023-2025 Google LLC
2+ * Copyright 2026 Google LLC
33 *
44 * Licensed under the Apache License, Version 2.0 (the "License");
55 * you may not use this file except in compliance with the License.
@@ -27,58 +27,32 @@ import com.google.android.fhir.db.impl.DatabaseImpl.Companion.UNENCRYPTED_DATABA
2727import java.time.Duration
2828import kotlinx.coroutines.delay
2929import kotlinx.coroutines.runBlocking
30- import net.sqlcipher.database.SQLiteDatabase
31- import net.sqlcipher.database.SQLiteDatabaseHook
32- import net.sqlcipher.database.SQLiteOpenHelper
30+ import net.zetetic.database.sqlcipher.SQLiteDatabaseHook
31+ import net.zetetic.database.sqlcipher.SupportOpenHelperFactory
3332import timber.log.Timber
3433
35- /* * A [SupportSQLiteOpenHelper] which initializes a [SQLiteDatabase] with a passphrase. */
34+ /* * A [SupportSQLiteOpenHelper] which initializes SQLCipher with a passphrase. */
3635internal class SQLCipherSupportHelper (
3736 private val configuration : SupportSQLiteOpenHelper .Configuration ,
38- hook : SQLiteDatabaseHook ? = null ,
37+ private val hook : SQLiteDatabaseHook ? = null ,
3938 private val databaseErrorStrategy : DatabaseErrorStrategy ,
4039 private val passphraseFetcher : () -> ByteArray ,
4140) : SupportSQLiteOpenHelper {
4241
4342 init {
44- SQLiteDatabase .loadLibs(configuration.context )
43+ System .loadLibrary( " sqlcipher " )
4544 }
4645
47- private val standardHelper =
48- object :
49- SQLiteOpenHelper (
50- configuration.context,
51- configuration.name,
52- /* factory= */ null ,
53- configuration.callback.version,
54- hook,
55- ) {
56- override fun onCreate (db : SQLiteDatabase ) {
57- configuration.callback.onCreate(db)
58- }
46+ @Volatile private var delegate: SupportSQLiteOpenHelper ? = null
5947
60- override fun onUpgrade (db : SQLiteDatabase , oldVersion : Int , newVersion : Int ) {
61- configuration.callback.onUpgrade(db, oldVersion, newVersion)
62- }
48+ @Volatile private var walEnabled: Boolean = false
6349
64- override fun onDowngrade (db : SQLiteDatabase , oldVersion : Int , newVersion : Int ) {
65- configuration.callback.onDowngrade(db, oldVersion, newVersion)
66- }
67-
68- override fun onOpen (db : SQLiteDatabase ) {
69- configuration.callback.onOpen(db)
70- }
71-
72- override fun onConfigure (db : SQLiteDatabase ) {
73- configuration.callback.onConfigure(db)
74- }
75- }
76-
77- override val databaseName
78- get() = standardHelper.databaseName
50+ override val databaseName: String?
51+ get() = configuration.name
7952
8053 override fun setWriteAheadLoggingEnabled (enabled : Boolean ) {
81- standardHelper.setWriteAheadLoggingEnabled(enabled)
54+ walEnabled = enabled
55+ delegate?.setWriteAheadLoggingEnabled(enabled)
8256 }
8357
8458 override val writableDatabase: SupportSQLiteDatabase
@@ -87,20 +61,52 @@ internal class SQLCipherSupportHelper(
8761 " Unexpected unencrypted database, $UNENCRYPTED_DATABASE_NAME , already exists. " +
8862 " Check if you have accidentally disabled database encryption across releases."
8963 }
90- val key = runBlocking { getPassphraseWithRetry() }
64+
65+ val helper = delegate ? : createDelegate().also { delegate = it }
66+
9167 return try {
92- standardHelper.getWritableDatabase(key)
68+ helper.writableDatabase
9369 } catch (ex: SQLiteException ) {
9470 if (databaseErrorStrategy == DatabaseErrorStrategy .RECREATE_AT_OPEN ) {
9571 Timber .w(" Fail to open database. Recreating database." )
9672 configuration.context.getDatabasePath(databaseName).delete()
97- standardHelper.getWritableDatabase(key)
73+
74+ // Reset and retry with a fresh helper instance
75+ delegate?.close()
76+ delegate = null
77+ createDelegate().also { delegate = it }.writableDatabase
9878 } else {
9979 throw ex
10080 }
10181 }
10282 }
10383
84+ override val readableDatabase: SupportSQLiteDatabase
85+ get() = writableDatabase
86+
87+ override fun close () {
88+ delegate?.close()
89+ delegate = null
90+ }
91+
92+ /* * Creates a SQLCipher-aware SupportSQLiteOpenHelper using SupportOpenHelperFactory. */
93+ private fun createDelegate (): SupportSQLiteOpenHelper {
94+ val passphrase = runBlocking { getPassphraseWithRetry() }
95+
96+ val factory =
97+ if (hook == null ) {
98+ SupportOpenHelperFactory (passphrase)
99+ } else {
100+ SupportOpenHelperFactory (
101+ passphrase,
102+ hook,
103+ /* enableWriteAheadLogging = */ walEnabled,
104+ )
105+ }
106+
107+ return factory.create(configuration)
108+ }
109+
104110 private suspend fun getPassphraseWithRetry (): ByteArray {
105111 var lastException: DatabaseEncryptionException ? = null
106112 for (retryAttempt in 1 .. MAX_RETRY_ATTEMPTS ) {
@@ -120,13 +126,6 @@ internal class SQLCipherSupportHelper(
120126 throw lastException ? : DatabaseEncryptionException (Exception (), UNKNOWN )
121127 }
122128
123- override val readableDatabase
124- get() = writableDatabase
125-
126- override fun close () {
127- standardHelper.close()
128- }
129-
130129 private companion object {
131130 const val MAX_RETRY_ATTEMPTS = 3
132131
0 commit comments