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