package com.gettingmobile.android.database;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteException;
import android.util.Log;
import com.gettingmobile.goodnews.storage.StorageProvider;
import com.gettingmobile.io.IOUtils;
import java.io.File;
import java.io.IOException;
/**
* A helper class to manage database creation and version management.
* <p/>
* <p>You create a subclass by implementing {@link #onCreate} an {@link #onUpgrade}, and this class takes care of
* opening the database
* if it exists, creating it if it does not, and upgrading it as necessary.
* Transactions are used to make sure the database is always in a sensible state.
* <p/>
* <p class="note"><strong>Note:</strong> this class assumes
* monotonically increasing version numbers for upgrades. Also, there
* is no concept of a database downgrade; installing a new version of
* your app which uses a lower version number than a
* previously-installed version will result in undefined behavior.</p>
*/
public abstract class DatabaseOpenHelper {
private static final String TAG = "goodnews.DatabaseOpenHelper";
private final String mName;
private final CursorFactory mFactory;
private final int mNewVersion;
private SQLiteDatabase mDatabase = null;
private boolean mIsInitializing = false;
/**
* Create a helper object to create, open, and/or manage a database.
* This method always returns very quickly. The database is not actually
* created or opened until {@link #openOrCreateDatabase} is called.
*
* @param name of the database file, or null for an in-memory database
* @param factory to use for creating cursor objects, or null for the default
* @param version number of the database (starting at 1); if the database is older,
* {@link #onUpgrade} will be used to upgrade the database
*/
public DatabaseOpenHelper(String name, CursorFactory factory, int version) {
if (version < 1) throw new IllegalArgumentException("Version must be >= 1, was " + version);
mName = name;
mFactory = factory;
mNewVersion = version;
}
/**
* Moves the database from one storage to another one. If the source and the destination storage are the same
* nothing will be done. Before invoked, the database needs to be closed and afterwards the database needs to be
* reopened.
* @param source storage provider referencing the storage to move the database from.
* @param destination storage provider referencing the storage to move the database to.
* @throws IOException if moving the database fails.
*/
public synchronized void moveDatabase(StorageProvider source, StorageProvider destination) throws IOException {
final File src = source.getDatabasePath(mName);
final File dest = destination.getDatabasePath(mName);
final File destDir = dest.getParentFile();
if (!src.getAbsolutePath().equals(dest.getAbsolutePath())) {
if (!source.isStorageAvailable())
throw new IOException("Source storage not readable");
if (!destination.isStorageWritable())
throw new IOException("Destination storage not writable");
if (!src.exists())
throw new IOException("Source file " + src.getAbsolutePath() + " does not exist!");
if (!destDir.exists() && !destDir.mkdirs())
throw new IOException("Failed to create destination directory " + destDir.getAbsolutePath());
Log.i(TAG, "Moving database from " + src.getAbsolutePath() + " to " + dest.getAbsolutePath());
IOUtils.move(src, dest);
}
}
/**
* Same as [@link #moveDatabase} but allows the database to be open and opens the new database afterwards.
*/
public synchronized void moveOpenDatabase(StorageProvider source, StorageProvider destination) throws IOException {
/*
* close the current database
*/
close();
/*
* move the database
*/
try {
moveDatabase(source, destination);
} catch (IOException ex) {
Log.e(TAG, "Failed to move database!", ex);
/*
* we failed, so we should open the old database
*/
openOrCreateDatabase(source);
throw ex;
}
/*
* open the new database
*/
openOrCreateDatabase(destination);
}
public synchronized void deleteDatabase(StorageProvider storageProvider) throws IOException {
IOUtils.delete(storageProvider.getDatabasePath(mName));
}
public synchronized void recreateOpenDatabase(StorageProvider storageProvider) throws IOException {
/*
* close current database
*/
close();
/*
* delete the database
*/
deleteDatabase(storageProvider);
/*
* open the existing one or create a new database
*/
openOrCreateDatabase(storageProvider);
}
private File getDatabasePath(StorageProvider storageProvider) {
final File dbPath = storageProvider.getDatabasePath(mName);
Log.i(TAG, "Open or create database at " + dbPath.getAbsolutePath());
final File dbDir = dbPath.getParentFile();
if (!dbDir.exists() && !dbDir.mkdirs()) {
final String msg = "Failed to create database directory at " + dbDir.getAbsolutePath();
Log.e(TAG, msg);
throw new SQLiteException(msg);
}
return dbPath;
}
/**
* Create and/or open a database that will be used for reading and writing.
* The first time this is called, the database will be opened and
* {@link #onCreate} and if applicable {@link #onUpgrade} will be called.
* <p/>
* <p>Once opened successfully, the database is cached, so you can
* call {@link #getDatabase()} to get access to the database.
* (Make sure to call {@link #close} when you no longer need the database.)
* Errors such as bad permissions or a full disk may cause this method
* to fail, but future attempts may succeed if the problem is fixed.</p>
* <p/>
* <p class="caution">Database upgrade may take a long time, you
* should not call this method from the application main thread, including
* from {@link android.content.ContentProvider#onCreate ContentProvider.onCreate()}.
*
* @param storageProvider the storage provider to be used to reference the database file.
* @throws android.database.sqlite.SQLiteException if the database cannot be opened for writing
*/
public synchronized void openOrCreateDatabase(StorageProvider storageProvider) {
if (mDatabase != null && mDatabase.isOpen() && !mDatabase.isReadOnly()) {
return; // The database is already open for business
}
if (mIsInitializing) {
throw new IllegalStateException("openOrCreateDatabase called recursively");
}
// If we have a read-only database open, someone could be using it
// (though they shouldn't), which would cause a lock to be held on
// the file, and our attempts to open the database read-write would
// fail waiting for the file lock. To prevent that, we acquire the
// lock on the read-only database, which shuts out other users.
boolean success = false;
SQLiteDatabase db = null;
//if (mDatabase != null) mDatabase.lock();
try {
mIsInitializing = true;
final File dbPath = getDatabasePath(storageProvider);
if (mName == null) {
db = SQLiteDatabase.create(null);
} else {
db = SQLiteDatabase.openOrCreateDatabase(dbPath, mFactory);
}
int version = db.getVersion();
if (version != mNewVersion) {
db.beginTransaction();
try {
if (version == 0) {
onCreate(db);
} else {
if (version > mNewVersion) {
Log.e(TAG, "Can't downgrade read-only database from version " +
version + " to " + mNewVersion + ": " + db.getPath());
}
onUpgrade(db, version, mNewVersion);
}
db.setVersion(mNewVersion);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
success = true;
} finally {
mIsInitializing = false;
if (success) {
if (mDatabase != null) {
try {
mDatabase.close();
} catch (Exception e) {
Log.w(TAG, "Failed to close database", e);
}
//mDatabase.unlock();
}
mDatabase = db;
} else {
//if (mDatabase != null) mDatabase.unlock();
if (db != null) db.close();
}
}
}
public synchronized SQLiteDatabase getDatabase() {
return mDatabase;
}
public synchronized SQLiteDatabase getReadOnlyDatabase() {
return mDatabase;
}
/**
* Close any open database object.
*/
public synchronized void close() {
if (mIsInitializing) throw new IllegalStateException("Closed during initialization");
if (mDatabase != null && mDatabase.isOpen()) {
mDatabase.close();
mDatabase = null;
}
}
/**
* Called when the database is created for the first time. This is where the
* creation of tables and the initial population of the tables should happen.
*
* @param db The database.
*/
public abstract void onCreate(SQLiteDatabase db);
/**
* Called when the database needs to be upgraded. The implementation
* should use this method to drop tables, add tables, or do anything else it
* needs to upgrade to the new schema version.
* <p/>
* <p>The SQLite ALTER TABLE documentation can be found
* <a href="http://sqlite.org/lang_altertable.html">here</a>. If you add new columns
* you can use ALTER TABLE to insert them into a live table. If you rename or remove columns
* you can use ALTER TABLE to rename the old table, then create the new table and then
* populate the new table with the contents of the old table.
*
* @param db The database.
* @param oldVersion The old database version.
* @param newVersion The new database version.
*/
public abstract void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);
}