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