FairEmail/app/src/main/java/androidx/room/SQLiteCopyOpenHelper.kt

243 lines
9.4 KiB
Kotlin

/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room
import android.content.Context
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.room.Room.LOG_TAG
import androidx.room.util.copy
import androidx.room.util.readVersion
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteOpenHelper
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.sqlite.util.ProcessLock
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.lang.Exception
import java.lang.IllegalStateException
import java.lang.RuntimeException
import java.nio.channels.Channels
import java.nio.channels.ReadableByteChannel
import java.util.concurrent.Callable
/**
* An open helper that will copy & open a pre-populated database if it doesn't exists in internal
* storage.
*/
@Suppress("BanSynchronizedMethods")
internal class SQLiteCopyOpenHelper(
private val context: Context,
private val copyFromAssetPath: String?,
private val copyFromFile: File?,
private val copyFromInputStream: Callable<InputStream>?,
private val databaseVersion: Int,
override val delegate: SupportSQLiteOpenHelper
) : SupportSQLiteOpenHelper, DelegatingOpenHelper {
private lateinit var databaseConfiguration: DatabaseConfiguration
private var verified = false
override val databaseName: String?
get() = delegate.databaseName
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
override fun setWriteAheadLoggingEnabled(enabled: Boolean) {
delegate.setWriteAheadLoggingEnabled(enabled)
}
override val writableDatabase: SupportSQLiteDatabase
get() {
if (!verified) {
verifyDatabaseFile(true)
verified = true
}
return delegate.writableDatabase
}
override val readableDatabase: SupportSQLiteDatabase
get() {
if (!verified) {
verifyDatabaseFile(false)
verified = true
}
return delegate.readableDatabase
}
@Synchronized
override fun close() {
delegate.close()
verified = false
}
// Can't be constructor param because the factory is needed by the database builder which in
// turn is the one that actually builds the configuration.
fun setDatabaseConfiguration(databaseConfiguration: DatabaseConfiguration) {
this.databaseConfiguration = databaseConfiguration
}
private fun verifyDatabaseFile(writable: Boolean) {
val name = checkNotNull(databaseName)
val databaseFile = context.getDatabasePath(name)
val processLevelLock = (databaseConfiguration.multiInstanceInvalidation)
val copyLock = ProcessLock(
name,
context.filesDir,
processLevelLock
)
try {
// Acquire a copy lock, this lock works across threads and processes, preventing
// concurrent copy attempts from occurring.
copyLock.lock()
if (!databaseFile.exists()) {
try {
// No database file found, copy and be done.
copyDatabaseFile(databaseFile, writable)
return
} catch (e: IOException) {
throw RuntimeException("Unable to copy database file.", e)
}
}
// A database file is present, check if we need to re-copy it.
val currentVersion = try {
readVersion(databaseFile)
} catch (e: IOException) {
Log.w(LOG_TAG, "Unable to read database version.", e)
return
}
if (currentVersion == databaseVersion) {
return
}
if (databaseConfiguration.isMigrationRequired(currentVersion, databaseVersion)) {
// From the current version to the desired version a migration is required, i.e.
// we won't be performing a copy destructive migration.
return
}
if (context.deleteDatabase(name)) {
try {
copyDatabaseFile(databaseFile, writable)
} catch (e: IOException) {
// We are more forgiving copying a database on a destructive migration since
// there is already a database file that can be opened.
Log.w(LOG_TAG, "Unable to copy database file.", e)
}
} else {
Log.w(
LOG_TAG, "Failed to delete database file ($name) for " +
"a copy destructive migration."
)
}
} finally {
copyLock.unlock()
}
}
@Throws(IOException::class)
private fun copyDatabaseFile(destinationFile: File, writable: Boolean) {
val input: ReadableByteChannel
if (copyFromAssetPath != null) {
input = Channels.newChannel(context.assets.open(copyFromAssetPath))
} else if (copyFromFile != null) {
input = FileInputStream(copyFromFile).channel
} else if (copyFromInputStream != null) {
val inputStream = try {
copyFromInputStream.call()
} catch (e: Exception) {
throw IOException("inputStreamCallable exception on call", e)
}
input = Channels.newChannel(inputStream)
} else {
throw IllegalStateException(
"copyFromAssetPath, copyFromFile and copyFromInputStream are all null!"
)
}
// An intermediate file is used so that we never end up with a half-copied database file
// in the internal directory.
val intermediateFile = File.createTempFile(
"room-copy-helper", ".tmp", context.cacheDir
)
intermediateFile.deleteOnExit()
val output = FileOutputStream(intermediateFile).channel
copy(input, output)
val parent = destinationFile.parentFile
if (parent != null && !parent.exists() && !parent.mkdirs()) {
throw IOException(
"Failed to create directories for ${destinationFile.absolutePath}"
)
}
// Temporarily open intermediate database file using FrameworkSQLiteOpenHelper and dispatch
// the open pre-packaged callback. If it fails then intermediate file won't be copied making
// invoking pre-packaged callback a transactional operation.
dispatchOnOpenPrepackagedDatabase(intermediateFile, writable)
if (!intermediateFile.renameTo(destinationFile)) {
throw IOException(
"Failed to move intermediate file (${intermediateFile.absolutePath}) to " +
"destination (${destinationFile.absolutePath})."
)
}
}
private fun dispatchOnOpenPrepackagedDatabase(databaseFile: File, writable: Boolean) {
if (databaseConfiguration.prepackagedDatabaseCallback == null
) {
return
}
createFrameworkOpenHelper(databaseFile).use { helper ->
val db = if (writable) helper.writableDatabase else helper.readableDatabase
databaseConfiguration.prepackagedDatabaseCallback!!.onOpenPrepackagedDatabase(db)
}
}
private fun createFrameworkOpenHelper(databaseFile: File): SupportSQLiteOpenHelper {
val version = try {
readVersion(databaseFile)
} catch (e: IOException) {
throw RuntimeException("Malformed database file, unable to read version.", e)
}
val factory = FrameworkSQLiteOpenHelperFactory()
val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
.name(databaseFile.absolutePath)
.callback(object : SupportSQLiteOpenHelper.Callback(version.coerceAtLeast(1)) {
override fun onCreate(db: SupportSQLiteDatabase) {}
override fun onUpgrade(
db: SupportSQLiteDatabase,
oldVersion: Int,
newVersion: Int
) {
}
override fun onOpen(db: SupportSQLiteDatabase) {
// If pre-packaged database has a version < 1 we will open it as if it was
// version 1 because the framework open helper does not allow version < 1.
// The database will be considered as newly created and onCreate() will be
// invoked, but we do nothing and reset the version back so Room later runs
// migrations as usual.
if (version < 1) {
db.version = version
}
}
})
.build()
return factory.create(configuration)
}
}