1
0
mirror of https://github.com/moparisthebest/k-9 synced 2024-12-25 00:58:50 -05:00

Factor out store locking code in an intermediate DB access layer.

This commit is contained in:
Fiouz 2010-12-18 22:56:40 +00:00
parent c5f7dbf028
commit ab5caba92c
4 changed files with 802 additions and 735 deletions

View File

@ -35,18 +35,6 @@ import java.util.concurrent.ConcurrentHashMap;
*/
public class Account implements BaseAccount
{
/**
* @see Account#setLocalStoreMigrationListener(LocalStoreMigrationListener)
*
*/
public interface LocalStoreMigrationListener
{
void onLocalStoreMigration(String oldStoreUri,
String newStoreUri) throws MessagingException;
}
public static final String EXPUNGE_IMMEDIATELY = "EXPUNGE_IMMEDIATELY";
public static final String EXPUNGE_MANUALLY = "EXPUNGE_MANUALLY";
public static final String EXPUNGE_ON_POLL = "EXPUNGE_ON_POLL";
@ -89,7 +77,6 @@ public class Account implements BaseAccount
* the moment.
*/
private final boolean mIsInUse = false;
private LocalStoreMigrationListener mLocalStoreMigrationListener;
private String mTransportUri;
private String mDescription;
private String mAlwaysBcc;
@ -1389,12 +1376,11 @@ public class Account implements BaseAccount
* Never <code>null</code>.
* @throws MessagingException
*/
public void switchLocalStorage(String newStorageProviderId) throws MessagingException
public void switchLocalStorage(final String newStorageProviderId) throws MessagingException
{
if (this.mLocalStoreMigrationListener != null && !mLocalStorageProviderId.equals(newStorageProviderId))
if (!mLocalStorageProviderId.equals(newStorageProviderId))
{
mLocalStoreMigrationListener.onLocalStoreMigration(mLocalStorageProviderId,
newStorageProviderId);
getLocalStore().switchLocalStorage(newStorageProviderId);
}
}
@ -1555,19 +1541,6 @@ public class Account implements BaseAccount
return mIsInUse;
}
/**
* Set a listener to be informed when the underlying {@link StorageProvider}
* of the {@link LocalStore} of this account changes. (e.g. via
* {@link #switchLocalStorage(Context, String)})
*
* @param listener
* @see #switchLocalStorage(Context, String)
*/
public void setLocalStoreMigrationListener(LocalStoreMigrationListener listener)
{
this.mLocalStoreMigrationListener = listener;
}
public synchronized CryptoProvider getCryptoProvider()
{
if (mCryptoProvider == null)

View File

@ -2,10 +2,16 @@
package com.fsck.k9.helper;
import android.text.Editable;
import android.util.Log;
import android.widget.EditText;
import android.widget.TextView;
import com.fsck.k9.K9;
import com.fsck.k9.mail.filter.Base64;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@ -496,4 +502,126 @@ public class Utility
}
}
/**
* @param parentDir
* @param name
* Never <code>null</code>.
*/
public static void touchFile(final File parentDir, final String name)
{
final File file = new File(parentDir, name);
try
{
if (!file.exists())
{
file.createNewFile();
}
else
{
file.setLastModified(System.currentTimeMillis());
}
}
catch (Exception e)
{
Log.d(K9.LOG_TAG, "Unable to touch file: " + file.getAbsolutePath(), e);
}
}
/**
* @param from
* @param to
* @return
*/
public static boolean move(final File from, final File to)
{
if (to.exists())
{
to.delete();
}
to.getParentFile().mkdirs();
try
{
FileInputStream in = new FileInputStream(from);
FileOutputStream out = new FileOutputStream(to);
byte[] buffer = new byte[1024];
int count = -1;
while ((count = in.read(buffer)) > 0)
{
out.write(buffer, 0, count);
}
out.close();
in.close();
from.delete();
return true;
}
catch (Exception e)
{
Log.w(K9.LOG_TAG, "cannot move " + from.getAbsolutePath() + " to " + to.getAbsolutePath(), e);
return false;
}
}
/**
* @param fromDir
* @param toDir
*/
public static void moveRecursive(final File fromDir, final File toDir)
{
if (!fromDir.exists())
{
return;
}
if (!fromDir.isDirectory())
{
if (toDir.exists())
{
if (!toDir.delete())
{
Log.w(K9.LOG_TAG, "cannot delete already existing file/directory " + toDir.getAbsolutePath());
}
}
if (!fromDir.renameTo(toDir))
{
Log.w(K9.LOG_TAG, "cannot rename " + fromDir.getAbsolutePath() + " to " + toDir.getAbsolutePath() + " - moving instead");
move(fromDir, toDir);
}
return;
}
if (!toDir.exists() || !toDir.isDirectory())
{
if (toDir.exists() )
{
toDir.delete();
}
if (!toDir.mkdirs())
{
Log.w(K9.LOG_TAG, "cannot create directory " + toDir.getAbsolutePath());
}
}
File[] files = fromDir.listFiles();
for (File file : files)
{
if (file.isDirectory())
{
moveRecursive(file, new File(toDir, file.getName()));
file.delete();
}
else
{
File target = new File(toDir, file.getName());
if (!file.renameTo(target))
{
Log.w(K9.LOG_TAG, "cannot rename " + file.getAbsolutePath() + " to " + target.getAbsolutePath() + " - moving instead");
move(file, target);
}
}
}
if (!fromDir.delete())
{
Log.w(K9.LOG_TAG, "cannot delete " + fromDir.getAbsolutePath());
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,591 @@
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 <T>
* Return value type for {@link #doDbWork(SQLiteDatabase)}
*/
public static interface DbCallback<T>
{
/**
* @param db
* The locked database on which the work should occur. Never
* <code>null</code>.
* @return Any relevant data. Can be <code>null</code>.
* @throws WrappedException
* @throws UnavailableStorageException
*/
T doDbWork(SQLiteDatabase db) throws WrappedException, UnavailableStorageException;
}
public static interface SchemaDefinition
{
int getVersion();
/**
* @param db Never <code>null</code>.
*/
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 ThreadLocal<Boolean> inTransaction = new ThreadLocal<Boolean>();
private SchemaDefinition mSchemaDefinition;
private String uUid;
/**
* @param application
* Never <code>null</code>.
* @param uUid
* Never <code>null</code>.
* @param schemaDefinition
* Never <code>null</code
*/
public LockableDatabase(final Application application, final String uUid, final SchemaDefinition schemaDefinition)
{
this.mApplication = application;
this.uUid = uUid;
this.mSchemaDefinition = schemaDefinition;
}
public void setStorageProviderId(String mStorageProviderId)
{
this.mStorageProviderId = mStorageProviderId;
}
public String getStorageProviderId()
{
return mStorageProviderId;
}
private StorageManager getStorageManager()
{
return StorageManager.getInstance(mApplication);
}
/**
* Lock the storage for shared operations (concurrent threads are allowed to
* run simultaneously).
*
* <p>
* You <strong>have to</strong> invoke {@link #unlockRead()} when you're
* done with the storage.
* </p>
*
* @throws UnavailableStorageException
* If storage can't be locked because it is not available
*/
protected void lockRead() throws UnavailableStorageException
{
mReadLock.lock();
try
{
getStorageManager().lockProvider(mStorageProviderId);
}
catch (UnavailableStorageException e)
{
mReadLock.unlock();
throw e;
}
catch (RuntimeException e)
{
mReadLock.unlock();
throw e;
}
}
protected void unlockRead()
{
getStorageManager().unlockProvider(mStorageProviderId);
mReadLock.unlock();
}
/**
* Lock the storage for exclusive access (other threads aren't allowed to
* run simultaneously)
*
* <p>
* You <strong>have to</strong> invoke {@link #unlockWrite()} when you're
* done with the storage.
* </p>
*
* @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)
*
* <p>
* You <strong>have to</strong> invoke {@link #unlockWrite()} when you're
* done with the storage.
* </p>
*
* @param providerId
* Never <code>null</code>.
*
* @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.
*
* <p>
* 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).
* </p>
*
* @param transactional
* <code>true</code> the callback must be executed in a
* transactional context.
* @param callback
* Never <code>null</code>.
*
* @param <T>
* @return Whatever {@link DbCallback#doDbWork(SQLiteDatabase)} returns.
* @throws UnavailableStorageException
*/
public <T> T execute(final boolean transactional, final DbCallback<T> callback) throws UnavailableStorageException
{
lockRead();
final boolean doTransaction = transactional && inTransaction.get() == null;
try
{
final boolean debug = K9.DEBUG;
if (doTransaction)
{
inTransaction.set(Boolean.TRUE);
mDb.beginTransaction();
}
try
{
final T result = callback.doDbWork(mDb);
if (doTransaction)
{
mDb.setTransactionSuccessful();
}
return result;
}
finally
{
if (doTransaction)
{
final long begin;
if (debug)
{
begin = System.currentTimeMillis();
}
else
{
begin = 0l;
}
// not doing endTransaction in the same 'finally' block of unlockRead() because endTransaction() may throw an exception
mDb.endTransaction();
if (debug)
{
Log.v(K9.LOG_TAG, "LockableDatabase: Transaction ended, took " + Long.toString(System.currentTimeMillis() - begin) + "ms / " + new Exception().getStackTrace()[1].toString());
}
}
}
}
finally
{
if (doTransaction)
{
inTransaction.set(null);
}
unlockRead();
}
}
/**
* @param newProviderId
* Never <code>null</code>.
* @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 <code>null</code>.
* @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 inconditionally 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
* <code>true</code> 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();
}
}
}