package com.fsck.k9.mail.store;
import java.io.File;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import android.app.Application;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.util.Log;
import com.fsck.k9.K9;
import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.MessagingException;
public class LockableDatabase {
/**
* Callback interface for DB operations. Concept is similar to Spring
* HibernateCallback.
*
* @param null
.
* @return Any relevant data. Can be null
.
* @throws WrappedException
* @throws UnavailableStorageException
*/
T doDbWork(SQLiteDatabase db) throws WrappedException, UnavailableStorageException;
}
public static interface SchemaDefinition {
int getVersion();
/**
* @param db Never null
.
*/
void doDbUpgrade(SQLiteDatabase db);
}
/**
* Workaround exception wrapper used to keep the inner exception generated
* in a {@link DbCallback}.
*/
protected static class WrappedException extends RuntimeException {
/**
*
*/
private static final long serialVersionUID = 8184421232587399369L;
public WrappedException(final Exception cause) {
super(cause);
}
}
/**
* Open the DB on mount and close the DB on unmount
*/
private class StorageListener implements StorageManager.StorageListener {
@Override
public void onUnmount(final String providerId) {
if (!providerId.equals(mStorageProviderId)) {
return;
}
if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "LockableDatabase: Closing DB " + uUid + " due to unmount event on StorageProvider: " + providerId);
}
try {
lockWrite();
try {
mDb.close();
} finally {
unlockWrite();
}
} catch (UnavailableStorageException e) {
Log.w(K9.LOG_TAG, "Unable to writelock on unmount", e);
}
}
@Override
public void onMount(final String providerId) {
if (!providerId.equals(mStorageProviderId)) {
return;
}
if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "LockableDatabase: Opening DB " + uUid + " due to mount event on StorageProvider: " + providerId);
}
try {
openOrCreateDataspace(mApplication);
} catch (UnavailableStorageException e) {
Log.e(K9.LOG_TAG, "Unable to open DB on mount", e);
}
}
}
private String mStorageProviderId;
private SQLiteDatabase mDb;
/**
* Reentrant read lock
*/
private final Lock mReadLock;
/**
* Reentrant write lock (if you lock it 2x from the same thread, you have to
* unlock it 2x to release it)
*/
private final Lock mWriteLock;
{
final ReadWriteLock lock = new ReentrantReadWriteLock(true);
mReadLock = lock.readLock();
mWriteLock = lock.writeLock();
}
private final StorageListener mStorageListener = new StorageListener();
private Application mApplication;
/**
* {@link ThreadLocal} to check whether a DB transaction is occuring in the
* current {@link Thread}.
*
* @see #execute(boolean, DbCallback)
*/
private ThreadLocalnull
.
* @param uUid
* Never null
.
* @param schemaDefinition
* Never null
* You have to invoke {@link #unlockRead()} when you're
* done with the storage.
*
* You have to invoke {@link #unlockWrite()} when you're * done with the storage. *
* * @throws UnavailableStorageException * If storage can't be locked because it is not available. */ protected void lockWrite() throws UnavailableStorageException { lockWrite(mStorageProviderId); } /** * Lock the storage for exclusive access (other threads aren't allowed to * run simultaneously) * ** You have to invoke {@link #unlockWrite()} when you're * done with the storage. *
* * @param providerId * Nevernull
.
*
* @throws UnavailableStorageException
* If storage can't be locked because it is not available.
*/
protected void lockWrite(final String providerId) throws UnavailableStorageException {
mWriteLock.lock();
try {
getStorageManager().lockProvider(providerId);
} catch (UnavailableStorageException e) {
mWriteLock.unlock();
throw e;
} catch (RuntimeException e) {
mWriteLock.unlock();
throw e;
}
}
protected void unlockWrite() {
unlockWrite(mStorageProviderId);
}
protected void unlockWrite(final String providerId) {
getStorageManager().unlockProvider(providerId);
mWriteLock.unlock();
}
/**
* Execute a DB callback in a shared context (doesn't prevent concurrent
* shared executions), taking care of locking the DB storage.
*
* * Can be instructed to start a transaction if none is currently active in * the current thread. Callback will participe in any active transaction (no * inner transaction created). *
* * @param transactional *true
the callback must be executed in a
* transactional context.
* @param callback
* Never null
.
*
* @param null
.
* @throws MessagingException
*/
public void switchProvider(final String newProviderId) throws MessagingException {
if (newProviderId.equals(mStorageProviderId)) {
Log.v(K9.LOG_TAG, "LockableDatabase: Ignoring provider switch request as they are equal: " + newProviderId);
return;
}
final String oldProviderId = mStorageProviderId;
lockWrite(oldProviderId);
try {
lockWrite(newProviderId);
try {
try {
mDb.close();
} catch (Exception e) {
Log.i(K9.LOG_TAG, "Unable to close DB on local store migration", e);
}
final StorageManager storageManager = getStorageManager();
// create new path
prepareStorage(newProviderId);
// move all database files
Utility.moveRecursive(storageManager.getDatabase(uUid, oldProviderId), storageManager.getDatabase(uUid, newProviderId));
// move all attachment files
Utility.moveRecursive(storageManager.getAttachmentDirectory(uUid, oldProviderId), storageManager.getAttachmentDirectory(uUid, newProviderId));
mStorageProviderId = newProviderId;
// re-initialize this class with the new Uri
openOrCreateDataspace(mApplication);
} finally {
unlockWrite(newProviderId);
}
} finally {
unlockWrite(oldProviderId);
}
}
public void open() throws UnavailableStorageException {
lockWrite();
try {
openOrCreateDataspace(mApplication);
} finally {
unlockWrite();
}
StorageManager.getInstance(mApplication).addListener(mStorageListener);
}
/**
*
* @param application
* @throws UnavailableStorageException
*/
protected void openOrCreateDataspace(final Application application) throws UnavailableStorageException {
lockWrite();
try {
final File databaseFile = prepareStorage(mStorageProviderId);
try {
mDb = SQLiteDatabase.openOrCreateDatabase(databaseFile, null);
} catch (SQLiteException e) {
// try to gracefully handle DB corruption - see issue 2537
Log.w(K9.LOG_TAG, "Unable to open DB " + databaseFile + " - removing file and retrying", e);
databaseFile.delete();
mDb = SQLiteDatabase.openOrCreateDatabase(databaseFile, null);
}
if (mDb.getVersion() != mSchemaDefinition.getVersion()) {
mSchemaDefinition.doDbUpgrade(mDb);
}
} finally {
unlockWrite();
}
}
/**
* @param providerId
* Never null
.
* @return DB file.
* @throws UnavailableStorageException
*/
protected File prepareStorage(final String providerId) throws UnavailableStorageException {
final StorageManager storageManager = getStorageManager();
final File databaseFile;
final File databaseParentDir;
databaseFile = storageManager.getDatabase(uUid, providerId);
databaseParentDir = databaseFile.getParentFile();
if (databaseParentDir.isFile()) {
// should be safe to unconditionally delete clashing file: user is not supposed to mess with our directory
databaseParentDir.delete();
}
if (!databaseParentDir.exists()) {
if (!databaseParentDir.mkdirs()) {
// Android seems to be unmounting the storage...
throw new UnavailableStorageException("Unable to access: " + databaseParentDir);
}
Utility.touchFile(databaseParentDir, ".nomedia");
}
final File attachmentDir;
final File attachmentParentDir;
attachmentDir = storageManager
.getAttachmentDirectory(uUid, providerId);
attachmentParentDir = attachmentDir.getParentFile();
if (!attachmentParentDir.exists()) {
attachmentParentDir.mkdirs();
Utility.touchFile(attachmentParentDir, ".nomedia");
}
if (!attachmentDir.exists()) {
attachmentDir.mkdirs();
}
return databaseFile;
}
/**
* Delete the backing database.
*
* @throws UnavailableStorageException
*/
public void delete() throws UnavailableStorageException {
delete(false);
}
public void recreate() throws UnavailableStorageException {
delete(true);
}
/**
* @param recreate
* true
if the DB should be recreated after delete
* @throws UnavailableStorageException
*/
private void delete(final boolean recreate) throws UnavailableStorageException {
lockWrite();
try {
try {
mDb.close();
} catch (Exception e) {
}
final StorageManager storageManager = getStorageManager();
try {
final File attachmentDirectory = storageManager.getAttachmentDirectory(uUid, mStorageProviderId);
final File[] attachments = attachmentDirectory.listFiles();
for (File attachment : attachments) {
if (attachment.exists()) {
attachment.delete();
}
}
if (attachmentDirectory.exists()) {
attachmentDirectory.delete();
}
} catch (Exception e) {
}
try {
storageManager.getDatabase(uUid, mStorageProviderId).delete();
} catch (Exception e) {
Log.i(K9.LOG_TAG, "LockableDatabase: delete(): Unable to delete backing DB file", e);
}
if (recreate) {
openOrCreateDataspace(mApplication);
} else {
// stop waiting for mount/unmount events
getStorageManager().removeListener(mStorageListener);
}
} finally {
unlockWrite();
}
}
}