package com.fsck.k9.mail.store; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import android.app.Application; import android.content.Context; import android.os.Build; import android.os.Environment; import android.util.Log; import com.fsck.k9.K9; import com.fsck.k9.R; /** * Manager for different {@link StorageProvider} -classes that abstract access * to sd-cards, additional internal memory and other storage-locations. */ public class StorageManager { /** * Provides entry points (File objects) to an underlying storage, * alleviating the caller from having to know where that storage is located. * *
* Allow checking for the denoted storage availability since its lifecycle * can evolving (a storage might become unavailable at some time and be back * online later). *
*/ public static interface StorageProvider { /** * Retrieve the uniquely identifier for the current implementation. * ** It is expected that the identifier doesn't change over reboots since * it'll be used to save settings and retrieve the provider at a later * time. *
* ** The returned identifier doesn't have to be user friendly. *
* * @return Nevernull
.
*/
String getId();
/**
* Hook point for provider initialization.
*
* @param context
* Never null
.
*/
void init(Context context);
/**
* @param context
* Never null
.
* @return A user displayable, localized name for this provider. Never
* null
.
*/
String getName(Context context);
/**
* Some implementations may not be able to return valid File handles
* because the device doesn't provide the denoted storage. You can check
* the provider compatibility with this method to prevent from having to
* invoke this provider ever again.
*
* @param context
* TODO
* @return Whether this provider supports the current device.
* @see StorageManager#getAvailableProviders()
*/
boolean isSupported(Context context);
/**
* Return the {@link File} to the choosen email database file. The
* resulting {@link File} doesn't necessarily match an existing file on
* the filesystem.
*
* @param context
* Never null
.
* @param id
* Never null
.
* @return Never null
.
*/
File getDatabase(Context context, String id);
/**
* Return the {@link File} to the choosen attachment directory. The
* resulting {@link File} doesn't necessarily match an existing
* directory on the filesystem.
*
* @param context
* Never null
.
* @param id
* Never null
.
* @return Never null
.
*/
File getAttachmentDirectory(Context context, String id);
/**
* Check for the underlying storage availability.
*
* @param context
* Never null
.
* @return Whether the underlying storage returned by this provider is
* ready for read/write operations at the time of invokation.
*/
boolean isReady(Context context);
/**
* Retrieve the root of the underlying storage.
*
* @param context
* Never null
.
* @return The root directory of the denoted storage. Never
* null
.
*/
File getRoot(Context context);
}
/**
* Interface for components wanting to be notified of storage availability
* events.
*/
public static interface StorageListener {
/**
* Invoked on storage mount (with read/write access).
*
* @param providerId
* Identifier (as returned by {@link StorageProvider#getId()}
* of the newly mounted storage. Never null
.
*/
void onMount(String providerId);
/**
* Invoked when a storage is about to be unmounted.
*
* @param providerId
* Identifier (as returned by {@link StorageProvider#getId()}
* of the to-be-unmounted storage. Never null
.
*/
void onUnmount(String providerId);
}
/**
* Base provider class for providers that rely on well-known path to check
* for storage availability.
*
* * Since solely checking for paths can be unsafe, this class allows to check * for device compatibility using {@link #supportsVendor()}. If the vendor * specific check fails, the provider won't be able to provide any valid * File handle, regardless of the path existence. *
* ** Moreover, this class validates the denoted storage path against mount * points using {@link StorageManager#isMountPoint(File)}. *
*/ public abstract static class FixedStorageProviderBase implements StorageProvider { /** * The root of the denoted storage. Used for mount points checking. */ protected File mRoot; /** * Choosen base directory */ protected File mApplicationDir; @Override public void init(final Context context) { mRoot = computeRoot(context); // usenull
.
* @return Never null
.
*/
protected abstract File computeRoot(Context context);
}
/**
* Strategy to access the always available internal storage.
*
* * This implementation is expected to work on every device since it's based * on the regular Android API {@link Context#getDatabasePath(String)} and * uses the resul to retrieve the DB path and the attachment directory path. *
* ** The underlying storage has always been used by K-9. *
*/ public static class InternalStorageProvider implements StorageProvider { public static final String ID = "InternalStorage"; protected File mRoot; @Override public String getId() { return ID; } @Override public void init(Context context) { // XXX mRoot = new File("/"); } @Override public String getName(Context context) { return context.getString(R.string.local_storage_provider_internal_label); } @Override public boolean isSupported(Context context) { return true; } @Override public File getDatabase(Context context, String id) { return context.getDatabasePath(id + ".db"); } @Override public File getAttachmentDirectory(Context context, String id) { // we store attachments in the database directory return context.getDatabasePath(id + ".db_att"); } @Override public boolean isReady(Context context) { return true; } @Override public File getRoot(Context context) { return mRoot; } } /** * Strategy for accessing the storage as returned by * {@link Environment#getExternalStorageDirectory()}. In order to be * compliant with Android recommendation regarding application uninstalling * and to prevent from cluttering the storage root, the choosen directory * will be *<STORAGE_ROOT>/Android/data/<APPLICATION_PACKAGE_NAME>/files/
*
* * The denoted storage is usually a SD card. *
* ** This provider is expected to work on all devices but the returned * underlying storage might not be always available, due to * mount/unmount/USB share events. *
*/ public static class ExternalStorageProvider implements StorageProvider { public static final String ID = "ExternalStorage"; /** * Root of the denoted storage. */ protected File mRoot; /** * Choosen base directory. */ protected File mApplicationDirectory; public String getId() { return ID; } @Override public void init(Context context) { mRoot = Environment.getExternalStorageDirectory(); mApplicationDirectory = new File(new File(new File(new File(mRoot, "Android"), "data"), context.getPackageName()), "files"); } @Override public String getName(Context context) { return context.getString(R.string.local_storage_provider_external_label); } @Override public boolean isSupported(Context context) { return true; } @Override public File getDatabase(Context context, String id) { return new File(mApplicationDirectory, id + ".db"); } @Override public File getAttachmentDirectory(Context context, String id) { return new File(mApplicationDirectory, id + ".db_att"); } @Override public boolean isReady(Context context) { return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()); } @Override public File getRoot(Context context) { return mRoot; } } /** * Storage provider to allow access the /emmc directory on a HTC Incredible. * ** This implementation is experimental and _untested_. *
* * See http://groups.google.com/group/android-developers/browse_frm/thread/96f15e57150ed173 * * @see FixedStorageProviderBase */ public static class HtcIncredibleStorageProvider extends FixedStorageProviderBase { public static final String ID = "HtcIncredibleStorage"; public String getId() { return ID; } @Override public String getName(Context context) { return context.getString(R.string.local_storage_provider_samsunggalaxy_label, Build.MODEL); } @Override protected boolean supportsVendor() { return "inc".equals(Build.DEVICE); } @Override protected File computeRoot(Context context) { return new File("/emmc"); } } /** * Storage provider to allow access the Samsung Galaxy S 'internal SD card'. * ** This implementation is experimental and _untested_. *
* * See http://groups.google.com/group/android-developers/browse_frm/thread/a1adf7122a75a657 * * @see FixedStorageProviderBase */ public static class SamsungGalaxySStorageProvider extends FixedStorageProviderBase { public static final String ID = "SamsungGalaxySStorage"; public String getId() { return ID; } @Override public String getName(Context context) { return context.getString(R.string.local_storage_provider_samsunggalaxy_label, Build.MODEL); } @Override protected boolean supportsVendor() { // FIXME return "GT-I5800".equals(Build.DEVICE) || "GT-I9000".equals(Build.DEVICE) || "SGH-T959".equals(Build.DEVICE) || "SGH-I897".equals(Build.DEVICE); } @Override protected File computeRoot(Context context) { return Environment.getExternalStorageDirectory(); // was: new // File("/sdcard") } } /** * Stores storage provider locking informations */ public static class SynchronizationAid { /** * {@link Lock} has a thread semantic so it can't be released from * another thread - this flags act as a holder for the unmount state */ public boolean unmounting = false; public final Lock readLock; public final Lock writeLock; { final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true); readLock = readWriteLock.readLock(); writeLock = readWriteLock.writeLock(); } } /** * The active storage providers. */ private final Mapnull
.
* @return Whether the specified file matches a filesystem root.
* @throws IOException
*/
public static boolean isMountPoint(final File file) {
for (final File root : File.listRoots()) {
if (root.equals(file)) {
return true;
}
}
return false;
}
/**
* @param application
* Never null
.
* @throws NullPointerException
* If application is null
.
*/
protected StorageManager(final Application application) throws NullPointerException {
if (application == null) {
throw new NullPointerException("No application instance given");
}
mApplication = application;
/*
* 20101113/fiouzy:
*
* Here is where we define which providers are used, currently we only
* allow the internal storage and the regular external storage.
*
* HTC Incredible storage and Samsung Galaxy S are omitted on purpose
* (they're experimental and I don't have those devices to test).
*
*
* !!! Make sure InternalStorageProvider is the first provider as it'll
* be considered as the default provider !!!
*/
final Listnull
.
*/
public String getDefaultProviderId() {
// assume there is at least 1 provider defined
return mProviders.entrySet().iterator().next().getKey();
}
/**
* @param providerId
* Never null
.
* @return null
if not found.
*/
protected StorageProvider getProvider(final String providerId) {
return mProviders.get(providerId);
}
/**
* @param dbName
* Never null
.
* @param providerId
* Never null
.
* @return The resolved database file for the given provider ID.
*/
public File getDatabase(final String dbName, final String providerId) {
StorageProvider provider = getProvider(providerId);
// TODO fallback to internal storage if no provider
return provider.getDatabase(mApplication, dbName);
}
/**
* @param dbName
* Never null
.
* @param providerId
* Never null
.
* @return The resolved attachement directory for the given provider ID.
*/
public File getAttachmentDirectory(final String dbName, final String providerId) {
StorageProvider provider = getProvider(providerId);
// TODO fallback to internal storage if no provider
return provider.getAttachmentDirectory(mApplication, dbName);
}
/**
* @param providerId
* Never null
.
* @return Whether the specified provider is ready for read/write operations
*/
public boolean isReady(final String providerId) {
StorageProvider provider = getProvider(providerId);
if (provider == null) {
Log.w(K9.LOG_TAG, "Storage-Provider \"" + providerId + "\" does not exist");
return false;
}
return provider.isReady(mApplication);
}
/**
* @return A map of available providers names, indexed by their ID. Never
* null
.
* @see StorageManager
* @see StorageProvider#isSupported(Context)
*/
public Mapnull
.
* @return The corresponding provider. null
if no match.
*/
protected StorageProvider resolveProvider(final String path) {
for (final StorageProvider provider : mProviders.values()) {
if (path.equals(provider.getRoot(mApplication).getAbsolutePath())) {
return provider;
}
}
return null;
}
public void addListener(final StorageListener listener) {
mListeners.add(listener);
}
public void removeListener(final StorageListener listener) {
mListeners.remove(listener);
}
/**
* Try to lock the underlying storage to prevent concurrent unmount.
*
* * You must invoke {@link #unlockProvider(String)} when you're done with the * storage. *
* * @param providerId * @throws UnavailableStorageException * If the storage can't be locked. */ public void lockProvider(final String providerId) throws UnavailableStorageException { final StorageProvider provider = getProvider(providerId); if (provider == null) { throw new UnavailableStorageException("StorageProvider not found: " + providerId); } // lock provider final SynchronizationAid sync = mProviderLocks.get(provider); final boolean locked = sync.readLock.tryLock(); if (!locked || (locked && sync.unmounting)) { if (locked) { sync.readLock.unlock(); } throw new UnavailableStorageException("StorageProvider is unmounting"); } else if (locked && !provider.isReady(mApplication)) { sync.readLock.unlock(); throw new UnavailableStorageException("StorageProvider not ready"); } } public void unlockProvider(final String providerId) { final StorageProvider provider = getProvider(providerId); final SynchronizationAid sync = mProviderLocks.get(provider); sync.readLock.unlock(); } }