1 /* 2 * Copyright (C) 2016 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 17 package androidx.sqlite.db; 18 19 import android.content.Context; 20 import android.database.sqlite.SQLiteDatabase; 21 import android.database.sqlite.SQLiteException; 22 import android.os.Build; 23 import android.util.Log; 24 import android.util.Pair; 25 26 import androidx.annotation.NonNull; 27 import androidx.annotation.Nullable; 28 import androidx.annotation.RequiresApi; 29 30 import java.io.File; 31 import java.io.IOException; 32 import java.util.List; 33 34 /** 35 * An interface to map the behavior of {@link android.database.sqlite.SQLiteOpenHelper}. 36 * Note that since that class requires overriding certain methods, support implementation 37 * uses {@link Factory#create(Configuration)} to create this and {@link Callback} to implement 38 * the methods that should be overridden. 39 */ 40 @SuppressWarnings("unused") 41 public interface SupportSQLiteOpenHelper { 42 /** 43 * Return the name of the SQLite database being opened, as given to 44 * the constructor. 45 */ getDatabaseName()46 String getDatabaseName(); 47 48 /** 49 * Enables or disables the use of write-ahead logging for the database. 50 * 51 * Write-ahead logging cannot be used with read-only databases so the value of 52 * this flag is ignored if the database is opened read-only. 53 * 54 * @param enabled True if write-ahead logging should be enabled, false if it 55 * should be disabled. 56 * @see SupportSQLiteDatabase#enableWriteAheadLogging() 57 */ 58 @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) setWriteAheadLoggingEnabled(boolean enabled)59 void setWriteAheadLoggingEnabled(boolean enabled); 60 61 /** 62 * Create and/or open a database that will be used for reading and writing. 63 * The first time this is called, the database will be opened and 64 * {@link Callback#onCreate}, {@link Callback#onUpgrade} and/or {@link Callback#onOpen} will be 65 * called. 66 * 67 * <p>Once opened successfully, the database is cached, so you can 68 * call this method every time you need to write to the database. 69 * (Make sure to call {@link #close} when you no longer need the database.) 70 * Errors such as bad permissions or a full disk may cause this method 71 * to fail, but future attempts may succeed if the problem is fixed.</p> 72 * 73 * <p class="caution">Database upgrade may take a long time, you 74 * should not call this method from the application main thread, including 75 * from {@link android.content.ContentProvider#onCreate ContentProvider.onCreate()}. 76 * 77 * @return a read/write database object valid until {@link #close} is called 78 * @throws SQLiteException if the database cannot be opened for writing 79 */ getWritableDatabase()80 SupportSQLiteDatabase getWritableDatabase(); 81 82 /** 83 * Create and/or open a database. This will be the same object returned by 84 * {@link #getWritableDatabase} unless some problem, such as a full disk, 85 * requires the database to be opened read-only. In that case, a read-only 86 * database object will be returned. If the problem is fixed, a future call 87 * to {@link #getWritableDatabase} may succeed, in which case the read-only 88 * database object will be closed and the read/write object will be returned 89 * in the future. 90 * 91 * <p class="caution">Like {@link #getWritableDatabase}, this method may 92 * take a long time to return, so you should not call it from the 93 * application main thread, including from 94 * {@link android.content.ContentProvider#onCreate ContentProvider.onCreate()}. 95 * 96 * @return a database object valid until {@link #getWritableDatabase} 97 * or {@link #close} is called. 98 * @throws SQLiteException if the database cannot be opened 99 */ getReadableDatabase()100 SupportSQLiteDatabase getReadableDatabase(); 101 102 /** 103 * Close any open database object. 104 */ close()105 void close(); 106 107 /** 108 * Handles various lifecycle events for the SQLite connection, similar to 109 * {@link android.database.sqlite.SQLiteOpenHelper}. 110 */ 111 @SuppressWarnings({"unused", "WeakerAccess"}) 112 abstract class Callback { 113 private static final String TAG = "SupportSQLite"; 114 /** 115 * Version number of the database (starting at 1); if the database is older, 116 * {@link SupportSQLiteOpenHelper.Callback#onUpgrade(SupportSQLiteDatabase, int, int)} 117 * will be used to upgrade the database; if the database is newer, 118 * {@link SupportSQLiteOpenHelper.Callback#onDowngrade(SupportSQLiteDatabase, int, int)} 119 * will be used to downgrade the database. 120 */ 121 public final int version; 122 123 /** 124 * Creates a new Callback to get database lifecycle events. 125 * @param version The version for the database instance. See {@link #version}. 126 */ Callback(int version)127 public Callback(int version) { 128 this.version = version; 129 } 130 131 /** 132 * Called when the database connection is being configured, to enable features such as 133 * write-ahead logging or foreign key support. 134 * <p> 135 * This method is called before {@link #onCreate}, {@link #onUpgrade}, {@link #onDowngrade}, 136 * or {@link #onOpen} are called. It should not modify the database except to configure the 137 * database connection as required. 138 * </p> 139 * <p> 140 * This method should only call methods that configure the parameters of the database 141 * connection, such as {@link SupportSQLiteDatabase#enableWriteAheadLogging} 142 * {@link SupportSQLiteDatabase#setForeignKeyConstraintsEnabled}, 143 * {@link SupportSQLiteDatabase#setLocale}, 144 * {@link SupportSQLiteDatabase#setMaximumSize}, or executing PRAGMA statements. 145 * </p> 146 * 147 * @param db The database. 148 */ onConfigure(SupportSQLiteDatabase db)149 public void onConfigure(SupportSQLiteDatabase db) { 150 151 } 152 153 /** 154 * Called when the database is created for the first time. This is where the 155 * creation of tables and the initial population of the tables should happen. 156 * 157 * @param db The database. 158 */ onCreate(SupportSQLiteDatabase db)159 public abstract void onCreate(SupportSQLiteDatabase db); 160 161 /** 162 * Called when the database needs to be upgraded. The implementation 163 * should use this method to drop tables, add tables, or do anything else it 164 * needs to upgrade to the new schema version. 165 * 166 * <p> 167 * The SQLite ALTER TABLE documentation can be found 168 * <a href="http://sqlite.org/lang_altertable.html">here</a>. If you add new columns 169 * you can use ALTER TABLE to insert them into a live table. If you rename or remove columns 170 * you can use ALTER TABLE to rename the old table, then create the new table and then 171 * populate the new table with the contents of the old table. 172 * </p><p> 173 * This method executes within a transaction. If an exception is thrown, all changes 174 * will automatically be rolled back. 175 * </p> 176 * 177 * @param db The database. 178 * @param oldVersion The old database version. 179 * @param newVersion The new database version. 180 */ onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion)181 public abstract void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion); 182 183 /** 184 * Called when the database needs to be downgraded. This is strictly similar to 185 * {@link #onUpgrade} method, but is called whenever current version is newer than requested 186 * one. 187 * However, this method is not abstract, so it is not mandatory for a customer to 188 * implement it. If not overridden, default implementation will reject downgrade and 189 * throws SQLiteException 190 * 191 * <p> 192 * This method executes within a transaction. If an exception is thrown, all changes 193 * will automatically be rolled back. 194 * </p> 195 * 196 * @param db The database. 197 * @param oldVersion The old database version. 198 * @param newVersion The new database version. 199 */ onDowngrade(SupportSQLiteDatabase db, int oldVersion, int newVersion)200 public void onDowngrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) { 201 throw new SQLiteException("Can't downgrade database from version " 202 + oldVersion + " to " + newVersion); 203 } 204 205 /** 206 * Called when the database has been opened. The implementation 207 * should check {@link SupportSQLiteDatabase#isReadOnly} before updating the 208 * database. 209 * <p> 210 * This method is called after the database connection has been configured 211 * and after the database schema has been created, upgraded or downgraded as necessary. 212 * If the database connection must be configured in some way before the schema 213 * is created, upgraded, or downgraded, do it in {@link #onConfigure} instead. 214 * </p> 215 * 216 * @param db The database. 217 */ onOpen(SupportSQLiteDatabase db)218 public void onOpen(SupportSQLiteDatabase db) { 219 220 } 221 222 /** 223 * The method invoked when database corruption is detected. Default implementation will 224 * delete the database file. 225 * 226 * @param db the {@link SupportSQLiteDatabase} object representing the database on which 227 * corruption is detected. 228 */ onCorruption(SupportSQLiteDatabase db)229 public void onCorruption(SupportSQLiteDatabase db) { 230 // the following implementation is taken from {@link DefaultDatabaseErrorHandler}. 231 232 Log.e(TAG, "Corruption reported by sqlite on database: " + db.getPath()); 233 // is the corruption detected even before database could be 'opened'? 234 if (!db.isOpen()) { 235 // database files are not even openable. delete this database file. 236 // NOTE if the database has attached databases, then any of them could be corrupt. 237 // and not deleting all of them could cause corrupted database file to remain and 238 // make the application crash on database open operation. To avoid this problem, 239 // the application should provide its own {@link DatabaseErrorHandler} impl class 240 // to delete ALL files of the database (including the attached databases). 241 deleteDatabaseFile(db.getPath()); 242 return; 243 } 244 245 List<Pair<String, String>> attachedDbs = null; 246 try { 247 // Close the database, which will cause subsequent operations to fail. 248 // before that, get the attached database list first. 249 try { 250 attachedDbs = db.getAttachedDbs(); 251 } catch (SQLiteException e) { 252 /* ignore */ 253 } 254 try { 255 db.close(); 256 } catch (IOException e) { 257 /* ignore */ 258 } 259 } finally { 260 // Delete all files of this corrupt database and/or attached databases 261 if (attachedDbs != null) { 262 for (Pair<String, String> p : attachedDbs) { 263 deleteDatabaseFile(p.second); 264 } 265 } else { 266 // attachedDbs = null is possible when the database is so corrupt that even 267 // "PRAGMA database_list;" also fails. delete the main database file 268 deleteDatabaseFile(db.getPath()); 269 } 270 } 271 } 272 deleteDatabaseFile(String fileName)273 private void deleteDatabaseFile(String fileName) { 274 if (fileName.equalsIgnoreCase(":memory:") || fileName.trim().length() == 0) { 275 return; 276 } 277 Log.w(TAG, "deleting the database file: " + fileName); 278 try { 279 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 280 SQLiteDatabase.deleteDatabase(new File(fileName)); 281 } else { 282 try { 283 final boolean deleted = new File(fileName).delete(); 284 if (!deleted) { 285 Log.e(TAG, "Could not delete the database file " + fileName); 286 } 287 } catch (Exception error) { 288 Log.e(TAG, "error while deleting corrupted database file", error); 289 } 290 } 291 } catch (Exception e) { 292 /* print warning and ignore exception */ 293 Log.w(TAG, "delete failed: ", e); 294 } 295 } 296 } 297 298 /** 299 * The configuration to create an SQLite open helper object using {@link Factory}. 300 */ 301 @SuppressWarnings("WeakerAccess") 302 class Configuration { 303 /** 304 * Context to use to open or create the database. 305 */ 306 @NonNull 307 public final Context context; 308 /** 309 * Name of the database file, or null for an in-memory database. 310 */ 311 @Nullable 312 public final String name; 313 /** 314 * The callback class to handle creation, upgrade and downgrade. 315 */ 316 @NonNull 317 public final SupportSQLiteOpenHelper.Callback callback; 318 Configuration(@onNull Context context, @Nullable String name, @NonNull Callback callback)319 Configuration(@NonNull Context context, @Nullable String name, @NonNull Callback callback) { 320 this.context = context; 321 this.name = name; 322 this.callback = callback; 323 } 324 325 /** 326 * Creates a new Configuration.Builder to create an instance of Configuration. 327 * 328 * @param context to use to open or create the database. 329 */ builder(Context context)330 public static Builder builder(Context context) { 331 return new Builder(context); 332 } 333 334 /** 335 * Builder class for {@link Configuration}. 336 */ 337 public static class Builder { 338 Context mContext; 339 String mName; 340 SupportSQLiteOpenHelper.Callback mCallback; 341 build()342 public Configuration build() { 343 if (mCallback == null) { 344 throw new IllegalArgumentException("Must set a callback to create the" 345 + " configuration."); 346 } 347 if (mContext == null) { 348 throw new IllegalArgumentException("Must set a non-null context to create" 349 + " the configuration."); 350 } 351 return new Configuration(mContext, mName, mCallback); 352 } 353 Builder(@onNull Context context)354 Builder(@NonNull Context context) { 355 mContext = context; 356 } 357 358 /** 359 * @param name Name of the database file, or null for an in-memory database. 360 * @return This 361 */ name(@ullable String name)362 public Builder name(@Nullable String name) { 363 mName = name; 364 return this; 365 } 366 367 /** 368 * @param callback The callback class to handle creation, upgrade and downgrade. 369 * @return this 370 */ callback(@onNull Callback callback)371 public Builder callback(@NonNull Callback callback) { 372 mCallback = callback; 373 return this; 374 } 375 } 376 } 377 378 /** 379 * Factory class to create instances of {@link SupportSQLiteOpenHelper} using 380 * {@link Configuration}. 381 */ 382 interface Factory { 383 /** 384 * Creates an instance of {@link SupportSQLiteOpenHelper} using the given configuration. 385 * 386 * @param configuration The configuration to use while creating the open helper. 387 * 388 * @return A SupportSQLiteOpenHelper which can be used to open a database. 389 */ create(Configuration configuration)390 SupportSQLiteOpenHelper create(Configuration configuration); 391 } 392 } 393