1 /* <lambda>null2 * Copyright 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package androidx.room.support 17 18 import android.content.Context 19 import android.util.Log 20 import androidx.room.DatabaseConfiguration 21 import androidx.room.DelegatingOpenHelper 22 import androidx.room.Room.LOG_TAG 23 import androidx.room.util.copy 24 import androidx.room.util.readVersion 25 import androidx.sqlite.db.SupportSQLiteDatabase 26 import androidx.sqlite.db.SupportSQLiteOpenHelper 27 import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory 28 import androidx.sqlite.util.ProcessLock 29 import java.io.File 30 import java.io.FileInputStream 31 import java.io.FileOutputStream 32 import java.io.IOException 33 import java.io.InputStream 34 import java.nio.channels.Channels 35 import java.nio.channels.ReadableByteChannel 36 import java.util.concurrent.Callable 37 38 /** 39 * An open helper that will copy & open a pre-populated database if it doesn't exists in internal 40 * storage. 41 */ 42 @Suppress("BanSynchronizedMethods") 43 internal class PrePackagedCopyOpenHelper( 44 private val context: Context, 45 private val copyFromAssetPath: String?, 46 private val copyFromFile: File?, 47 private val copyFromInputStream: Callable<InputStream>?, 48 private val databaseVersion: Int, 49 override val delegate: SupportSQLiteOpenHelper 50 ) : SupportSQLiteOpenHelper, DelegatingOpenHelper { 51 private lateinit var databaseConfiguration: DatabaseConfiguration 52 private var verified = false 53 54 override val databaseName: String? 55 get() = delegate.databaseName 56 57 override fun setWriteAheadLoggingEnabled(enabled: Boolean) { 58 delegate.setWriteAheadLoggingEnabled(enabled) 59 } 60 61 override val writableDatabase: SupportSQLiteDatabase 62 get() { 63 if (!verified) { 64 verifyDatabaseFile(true) 65 verified = true 66 } 67 return delegate.writableDatabase 68 } 69 70 override val readableDatabase: SupportSQLiteDatabase 71 get() { 72 if (!verified) { 73 verifyDatabaseFile(false) 74 verified = true 75 } 76 return delegate.readableDatabase 77 } 78 79 @Synchronized 80 override fun close() { 81 delegate.close() 82 verified = false 83 } 84 85 // Can't be constructor param because the factory is needed by the database builder which in 86 // turn is the one that actually builds the configuration. 87 fun setDatabaseConfiguration(databaseConfiguration: DatabaseConfiguration) { 88 this.databaseConfiguration = databaseConfiguration 89 } 90 91 private fun verifyDatabaseFile(writable: Boolean) { 92 val name = checkNotNull(databaseName) 93 val databaseFile = context.getDatabasePath(name) 94 val processLevelLock = (databaseConfiguration.multiInstanceInvalidation) 95 val copyLock = ProcessLock(name, context.filesDir, processLevelLock) 96 try { 97 // Acquire a copy lock, this lock works across threads and processes, preventing 98 // concurrent copy attempts from occurring. 99 copyLock.lock() 100 if (!databaseFile.exists()) { 101 try { 102 // No database file found, copy and be done. 103 copyDatabaseFile(databaseFile, writable) 104 return 105 } catch (e: IOException) { 106 throw RuntimeException("Unable to copy database file.", e) 107 } 108 } 109 110 // A database file is present, check if we need to re-copy it. 111 val currentVersion = 112 try { 113 readVersion(databaseFile) 114 } catch (e: IOException) { 115 Log.w(LOG_TAG, "Unable to read database version.", e) 116 return 117 } 118 if (currentVersion == databaseVersion) { 119 return 120 } 121 if (databaseConfiguration.isMigrationRequired(currentVersion, databaseVersion)) { 122 // From the current version to the desired version a migration is required, i.e. 123 // we won't be performing a copy destructive migration. 124 return 125 } 126 if (context.deleteDatabase(name)) { 127 try { 128 copyDatabaseFile(databaseFile, writable) 129 } catch (e: IOException) { 130 // We are more forgiving copying a database on a destructive migration since 131 // there is already a database file that can be opened. 132 Log.w(LOG_TAG, "Unable to copy database file.", e) 133 } 134 } else { 135 Log.w( 136 LOG_TAG, 137 "Failed to delete database file ($name) for " + "a copy destructive migration." 138 ) 139 } 140 } finally { 141 copyLock.unlock() 142 } 143 } 144 145 @Throws(IOException::class) 146 private fun copyDatabaseFile(destinationFile: File, writable: Boolean) { 147 val input: ReadableByteChannel 148 if (copyFromAssetPath != null) { 149 input = Channels.newChannel(context.assets.open(copyFromAssetPath)) 150 } else if (copyFromFile != null) { 151 input = FileInputStream(copyFromFile).channel 152 } else if (copyFromInputStream != null) { 153 val inputStream = 154 try { 155 copyFromInputStream.call() 156 } catch (e: Exception) { 157 throw IOException("inputStreamCallable exception on call", e) 158 } 159 input = Channels.newChannel(inputStream) 160 } else { 161 throw IllegalStateException( 162 "copyFromAssetPath, copyFromFile and copyFromInputStream are all null!" 163 ) 164 } 165 166 // An intermediate file is used so that we never end up with a half-copied database file 167 // in the internal directory. 168 val intermediateFile = File.createTempFile("room-copy-helper", ".tmp", context.cacheDir) 169 intermediateFile.deleteOnExit() 170 val output = FileOutputStream(intermediateFile).channel 171 copy(input, output) 172 val parent = destinationFile.parentFile 173 if (parent != null && !parent.exists() && !parent.mkdirs()) { 174 throw IOException("Failed to create directories for ${destinationFile.absolutePath}") 175 } 176 177 // Temporarily open intermediate database file using FrameworkSQLiteOpenHelper and dispatch 178 // the open pre-packaged callback. If it fails then intermediate file won't be copied making 179 // invoking pre-packaged callback a transactional operation. 180 dispatchOnOpenPrepackagedDatabase(intermediateFile, writable) 181 if (!intermediateFile.renameTo(destinationFile)) { 182 throw IOException( 183 "Failed to move intermediate file (${intermediateFile.absolutePath}) to " + 184 "destination (${destinationFile.absolutePath})." 185 ) 186 } 187 } 188 189 private fun dispatchOnOpenPrepackagedDatabase(databaseFile: File, writable: Boolean) { 190 if (databaseConfiguration.prepackagedDatabaseCallback == null) { 191 return 192 } 193 createFrameworkOpenHelper(databaseFile).use { helper -> 194 val db = if (writable) helper.writableDatabase else helper.readableDatabase 195 databaseConfiguration.prepackagedDatabaseCallback!!.onOpenPrepackagedDatabase(db) 196 } 197 } 198 199 private fun createFrameworkOpenHelper(databaseFile: File): SupportSQLiteOpenHelper { 200 val version = 201 try { 202 readVersion(databaseFile) 203 } catch (e: IOException) { 204 throw RuntimeException("Malformed database file, unable to read version.", e) 205 } 206 val factory = FrameworkSQLiteOpenHelperFactory() 207 val configuration = 208 SupportSQLiteOpenHelper.Configuration.builder(context) 209 .name(databaseFile.absolutePath) 210 .callback( 211 object : SupportSQLiteOpenHelper.Callback(version.coerceAtLeast(1)) { 212 override fun onCreate(db: SupportSQLiteDatabase) {} 213 214 override fun onUpgrade( 215 db: SupportSQLiteDatabase, 216 oldVersion: Int, 217 newVersion: Int 218 ) {} 219 220 override fun onOpen(db: SupportSQLiteDatabase) { 221 // If pre-packaged database has a version < 1 we will open it as if it 222 // was 223 // version 1 because the framework open helper does not allow version < 224 // 1. 225 // The database will be considered as newly created and onCreate() will 226 // be 227 // invoked, but we do nothing and reset the version back so Room later 228 // runs 229 // migrations as usual. 230 if (version < 1) { 231 db.version = version 232 } 233 } 234 } 235 ) 236 .build() 237 return factory.create(configuration) 238 } 239 } 240