1
0
mirror of https://github.com/moparisthebest/k-9 synced 2024-08-13 17:03:48 -04:00

Merge branch 'mail-on-sd'

* mail-on-sd: (40 commits)
  Added more comments to explain how the locking mecanism works for LocalStore
  Fixed wrong method being called during experimental provider initialization (since provider isn't enabled, that didn't harm)
  Add more comments about how the various StorageProviders work and how they're enabled
  find src/com/fsck/ -name \*.java|xargs astyle --style=ansi --mode=java --indent-switches --indent=spaces=4 --convert-tabs
  French localization for storage related settings
  Remove unused SD card strings (replaced with storage indirection)
  Merge mail-on-sd branch from trunk
  Reset mail service on storage mount (even if no account uses the storage, to be improved)
  find src/com/fsck/ -name \*.java|xargs astyle --style=ansi --mode=java --indent-switches --indent=spaces=4 --convert-tabs
  Migraion -> Migration
  move the Storage location preference into preferences rather than the wizard.
  Made LocalStore log less verbose Added @Override compile checks
  Added ACTION_SHUTDOWN broadcast receiver to properly initiate shutdown sequence (not yet implemented) and cancel any scheduled Intent
  Be more consistent about which SQLiteDatabase variable is used (from instance variable to argument variable) to make code more refactoring-friendly (class is already big, code extraction should be easier if not referencing the instance variable).
  Added transaction timing logging
  Factorised storage lock/transaction handling code for regular operations.
  Use DB transactions to batch modifications (makes code more robust / could improve performances)
  Merge mail-on-sd branch from trunk
  Update issue 888 Added DB close on unmount / DB open on mount
  Update issue 888 Back to account list when underlying storage not available/unmounting in MessageView / MessageList
  ...
This commit is contained in:
Jesse Vincent 2010-11-13 21:40:56 +00:00
parent 4449642410
commit 14055691a3
36 changed files with 4293 additions and 1323 deletions

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>k9mail</name>
<name>k9mail-sdcard</name>
<comment></comment>
<projects>
</projects>

View File

@ -315,6 +315,29 @@
<action android:name="com.fsck.k9.service.CoreReceiver.wakeLockRelease"/>
</intent-filter>
</receiver>
<receiver android:name="com.fsck.k9.service.StorageReceiver"
android:enabled="true"
>
<intent-filter>
<!--
android.intent.action.MEDIA_MOUNTED
* Broadcast Action: External media is present and mounted at its mount point.
* The path to the mount point for the removed media is contained in the Intent.mData field.
* The Intent contains an extra with name "read-only" and Boolean value to indicate if the
* media was mounted read only.
-->
<action android:name="android.intent.action.MEDIA_MOUNTED"/>
<!--
MEDIA_EJECT and MEDIA_UNMOUNTED are not defined here: they have to be dynamically registered
otherwise it would make K-9 start at the wrong time
-->
<data android:scheme="file"/>
</intent-filter>
</receiver>
<service
android:name="com.fsck.k9.service.MailService"
android:enabled="true"

View File

@ -392,6 +392,13 @@ Willkommen zum \"K-9 Mail\"-Setup. K-9 ist eine quelloffene E-Mail-Anwendung fü
<string name="account_setup_incoming_save_all_headers_title">Alle Mail-Header herunterladen</string>
<string name="account_setup_incoming_save_all_headers_label">Alle Header lokal speichern</string>
<string name="account_setup_incoming_use_sd_card_title">Nutze SD-Karte</string>
<string name="account_setup_incoming_use_sd_card_label">Speichere die Mails auf der SD-Karte</string>
<string name="local_storage_provider_external_label">Externes Medium (SD-Karte)</string>
<string name="local_storage_provider_internal_label">Interner Speicher</string>
<string name="local_storage_provider_samsunggalaxy_label">%1$s zusätzlicher interner Speicher</string>
<string name="local_storage_provider_label">Speicherort</string>
<string name="account_setup_expunge_policy_label">Ordner bereinigen (Expunge)</string>
<string name="account_setup_expunge_policy_immediately">Sofort nach Verschieben oder Kopieren</string>
<string name="account_setup_expunge_policy_on_poll">Bei jedem Abrufen</string>

View File

@ -364,6 +364,12 @@
<string name="account_setup_incoming_other_label">Autre</string>
<string name="account_setup_incoming_save_all_headers_title">Téléchargement des entêtes de messages</string>
<string name="account_setup_incoming_save_all_headers_label">Enregistrer toutes les entêtes localement</string>
<string name="local_storage_provider_external_label">Stockage externe (carte SD)</string>
<string name="local_storage_provider_internal_label">Stockage interne</string>
<string name="local_storage_provider_samsunggalaxy_label">Stockage additionnel %1$s</string>
<string name="local_storage_provider_label">Emplacement du stockage</string>
<string name="account_setup_expunge_policy_label">Élimination des messages</string>
<string name="account_setup_expunge_policy_immediately">Immédiatement après avoir supprimé ou déplacé</string>
<string name="account_setup_expunge_policy_on_poll">Pendant chaque récupération</string>
@ -502,6 +508,10 @@
<string name="account_settings_mail_check_frequency_label">Fréquence de vérification du dossier</string>
<string name="account_settings_second_class_check_frequency_label">Fréquence de vérification pour 2ème classe</string>
<string name="account_settings_storage_title">Stockage</string>
<string name="account_settings_color_label">Couleur du compte</string>
<string name="account_settings_color_summary">Choisir la couleur du compte tel qu\'affichée dans les listes de dossiers ou de comptes</string>
<string name="account_settings_led_color_label">Couleur de la DEL de notification</string>

View File

@ -398,6 +398,11 @@ Welcome to K-9 Mail setup. K-9 is an open source mail client for Android origin
<string name="account_setup_incoming_save_all_headers_title">Download headers</string>
<string name="account_setup_incoming_save_all_headers_label">Save all message headers locally</string>
<string name="local_storage_provider_external_label">External storage (SD card)</string>
<string name="local_storage_provider_internal_label">Regular internal storage</string>
<string name="local_storage_provider_samsunggalaxy_label">%1$s additional internal storage</string>
<string name="local_storage_provider_label">Storage location</string>
<string name="account_setup_expunge_policy_label">Expunge deleted messages</string>
<string name="account_setup_expunge_policy_immediately">Immediately</string>
<string name="account_setup_expunge_policy_on_poll">When polling</string>
@ -558,6 +563,9 @@ Welcome to K-9 Mail setup. K-9 is an open source mail client for Android origin
<string name="account_settings_mail_check_frequency_label">Folder poll frequency</string>
<string name="account_settings_second_class_check_frequency_label">2nd class check frequency</string>
<string name="account_settings_storage_title">Storage</string>
<string name="account_settings_color_label">Account color</string>
<string name="account_settings_color_summary">Choose the color of the account used in folder and account list</string>

View File

@ -283,6 +283,17 @@
android:dialogTitle="@string/account_settings_searchable_label" />
</PreferenceScreen>
<PreferenceScreen
android:title="@string/account_settings_storage_title"
android:key="folders">
<ListPreference
android:persistent="false"
android:key="local_storage_provider"
android:title="@string/local_storage_provider_label"
android:dialogTitle="@string/local_storage_provider_label"
/>
</PreferenceScreen>
<PreferenceScreen
android:title="@string/account_settings_notifications"

View File

@ -15,7 +15,10 @@ import com.fsck.k9.mail.Folder;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Store;
import com.fsck.k9.mail.store.LocalStore;
import com.fsck.k9.mail.store.StorageManager;
import com.fsck.k9.mail.store.LocalStore.LocalFolder;
import com.fsck.k9.mail.store.StorageManager.StorageProvider;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
@ -32,6 +35,18 @@ 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";
@ -62,7 +77,19 @@ public class Account implements BaseAccount
private String mUuid;
private String mStoreUri;
private String mLocalStoreUri;
/**
* Storage provider ID, used to locate and manage the underlying DB/file
* storage
*/
private String mLocalStorageProviderId;
/**
* True if {@link #mLocalStoreUri} may be in use at
* the moment.
*/
private boolean mIsInUse = false;
private LocalStoreMigrationListener mLocalStoreMigrationListener;
private String mTransportUri;
private String mDescription;
private String mAlwaysBcc;
@ -146,9 +173,8 @@ public class Account implements BaseAccount
protected Account(Context context)
{
// TODO Change local store path to something readable / recognizable
mUuid = UUID.randomUUID().toString();
mLocalStoreUri = "local://localhost/" + context.getDatabasePath(mUuid + ".db");
mLocalStorageProviderId = StorageManager.getInstance(K9.app).getDefaultProviderId();
mAutomaticCheckIntervalMinutes = -1;
mIdleRefreshMinutes = 24;
mSaveAllHeaders = true;
@ -216,7 +242,7 @@ public class Account implements BaseAccount
mStoreUri = Utility.base64Decode(prefs.getString(mUuid
+ ".storeUri", null));
mLocalStoreUri = prefs.getString(mUuid + ".localStoreUri", null);
mLocalStorageProviderId = prefs.getString(mUuid + ".localStorageProvider", StorageManager.getInstance(K9.app).getDefaultProviderId());
mTransportUri = Utility.base64Decode(prefs.getString(mUuid
+ ".transportUri", null));
mDescription = prefs.getString(mUuid + ".description", null);
@ -508,7 +534,7 @@ public class Account implements BaseAccount
}
editor.putString(mUuid + ".storeUri", Utility.base64Encode(mStoreUri));
editor.putString(mUuid + ".localStoreUri", mLocalStoreUri);
editor.putString(mUuid + ".localStorageProvider", mLocalStorageProviderId);
editor.putString(mUuid + ".transportUri", Utility.base64Encode(mTransportUri));
editor.putString(mUuid + ".description", mDescription);
editor.putString(mUuid + ".alwaysBcc", mAlwaysBcc);
@ -575,7 +601,6 @@ public class Account implements BaseAccount
}
public void resetVisibleLimits()
{
try
@ -590,8 +615,18 @@ public class Account implements BaseAccount
}
/**
* @param context
* @return <code>null</code> if not available
* @throws MessagingException
* @see {@link #isAvailable(Context)}
*/
public AccountStats getStats(Context context) throws MessagingException
{
if (!isAvailable(context))
{
return null;
}
long startTime = System.currentTimeMillis();
AccountStats stats = new AccountStats();
int unreadMessageCount = 0;
@ -772,16 +807,45 @@ public class Account implements BaseAccount
mRingNotified = ringNotified;
}
public synchronized String getLocalStoreUri()
public String getLocalStorageProviderId()
{
return mLocalStoreUri;
return mLocalStorageProviderId;
}
public synchronized void setLocalStoreUri(String localStoreUri)
public void setLocalStorageProviderId(String id)
{
this.mLocalStoreUri = localStoreUri;
if (!mLocalStorageProviderId.equals(id))
{
boolean successful = false;
try
{
switchLocalStorage(id);
successful = true;
}
catch (MessagingException e)
{
}
finally
{
// if migration to/from SD-card failed once, it will fail again.
if (!successful)
{
return;
}
}
mLocalStorageProviderId = id;
}
}
// public synchronized void setLocalStoreUri(String localStoreUri)
// {
// this.mLocalStoreUri = localStoreUri;
// }
/**
* Returns -1 for never.
*/
@ -1316,6 +1380,26 @@ public class Account implements BaseAccount
mSaveAllHeaders = saveAllHeaders;
}
/**
* Are we storing out localStore on the SD-card instead of the local device
* memory?<br/>
* Only to be called durin initial account-setup!<br/>
* Side-effect: changes {@link #mLocalStorageProviderId}.
*
* @param context
* @param newStorageProviderId
* Never <code>null</code>.
* @throws MessagingException
*/
public void switchLocalStorage(String newStorageProviderId) throws MessagingException
{
if (this.mLocalStoreMigrationListener != null && !mLocalStorageProviderId.equals(newStorageProviderId))
{
mLocalStoreMigrationListener.onLocalStoreMigration(mLocalStorageProviderId,
newStorageProviderId);
}
}
public synchronized boolean goToUnreadMessageSearch()
{
return goToUnreadMessageSearch;
@ -1468,6 +1552,24 @@ public class Account implements BaseAccount
lastSelectedFolderName = folderName;
}
public boolean isInUse()
{
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)
@ -1482,4 +1584,18 @@ public class Account implements BaseAccount
return mNotificationSetting;
}
/**
* @return <code>true</code> if our {@link StorageProvider} is ready. (e.g.
* card inserted)
*/
public boolean isAvailable(Context context)
{
String localStorageProviderId = getLocalStorageProviderId();
if (localStorageProviderId == null)
{
return true; // defaults to internal memory
}
return StorageManager.getInstance(K9.app).isReady(localStorageProviderId);
}
}

View File

@ -5,16 +5,21 @@ import java.io.File;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import android.app.Application;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.webkit.WebSettings;
@ -27,6 +32,8 @@ import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.internet.BinaryTempFileBody;
import com.fsck.k9.service.BootReceiver;
import com.fsck.k9.service.MailService;
import com.fsck.k9.service.ShutdownReceiver;
import com.fsck.k9.service.StorageGoneReceiver;
public class K9 extends Application
{
@ -305,7 +312,7 @@ public class K9 extends Application
*/
public static void setServicesEnabled(Context context)
{
int acctLength = Preferences.getPreferences(context).getAccounts().length;
int acctLength = Preferences.getPreferences(context).getAvailableAccounts().size();
setServicesEnabled(context, acctLength > 0, null);
@ -313,7 +320,7 @@ public class K9 extends Application
public static void setServicesEnabled(Context context, Integer wakeLockId)
{
setServicesEnabled(context, Preferences.getPreferences(context).getAccounts().length > 0, wakeLockId);
setServicesEnabled(context, Preferences.getPreferences(context).getAvailableAccounts().size() > 0, wakeLockId);
}
public static void setServicesEnabled(Context context, boolean enabled, Integer wakeLockId)
@ -360,6 +367,56 @@ public class K9 extends Application
}
/**
* Register BroadcastReceivers programmaticaly because doing it from manifest
* would make K-9 auto-start. We don't want auto-start because the initialization
* sequence isn't safe while some events occur (SD card unmount).
*/
protected void registerReceivers()
{
final StorageGoneReceiver receiver = new StorageGoneReceiver();
final IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_MEDIA_EJECT);
filter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
filter.addDataScheme("file");
final BlockingQueue<Handler> queue = new SynchronousQueue<Handler>();
// starting a new thread to handle unmount events
new Thread(new Runnable()
{
@Override
public void run()
{
Looper.prepare();
try
{
queue.put(new Handler());
}
catch (InterruptedException e)
{
Log.e(K9.LOG_TAG, "", e);
}
Looper.loop();
}
}, "Unmount-thread").start();
try
{
final Handler storageGoneHandler = queue.take();
registerReceiver(receiver, filter, null, storageGoneHandler);
Log.i(K9.LOG_TAG, "Registered: unmount receiver");
}
catch (InterruptedException e)
{
Log.e(K9.LOG_TAG, "Unable to register unmount receiver", e);
}
registerReceiver(new ShutdownReceiver(), new IntentFilter(Intent.ACTION_SHUTDOWN));
Log.i(K9.LOG_TAG, "Registered: shutdown receiver");
}
public static void save(SharedPreferences.Editor editor)
{
editor.putBoolean("enableDebugLogging", K9.DEBUG);
@ -452,7 +509,7 @@ public class K9 extends Application
K9.setK9Language(sprefs.getString("language", ""));
K9.setK9Theme(sprefs.getInt("theme", android.R.style.Theme_Light));
MessagingController.getInstance(this).resetVisibleLimits(prefs.getAccounts());
MessagingController.getInstance(this).resetVisibleLimits(prefs.getAvailableAccounts());
/*
* We have to give MimeMessage a temp directory because File.createTempFile(String, String)
@ -465,6 +522,7 @@ public class K9 extends Application
*/
setServicesEnabled(this);
registerReceivers();
MessagingController.getInstance(this).addListener(new MessagingListener()
{

View File

@ -2,6 +2,8 @@
package com.fsck.k9;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import android.content.Context;
import android.content.SharedPreferences;
@ -33,10 +35,12 @@ public class Preferences
private Storage mStorage;
private List<Account> accounts;
private Account newAccount;
private Context mContext;
private Preferences(Context context)
{
mStorage = Storage.getStorage(context);
mContext = context;
if (mStorage.size() == 0)
{
Log.i(K9.LOG_TAG, "Preferences storage is zero-size, importing from Android-style preferences");
@ -67,6 +71,7 @@ public class Preferences
/**
* Returns an array of the accounts on the system. If no accounts are
* registered the method returns an empty array.
* @return all accounts
*/
public synchronized Account[] getAccounts()
{
@ -84,6 +89,36 @@ public class Preferences
return accounts.toArray(EMPTY_ACCOUNT_ARRAY);
}
/**
* Returns an array of the accounts on the system. If no accounts are
* registered the method returns an empty array.
* @param context
* @return all accounts with {@link Account#isAvailable(Context)}
*/
public synchronized Collection<Account> getAvailableAccounts()
{
if (accounts == null)
{
loadAccounts();
}
if ((newAccount != null) && newAccount.getAccountNumber() != -1)
{
accounts.add(newAccount);
newAccount = null;
}
Collection<Account> retval = new ArrayList<Account>(accounts.size());
for (Account account : accounts)
{
if (account.isAvailable(mContext))
{
retval.add(account);
}
}
return retval;
}
public synchronized Account getAccount(String uuid)
{
if (accounts == null)
@ -137,10 +172,10 @@ public class Preferences
if (defaultAccount == null)
{
Account[] accounts = getAccounts();
if (accounts.length > 0)
Collection<Account> accounts = getAvailableAccounts();
if (accounts.size() > 0)
{
defaultAccount = accounts[0];
defaultAccount = accounts.iterator().next();
setDefaultAccount(defaultAccount);
}
}

View File

@ -164,7 +164,14 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
try
{
AccountStats stats = account.getStats(Accounts.this);
accountStatusChanged(account, stats);
if (stats == null)
{
Log.w(K9.LOG_TAG, "Unable to get account stats");
}
else
{
accountStatusChanged(account, stats);
}
}
catch (Exception e)
{
@ -180,6 +187,10 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
{
oldUnreadMessageCount = oldStats.unreadMessageCount;
}
if (stats == null)
{
stats = new AccountStats(); // empty stats for unavailable accounts
}
accountStats.put(account.getUuid(), stats);
if (account instanceof Account)
{
@ -343,8 +354,10 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
}
else if (startup && accounts.length == 1)
{
onOpenAccount(accounts[0]);
finish();
if (onOpenAccount(accounts[0]))
{
finish();
}
}
else
{
@ -513,7 +526,13 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
}
}
private void onOpenAccount(BaseAccount account)
/**
* Show that account's inbox or folder-list
* or return false if the account is not available.
* @param account the account to open ({@link SearchAccount} or {@link Account})
* @return false if unsuccessfull
*/
private boolean onOpenAccount(BaseAccount account)
{
if (account instanceof SearchAccount)
{
@ -523,6 +542,11 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
else
{
Account realAccount = (Account)account;
if (!realAccount.isAvailable(this))
{
Log.i(K9.LOG_TAG, "refusing to open account that is not available");
return false;
}
if (K9.FOLDER_NONE.equals(realAccount.getAutoExpandFolderName()))
{
FolderList.actionHandleAccount(this, realAccount);
@ -532,6 +556,7 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
MessageList.actionHandleFolder(this, realAccount, realAccount.getAutoExpandFolderName());
}
}
return true;
}
public void onClick(View view)
@ -609,7 +634,7 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
}
catch (Exception e)
{
// Ignore
// Ignore, this may lead to localStores on sd-cards that are currently not inserted to be left
}
MessagingController.getInstance(getApplication()).notifyAccountCancel(Accounts.this, realAccount);
Preferences.getPreferences(Accounts.this).deleteAccount(realAccount);
@ -914,6 +939,24 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
}
AccountStats stats = accountStats.get(account.getUuid());
/*
// 20101024/fiouzy: the following code throws NullPointerException because Background is null
// display unavailable accounts translucent
if (account instanceof Account) {
Account realAccount = (Account) account;
if (realAccount.isAvailable(Accounts.this)) {
holder.email.getBackground().setAlpha(255);
holder.description.getBackground().setAlpha(255);
} else {
holder.email.getBackground().setAlpha(127);
holder.description.getBackground().setAlpha(127);
}
} else {
holder.email.getBackground().setAlpha(255);
holder.description.getBackground().setAlpha(255);
}
*/
if (stats != null && account instanceof Account && stats.size >= 0)
{
holder.email.setText(SizeFormatter.formatSize(Accounts.this, stats.size));

View File

@ -3,6 +3,7 @@ package com.fsck.k9.activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
@ -63,6 +64,11 @@ public class ChooseAccount extends K9ExpandableListActivity
final Identity identity = (Identity) adapter.getChild(groupPosition, childPosition);
final Account account = (Account) adapter.getGroup(groupPosition);
if (!account.isAvailable(v.getContext()))
{
Log.i(K9.LOG_TAG, "Refusing selection of unavailable account");
return true;
}
final Intent intent = new Intent();
intent.putExtra(EXTRA_ACCOUNT, account.getUuid());
intent.putExtra(EXTRA_IDENTITY, identity);
@ -180,7 +186,6 @@ public class ChooseAccount extends K9ExpandableListActivity
final View v;
if (convertView == null)
{
// is it okay to reuse?
v = mLayoutInflater.inflate(R.layout.choose_account_item, parent, false);
}
else
@ -193,6 +198,22 @@ public class ChooseAccount extends K9ExpandableListActivity
description.setText(account.getDescription());
description.setTextSize(TypedValue.COMPLEX_UNIT_DIP, K9.getFontSizes().getAccountName());
// display unavailable accounts translucent
/*
* 20101030/fiouzy: NullPointerException on null getBackground()
*
if (account.isAvailable(parent.getContext()))
{
description.getBackground().setAlpha(255);
description.getBackground().setAlpha(255);
}
else
{
description.getBackground().setAlpha(127);
description.getBackground().setAlpha(127);
}
*/
v.findViewById(R.id.chip).setBackgroundColor(account.getChipColor());
return v;

View File

@ -67,6 +67,10 @@ public class FolderInfoHolder implements Comparable<FolderInfoHolder>
public FolderInfoHolder(Context context, Folder folder, Account account)
{
if (context == null)
{
throw new IllegalArgumentException("null context given");
}
populate(context, folder, account);
}

View File

@ -382,6 +382,13 @@ public class FolderList extends K9ListActivity
{
super.onResume();
if (!mAccount.isAvailable(this))
{
Log.i(K9.LOG_TAG, "account unavaliabale, not showing folder-list but account-list");
startActivity(new Intent(this, Accounts.class));
finish();
return;
}
if (mAdapter == null)
initializeActivityView();
@ -828,6 +835,10 @@ public class FolderList extends K9ListActivity
{
return;
}
if (stats == null)
{
return;
}
mUnreadMessageCount = stats.unreadMessageCount;
mHandler.refreshTitle();
}
@ -1028,6 +1039,11 @@ public class FolderList extends K9ListActivity
{
if (account != null && folderName != null)
{
if (!account.isAvailable(FolderList.this))
{
Log.i(K9.LOG_TAG, "not refreshing folder of unavailable account");
return;
}
localFolder = account.getLocalStore().getFolder(folderName);
int unreadMessageCount = localFolder.getUnreadMessageCount();
if (localFolder != null)

View File

@ -4,6 +4,7 @@ package com.fsck.k9.activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import android.view.View.OnClickListener;
@ -143,6 +144,18 @@ public class LauncherShortcuts extends K9ListActivity implements OnItemClickList
holder.chip.setBackgroundColor(realAccount.getChipColor());
holder.chip.getBackground().setAlpha(255);
// show unavailable accounts as translucent
if (realAccount.isAvailable(getContext()))
{
holder.email.getBackground().setAlpha(255);
holder.description.getBackground().setAlpha(255);
}
else
{
holder.email.getBackground().setAlpha(127);
holder.description.getBackground().setAlpha(127);
}
}
else
{
@ -158,7 +171,13 @@ public class LauncherShortcuts extends K9ListActivity implements OnItemClickList
{
public void onClick(View v)
{
FolderList.actionHandleAccount(LauncherShortcuts.this, (Account)account);
Account account2 = (Account)account;
if (!account2.isAvailable(getContext()))
{
Log.i(K9.LOG_TAG, "refusing selection of unavailable account");
return ;
}
FolderList.actionHandleAccount(LauncherShortcuts.this, account2);
}
});

View File

@ -75,6 +75,7 @@ import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.internet.TextBody;
import com.fsck.k9.mail.store.LocalStore;
import com.fsck.k9.mail.store.LocalStore.LocalAttachmentBody;
import com.fsck.k9.mail.store.UnavailableStorageException;
public class MessageCompose extends K9Activity implements OnClickListener, OnFocusChangeListener
{
@ -1525,7 +1526,7 @@ public class MessageCompose extends K9Activity implements OnClickListener, OnFoc
{
// keep things simple: trigger account choice only if there are more
// than 1 account
if (Preferences.getPreferences(this).getAccounts().length > 1)
if (Preferences.getPreferences(this).getAvailableAccounts().size() > 1)
{
final Intent intent = new Intent(this, ChooseAccount.class);
intent.putExtra(ChooseAccount.EXTRA_ACCOUNT, mAccount.getUuid());
@ -2335,11 +2336,22 @@ public class MessageCompose extends K9Activity implements OnClickListener, OnFoc
}
}
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "Saving identity: " + k9identity);
message.addHeader(K9.K9MAIL_IDENTITY, k9identity);
final MessagingController messagingController = MessagingController.getInstance(getApplication());
Message draftMessage = MessagingController.getInstance(getApplication()).saveDraft(mAccount, message);
if (K9.DEBUG)
{
Log.d(K9.LOG_TAG, "Saving identity: " + k9identity);
}
try
{
message.addHeader(K9.K9MAIL_IDENTITY, k9identity);
}
catch (UnavailableStorageException e)
{
messagingController.addErrorMessage(mAccount, "Unable to save identity", e);
}
Message draftMessage = messagingController.saveDraft(mAccount, message);
mDraftUid = draftMessage.getUid();
// Don't display the toast if the user is just changing the orientation

View File

@ -70,6 +70,7 @@ import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Folder;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.store.LocalStore;
import com.fsck.k9.mail.store.StorageManager;
import com.fsck.k9.mail.store.LocalStore.LocalFolder;
/**
@ -327,6 +328,33 @@ public class MessageList
/* package visibility for faster inner class access */
MessageHelper mMessageHelper = MessageHelper.getInstance(this);
private StorageManager.StorageListener mStorageListener = new StorageListenerImplementation();
private final class StorageListenerImplementation implements StorageManager.StorageListener
{
@Override
public void onUnmount(String providerId)
{
if (mAccount != null && providerId.equals(mAccount.getLocalStorageProviderId()))
{
runOnUiThread(new Runnable()
{
@Override
public void run()
{
onAccountUnavailable();
}
});
}
}
@Override
public void onMount(String providerId)
{
// no-op
}
}
class MessageListHandler
{
public void removeMessage(final List<MessageInfoHolder> messages)
@ -671,6 +699,13 @@ public class MessageList
mFolderName = intent.getStringExtra(EXTRA_FOLDER);
mQueryString = intent.getStringExtra(EXTRA_QUERY);
if (mAccount != null && !mAccount.isAvailable(this))
{
Log.i(K9.LOG_TAG, "not opening MessageList of unavailable account");
onAccountUnavailable();
return;
}
String queryFlags = intent.getStringExtra(EXTRA_QUERY_FLAGS);
if (queryFlags != null)
{
@ -727,6 +762,8 @@ public class MessageList
super.onPause();
mController.removeListener(mAdapter.mListener);
saveListState();
StorageManager.getInstance(getApplication()).removeListener(mStorageListener);
}
public void saveListState()
@ -769,6 +806,13 @@ public class MessageList
{
super.onResume();
if (mAccount != null && !mAccount.isAvailable(this))
{
onAccountUnavailable();
return;
}
StorageManager.getInstance(getApplication()).addListener(mStorageListener);
mStars = K9.messageListStars();
mCheckboxes = K9.messageListCheckboxes();
@ -3363,4 +3407,12 @@ public class MessageList
}
mController.copyMessages(mAccount, mCurrentFolder.name, messageList.toArray(EMPTY_MESSAGE_ARRAY), folderName, null);
}
protected void onAccountUnavailable()
{
finish();
// TODO inform user about account unavailability using Toast
Accounts.listAccounts(getApplicationContext());
}
}

View File

@ -7,6 +7,7 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
@ -81,6 +82,7 @@ import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Multipart;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.store.StorageManager;
import com.fsck.k9.mail.store.LocalStore.LocalAttachmentBodyPart;
import com.fsck.k9.mail.store.LocalStore.LocalMessage;
import com.fsck.k9.mail.store.LocalStore.LocalTextBody;
@ -170,6 +172,33 @@ public class MessageView extends K9Activity implements OnClickListener
private Contacts mContacts;
private StorageManager.StorageListener mStorageListener = new StorageListenerImplementation();
private final class StorageListenerImplementation implements StorageManager.StorageListener
{
@Override
public void onUnmount(String providerId)
{
if (providerId.equals(mAccount.getLocalStorageProviderId()))
{
runOnUiThread(new Runnable()
{
@Override
public void run()
{
onAccountUnavailable();
}
});
}
}
@Override
public void onMount(String providerId)
{
// no-op
}
}
/**
* Pair class is only available since API Level 5, so we need
* this helper class unfortunately
@ -970,7 +999,7 @@ public class MessageView extends K9Activity implements OnClickListener
if (segmentList.size() == 3)
{
String accountId = segmentList.get(0);
Account[] accounts = Preferences.getPreferences(this).getAccounts();
Collection<Account> accounts = Preferences.getPreferences(this).getAvailableAccounts();
boolean found = false;
for (Account account : accounts)
{
@ -1310,6 +1339,26 @@ public class MessageView extends K9Activity implements OnClickListener
public void onResume()
{
super.onResume();
if (!mAccount.isAvailable(this))
{
onAccountUnavailable();
return;
}
StorageManager.getInstance(getApplication()).addListener(mStorageListener);
}
@Override
protected void onPause()
{
StorageManager.getInstance(getApplication()).removeListener(mStorageListener);
super.onPause();
}
protected void onAccountUnavailable()
{
finish();
// TODO inform user about account unavailability using Toast
Accounts.listAccounts(this);
}
/**

View File

@ -14,6 +14,8 @@ import android.preference.RingtonePreference;
import android.util.Log;
import android.view.KeyEvent;
import java.util.Map;
import com.fsck.k9.Account;
import com.fsck.k9.Account.FolderMode;
import com.fsck.k9.K9;
@ -29,6 +31,12 @@ import com.fsck.k9.crypto.Apg;
import com.fsck.k9.mail.Store;
import com.fsck.k9.service.MailService;
import com.fsck.k9.mail.store.LocalStore;
import com.fsck.k9.mail.store.StorageManager;
import com.fsck.k9.mail.store.LocalStore.LocalFolder;
import com.fsck.k9.mail.store.StorageManager.StorageProvider;
public class AccountSettings extends K9PreferenceActivity
{
private static final String EXTRA_ACCOUNT = "account";
@ -80,6 +88,9 @@ public class AccountSettings extends K9PreferenceActivity
private static final String PREFERENCE_CRYPTO_APP = "crypto_app";
private static final String PREFERENCE_CRYPTO_AUTO_SIGNATURE = "crypto_auto_signature";
private static final String PREFERENCE_LOCAL_STORAGE_PROVIDER = "local_storage_provider";
private Account mAccount;
private boolean mIsPushCapable = false;
private boolean mIsExpungeCapable = false;
@ -124,6 +135,7 @@ public class AccountSettings extends K9PreferenceActivity
private ListPreference mCryptoApp;
private CheckBoxPreference mCryptoAutoSignature;
private ListPreference mLocalStorageProvider;
public static void actionSettings(Context context, Account account)
{
@ -411,6 +423,33 @@ public class AccountSettings extends K9PreferenceActivity
});
mLocalStorageProvider = (ListPreference) findPreference(PREFERENCE_LOCAL_STORAGE_PROVIDER);
{
final Map<String, String> providers;
providers = StorageManager.getInstance(K9.app).getAvailableProviders();
int i = 0;
final String[] providerLabels = new String[providers.size()];
final String[] providerIds = new String[providers.size()];
for (final Map.Entry<String, String> entry : providers.entrySet())
{
providerIds[i] = entry.getKey();
providerLabels[i] = entry.getValue();
i++;
}
mLocalStorageProvider.setEntryValues(providerIds);
mLocalStorageProvider.setEntries(providerLabels);
mLocalStorageProvider.setValue(mAccount.getLocalStorageProviderId());
mLocalStorageProvider.setSummary(providers.get((Object)mAccount.getLocalStorageProviderId()));
mLocalStorageProvider.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener()
{
public boolean onPreferenceChange(Preference preference, Object newValue)
{
mLocalStorageProvider.setSummary(providers.get(newValue));
return true;
}
});
}
// IMAP-specific preferences
mPushPollOnConnect = (CheckBoxPreference) findPreference(PREFERENCE_PUSH_POLL_ON_CONNECT);
@ -668,6 +707,8 @@ public class AccountSettings extends K9PreferenceActivity
mAccount.setReplyAfterQuote(mReplyAfterQuote.isChecked());
mAccount.setCryptoApp(mCryptoApp.getValue());
mAccount.setCryptoAutoSignature(mCryptoAutoSignature.isChecked());
mAccount.setLocalStorageProviderId(mLocalStorageProvider.getValue());
if (mIsPushCapable)
{

View File

@ -16,11 +16,14 @@ import com.fsck.k9.*;
import com.fsck.k9.activity.ChooseFolder;
import com.fsck.k9.activity.K9Activity;
import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.store.StorageManager;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Map;
public class AccountSetupIncoming extends K9Activity implements OnClickListener
{

View File

@ -66,7 +66,9 @@ import com.fsck.k9.mail.Transport;
import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.internet.TextBody;
import com.fsck.k9.mail.store.UnavailableAccountException;
import com.fsck.k9.mail.store.LocalStore;
import com.fsck.k9.mail.store.UnavailableStorageException;
import com.fsck.k9.mail.store.LocalStore.LocalFolder;
import com.fsck.k9.mail.store.LocalStore.LocalMessage;
import com.fsck.k9.mail.store.LocalStore.PendingCommand;
@ -279,7 +281,7 @@ public class MessagingController implements Runnable
String commandDescription = null;
try
{
Command command = mCommands.take();
final Command command = mCommands.take();
if (command != null)
{
@ -289,7 +291,32 @@ public class MessagingController implements Runnable
Log.i(K9.LOG_TAG, "Running " + (command.isForeground ? "Foreground" : "Background") + " command '" + command.description + "', seq = " + command.sequence);
mBusy = true;
command.runnable.run();
try
{
command.runnable.run();
}
catch (UnavailableAccountException e)
{
// retry later
new Thread()
{
@Override
public void run()
{
try
{
sleep(30 * 1000);
mCommands.put(command);
}
catch (InterruptedException e)
{
Log.e(K9.LOG_TAG, "interrupted while putting a pending command for"
+ " an unavailable account back into the queue."
+ " THIS SHOULD NEVER HAPPEN.");
}
}
} .start();
}
if (K9.DEBUG)
Log.i(K9.LOG_TAG, (command.isForeground ? "Foreground" : "Background") +
@ -434,43 +461,50 @@ public class MessagingController implements Runnable
l.listFoldersStarted(account);
}
List<? extends Folder> localFolders = null;
try
if (!account.isAvailable(mApplication))
{
Store localStore = account.getLocalStore();
localFolders = localStore.getPersonalNamespaces(false);
Folder[] folderArray = localFolders.toArray(EMPTY_FOLDER_ARRAY);
if (refreshRemote || localFolders == null || localFolders.size() == 0)
Log.i(K9.LOG_TAG, "not listing folders of unavailable account");
}
else
{
try
{
doRefreshRemote(account, listener);
Store localStore = account.getLocalStore();
localFolders = localStore.getPersonalNamespaces(false);
Folder[] folderArray = localFolders.toArray(EMPTY_FOLDER_ARRAY);
if (refreshRemote || localFolders == null || localFolders.size() == 0)
{
doRefreshRemote(account, listener);
return;
}
for (MessagingListener l : getListeners(listener))
{
l.listFolders(account, folderArray);
}
}
catch (Exception e)
{
for (MessagingListener l : getListeners(listener))
{
l.listFoldersFailed(account, e.getMessage());
}
addErrorMessage(account, null, e);
return;
}
for (MessagingListener l : getListeners(listener))
finally
{
l.listFolders(account, folderArray);
}
}
catch (Exception e)
{
for (MessagingListener l : getListeners(listener))
{
l.listFoldersFailed(account, e.getMessage());
}
addErrorMessage(account, null, e);
return;
}
finally
{
if (localFolders != null)
{
for (Folder localFolder : localFolders)
if (localFolders != null)
{
if (localFolder != null)
for (Folder localFolder : localFolders)
{
localFolder.close();
if (localFolder != null)
{
localFolder.close();
}
}
}
}
@ -748,6 +782,11 @@ public class MessagingController implements Runnable
boolean noSpecialFolders = true;
for (final Account account : accounts)
{
if (!account.isAvailable(mApplication))
{
Log.d(K9.LOG_TAG, "searchLocalMessagesSynchronous() ignores account that is not available");
continue;
}
if (accountUuids != null && !accountUuidsSet.contains(account.getUuid()))
{
continue;
@ -939,7 +978,7 @@ public class MessagingController implements Runnable
}
}
public void resetVisibleLimits(Account[] accounts)
public void resetVisibleLimits(Collection<Account> accounts)
{
for (Account account : accounts)
{
@ -2060,6 +2099,11 @@ public class MessagingController implements Runnable
{
processPendingCommandsSynchronous(account);
}
catch (UnavailableStorageException e)
{
Log.i(K9.LOG_TAG, "Failed to process pending command because storage is not available - trying again later.");
throw new UnavailableAccountException(e);
}
catch (MessagingException me)
{
Log.e(K9.LOG_TAG, "processPendingCommands", me);
@ -3385,6 +3429,10 @@ public class MessagingController implements Runnable
{
public void run()
{
if (!account.isAvailable(mApplication))
{
throw new UnavailableAccountException();
}
if (messagesPendingSend(account))
{
NotificationManager notifMgr =
@ -3611,6 +3659,11 @@ public class MessagingController implements Runnable
notifMgr.notify(-1500 - account.getAccountNumber(), notif);
}
}
catch (UnavailableStorageException e)
{
Log.i(K9.LOG_TAG, "Failed to send pending messages because storage is not available - trying again later.");
throw new UnavailableAccountException(e);
}
catch (Exception e)
{
for (MessagingListener l : getListeners())
@ -3834,6 +3887,11 @@ public class MessagingController implements Runnable
processPendingCommands(account);
}
catch (UnavailableStorageException e)
{
Log.i(K9.LOG_TAG, "Failed to move/copy message because storage is not available - trying again later.");
throw new UnavailableAccountException(e);
}
catch (MessagingException me)
{
addErrorMessage(account, null, me);
@ -4009,6 +4067,11 @@ public class MessagingController implements Runnable
unsuppressMessage(account, folder, uid);
}
}
catch (UnavailableStorageException e)
{
Log.i(K9.LOG_TAG, "Failed to delete message because storage is not available - trying again later.");
throw new UnavailableAccountException(e);
}
catch (MessagingException me)
{
addErrorMessage(account, null, me);
@ -4090,6 +4153,11 @@ public class MessagingController implements Runnable
queuePendingCommand(account, command);
processPendingCommands(account);
}
catch (UnavailableStorageException e)
{
Log.i(K9.LOG_TAG, "Failed to empty trash because storage is not available - trying again later.");
throw new UnavailableAccountException(e);
}
catch (Exception e)
{
Log.e(K9.LOG_TAG, "emptyTrash failed", e);
@ -4212,6 +4280,14 @@ public class MessagingController implements Runnable
for (final Account account : accounts)
{
if (!account.isAvailable(context))
{
if (K9.DEBUG)
{
Log.i(K9.LOG_TAG, "Skipping synchronizing unavailable account " + account.getDescription());
}
continue;
}
final long accountInterval = account.getAutomaticCheckIntervalMinutes() * 60 * 1000;
if (!ignoreLastCheckedTime && accountInterval <= 0)
{
@ -4369,7 +4445,8 @@ public class MessagingController implements Runnable
account.setRingNotified(false);
try
{
if (account.getStats(context).unreadMessageCount == 0)
AccountStats stats = account.getStats(context);
if (stats == null || stats.unreadMessageCount == 0)
{
notifyAccountCancel(context, account);
}
@ -4436,6 +4513,11 @@ public class MessagingController implements Runnable
l.accountSizeChanged(account, oldSize, newSize);
}
}
catch (UnavailableStorageException e)
{
Log.i(K9.LOG_TAG, "Failed to compact account because storage is not available - trying again later.");
throw new UnavailableAccountException(e);
}
catch (Exception e)
{
Log.e(K9.LOG_TAG, "Failed to compact account " + account.getDescription(), e);
@ -4472,6 +4554,11 @@ public class MessagingController implements Runnable
l.accountStatusChanged(account, stats);
}
}
catch (UnavailableStorageException e)
{
Log.i(K9.LOG_TAG, "Failed to clear account because storage is not available - trying again later.");
throw new UnavailableAccountException(e);
}
catch (Exception e)
{
Log.e(K9.LOG_TAG, "Failed to clear account " + account.getDescription(), e);
@ -4508,6 +4595,11 @@ public class MessagingController implements Runnable
l.accountStatusChanged(account, stats);
}
}
catch (UnavailableStorageException e)
{
Log.i(K9.LOG_TAG, "Failed to recreate an account because storage is not available - trying again later.");
throw new UnavailableAccountException(e);
}
catch (Exception e)
{
Log.e(K9.LOG_TAG, "Failed to recreate account " + account.getDescription(), e);

View File

@ -5,6 +5,7 @@ import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import com.fsck.k9.activity.MessageReference;
import com.fsck.k9.mail.store.UnavailableStorageException;
public abstract class Message implements Part, Body
{
@ -140,7 +141,7 @@ public abstract class Message implements Part, Body
public abstract String[] getHeader(String name) throws MessagingException;
public abstract Set<String> getHeaderNames();
public abstract Set<String> getHeaderNames() throws UnavailableStorageException;
public abstract void removeHeader(String name) throws MessagingException;
@ -204,7 +205,7 @@ public abstract class Message implements Part, Body
public abstract void saveChanges() throws MessagingException;
public abstract void setEncoding(String encoding);
public abstract void setEncoding(String encoding) throws UnavailableStorageException;
public MessageReference makeMessageReference()
{

View File

@ -2,12 +2,14 @@
package com.fsck.k9.mail;
import android.app.Application;
import android.content.Context;
import com.fsck.k9.Account;
import com.fsck.k9.mail.store.ImapStore;
import com.fsck.k9.mail.store.LocalStore;
import com.fsck.k9.mail.store.Pop3Store;
import com.fsck.k9.mail.store.WebDavStore;
import com.fsck.k9.mail.store.StorageManager.StorageProvider;
import java.util.HashMap;
import java.util.List;
@ -25,7 +27,14 @@ public abstract class Store
protected static final int SOCKET_CONNECT_TIMEOUT = 30000;
protected static final int SOCKET_READ_TIMEOUT = 60000;
/**
* Remote stores indexed by Uri.
*/
private static HashMap<String, Store> mStores = new HashMap<String, Store>();
/**
* Local stores indexed by UUid because the Uri may change due to migration to/from SD-card.
*/
private static HashMap<String, Store> mLocalStores = new HashMap<String, Store>();
protected final Account mAccount;
@ -78,33 +87,18 @@ public abstract class Store
/**
* Get an instance of a local mail store.
* @throws UnavailableStorageException if not {@link StorageProvider#isReady(Context)}
*/
public synchronized static LocalStore getLocalInstance(Account account, Application application) throws MessagingException
{
String uri = account.getLocalStoreUri();
if (!uri.startsWith("local"))
{
throw new RuntimeException("LocalStore URI doesn't start with 'local'");
}
Store store = mStores.get(uri);
Store store = mLocalStores.get(account.getUuid());
if (store == null)
{
store = new LocalStore(account, application);
if (store != null)
{
mStores.put(uri, store);
}
mLocalStores.put(account.getUuid(), store);
}
if (store == null)
{
throw new MessagingException("Unable to locate an applicable Store for " + uri);
}
return (LocalStore)store;
return (LocalStore) store;
}
public abstract Folder getFolder(String name) throws MessagingException;

View File

@ -2,6 +2,8 @@
package com.fsck.k9.mail.internet;
import com.fsck.k9.mail.*;
import com.fsck.k9.mail.store.UnavailableStorageException;
import org.apache.james.mime4j.BodyDescriptor;
import org.apache.james.mime4j.ContentHandler;
import org.apache.james.mime4j.EOLConvertingInputStream;
@ -340,7 +342,7 @@ public class MimeMessage extends Message
return "<"+UUID.randomUUID().toString()+"@email.android.com>";
}
public void setMessageId(String messageId)
public void setMessageId(String messageId) throws UnavailableStorageException
{
setHeader("Message-ID", messageId);
mMessageId = messageId;
@ -438,31 +440,31 @@ public class MimeMessage extends Message
}
@Override
public void addHeader(String name, String value)
public void addHeader(String name, String value) throws UnavailableStorageException
{
mHeader.addHeader(name, value);
}
@Override
public void setHeader(String name, String value)
public void setHeader(String name, String value) throws UnavailableStorageException
{
mHeader.setHeader(name, value);
}
@Override
public String[] getHeader(String name)
public String[] getHeader(String name) throws UnavailableStorageException
{
return mHeader.getHeader(name);
}
@Override
public void removeHeader(String name)
public void removeHeader(String name) throws UnavailableStorageException
{
mHeader.removeHeader(name);
}
@Override
public Set<String> getHeaderNames()
public Set<String> getHeaderNames() throws UnavailableStorageException
{
return mHeader.getHeaderNames();
}
@ -486,7 +488,7 @@ public class MimeMessage extends Message
}
@Override
public void setEncoding(String encoding)
public void setEncoding(String encoding) throws UnavailableStorageException
{
if (mBody instanceof Multipart)
{

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,830 @@
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;
import com.fsck.k9.service.MailService;
/**
* 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.
*
* <p>
* 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).
* </p>
*/
public static interface StorageProvider
{
/**
* Retrieve the uniquely identifier for the current implementation.
*
* <p>
* 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.
* </p>
*
* <p>
* The returned identifier doesn't have to be user friendly.
* </p>
*
* @return Never <code>null</code>.
*/
String getId();
/**
* Hook point for provider initialization.
*
* @param context
* Never <code>null</code>.
*/
void init(Context context);
/**
* @param context
* Never <code>null</code>.
* @return A user displayable, localized name for this provider. Never
* <code>null</code>.
*/
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 <code>null</code>.
* @param id
* Never <code>null</code>.
* @return Never <code>null</code>.
*/
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 <code>null</code>.
* @param id
* Never <code>null</code>.
* @return Never <code>null</code>.
*/
File getAttachmentDirectory(Context context, String id);
/**
* Check for the underlying storage availability.
*
* @param context
* Never <code>null</code>.
* @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 <code>null</code>.
* @return The root directory of the denoted storage. Never
* <code>null</code>.
*/
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 <code>null</code>.
*/
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 <code>null</code>.
*/
void onUnmount(String providerId);
}
/**
* Base provider class for providers that rely on well-known path to check
* for storage availability.
*
* <p>
* 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.
* </p>
*
* <p>
* Moreover, this class validates the denoted storage path matches against
* mount points using {@link StorageManager#isMountPoint(File)}.
* </p>
*/
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);
// use <STORAGE_ROOT>/k9
mApplicationDir = new File(mRoot, "k9");
}
/**
* Vendor specific checks
*
* @return Whether this provider supports the underlying vendor specific
* storage
*/
protected abstract boolean supportsVendor();
@Override
public boolean isReady(Context context)
{
try
{
final File root = mRoot.getCanonicalFile();
return isMountPoint(root)
&& Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
}
catch (IOException e)
{
Log.w(K9.LOG_TAG, "Specified root isn't ready: " + mRoot, e);
return false;
}
}
@Override
public final boolean isSupported(Context context)
{
return mRoot.isDirectory() && supportsVendor();
}
@Override
public File getDatabase(Context context, String id)
{
return new File(mApplicationDir, id + ".db");
}
@Override
public File getAttachmentDirectory(Context context, String id)
{
return new File(mApplicationDir, id + ".db_att");
}
@Override
public final File getRoot(Context context)
{
return mRoot;
}
/**
* Retrieve the well-known storage root directory from the actual
* implementation.
*
* @param context
* Never <code>null</code>.
* @return Never <code>null</code>.
*/
protected abstract File computeRoot(Context context);
}
/**
* Strategy to access the always available internal storage.
*
* <p>
* 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.
* </p>
*
* <p>
* The underlying storage has always been used by K-9.
* </p>
*/
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
* <code>&lt;STORAGE_ROOT&gt;/Android/data/&lt;APPLICATION_PACKAGE_NAME&gt;/files/</code>
*
* <p>
* The denoted storage is usually a SD card.
* </p>
*
* <p>
* 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.
* </p>
*/
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.
*
* <p>
* This implementation is experimental and untested.
* </p>
*
* 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 /emmc directory on a Samsung Galaxy S.
*
* <p>
* This implementation is experimental and untested.
* </p>
*
* 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 Map<String, StorageProvider> mProviders = new LinkedHashMap<String, StorageProvider>();
/**
* Locking data for the active storage providers.
*/
private final Map<StorageProvider, SynchronizationAid> mProviderLocks = new IdentityHashMap<StorageProvider, SynchronizationAid>();
protected final Application mApplication;
/**
* Listener to be notified for storage related events.
*/
private List<StorageListener> mListeners = new ArrayList<StorageListener>();
private static transient StorageManager instance;
public static synchronized StorageManager getInstance(final Application application)
{
if (instance == null)
{
instance = new StorageManager(application);
}
return instance;
}
/**
* @param file
* Canonical file to match. Never <code>null</code>.
* @return Whether the specified file matches a filesystem root.
* @throws IOException
*/
public static boolean isMountPoint(final File file) throws IOException
{
for (final File root : File.listRoots())
{
if (root.equals(file))
{
return true;
}
}
return false;
}
/**
* @param application
* Never <code>null</code>.
* @throws NullPointerException
* If <tt>application</tt> is <code>null</code>.
*/
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).
*/
final List<StorageProvider> allProviders = Arrays.asList(new InternalStorageProvider(),
new ExternalStorageProvider());
for (final StorageProvider provider : allProviders)
{
// check for provider compatibility
if (provider.isSupported(mApplication))
{
// provider is compatible! proceeding
provider.init(application);
mProviders.put(provider.getId(), provider);
mProviderLocks.put(provider, new SynchronizationAid());
}
}
}
/**
* @return Never <code>null</code>.
*/
public String getDefaultProviderId()
{
// assume there is at least 1 provider defined
return mProviders.entrySet().iterator().next().getKey();
}
/**
* @param providerId
* Never <code>null</code>.
* @return <code>null</code> if not found.
*/
protected StorageProvider getProvider(final String providerId)
{
return mProviders.get(providerId);
}
/**
* @param dbName
* Never <code>null</code>.
* @param providerId
* Never <code>null</code>.
* @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 <code>null</code>.
* @param providerId
* Never <code>null</code>.
* @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 <code>null</code>.
* @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
* <code>null</code>.
* @see StorageManager
* @see StorageProvider#isSupported(Context)
*/
public Map<String, String> getAvailableProviders()
{
final Map<String, String> result = new LinkedHashMap<String, String>();
for (final Map.Entry<String, StorageProvider> entry : mProviders.entrySet())
{
result.put(entry.getKey(), entry.getValue().getName(mApplication));
}
return result;
}
/**
* @param path
*/
public void onBeforeUnmount(final String path)
{
Log.i(K9.LOG_TAG, "storage path \"" + path + "\" unmounting");
final StorageProvider provider = resolveProvider(path);
if (provider == null)
{
return;
}
for (final StorageListener listener : mListeners)
{
try
{
listener.onUnmount(provider.getId());
}
catch (Exception e)
{
Log.w(K9.LOG_TAG, "Error while notifying StorageListener", e);
}
}
final SynchronizationAid sync = mProviderLocks.get(resolveProvider(path));
sync.writeLock.lock();
sync.unmounting = true;
sync.writeLock.unlock();
}
public void onAfterUnmount(final String path)
{
Log.i(K9.LOG_TAG, "storage path \"" + path + "\" unmounted");
final StorageProvider provider = resolveProvider(path);
if (provider == null)
{
return;
}
final SynchronizationAid sync = mProviderLocks.get(resolveProvider(path));
sync.writeLock.lock();
sync.unmounting = false;
sync.writeLock.unlock();
}
/**
* @param path
* @param readOnly
*/
public void onMount(final String path, final boolean readOnly)
{
Log.i(K9.LOG_TAG, "storage path \"" + path + "\" mounted readOnly=" + readOnly);
if (readOnly)
{
return;
}
final StorageProvider provider = resolveProvider(path);
if (provider == null)
{
return;
}
for (final StorageListener listener : mListeners)
{
try
{
listener.onMount(provider.getId());
}
catch (Exception e)
{
Log.w(K9.LOG_TAG, "Error while notifying StorageListener", e);
}
}
// XXX we should reset mail service ONLY if there are accounts using the storage (this is not done in a regular listener because it has to be invoked afterward)
MailService.actionReset(mApplication, null);
}
/**
* @param path
* Never <code>null</code>.
* @return The corresponding provider. <code>null</code> 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.
*
* <p>
* You must invoke {@link #unlockProvider(String)} when you're done with the
* storage.
* </p>
*
* @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();
}
}

View File

@ -0,0 +1,47 @@
package com.fsck.k9.mail.store;
import com.fsck.k9.Account;
/**
* An {@link Account} is not
* {@link Account#isAvailable(android.content.Context)}.<br/>
* The operation may be retried later.
*/
public class UnavailableAccountException extends RuntimeException
{
/**
*
*/
private static final long serialVersionUID = -1827283277120501465L;
public UnavailableAccountException()
{
super("please try again later");
}
/**
* @param detailMessage
* @param throwable
*/
public UnavailableAccountException(String detailMessage, Throwable throwable)
{
super(detailMessage, throwable);
}
/**
* @param detailMessage
*/
public UnavailableAccountException(String detailMessage)
{
super(detailMessage);
}
/**
* @param throwable
*/
public UnavailableAccountException(Throwable throwable)
{
super(throwable);
}
}

View File

@ -0,0 +1,32 @@
package com.fsck.k9.mail.store;
import com.fsck.k9.mail.MessagingException;
public class UnavailableStorageException extends MessagingException
{
private static final long serialVersionUID = 1348267375054620792L;
public UnavailableStorageException(String message)
{
// consider this exception as a permanent failure by default
this(message, true);
}
public UnavailableStorageException(String message, boolean perm)
{
super(message, perm);
}
public UnavailableStorageException(String message, Throwable throwable)
{
// consider this exception as permanent failure by default
this(message, true, throwable);
}
public UnavailableStorageException(String message, boolean perm, Throwable throwable)
{
super(message, perm, throwable);
}
}

View File

@ -10,17 +10,22 @@ import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.os.Environment;
import android.util.Log;
import com.fsck.k9.Account;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.store.LocalStore;
import com.fsck.k9.mail.store.LocalStore.AttachmentInfo;
import com.fsck.k9.mail.store.StorageManager;
import java.io.*;
import java.util.List;
/*
* A simple ContentProvider that allows file access to Email's attachments.
/**
* A simple ContentProvider that allows file access to Email's attachments.<br/>
* Warning! We make heavy assumptions about the Uris used by the {@link LocalStore} for an {@link Account} here.
*/
public class AttachmentProvider extends ContentProvider
{
@ -39,13 +44,13 @@ public class AttachmentProvider extends ContentProvider
public static Uri getAttachmentUri(Account account, long id)
{
return getAttachmentUri(account.getUuid() + ".db" , id);
return getAttachmentUri(account.getUuid(), id);
}
public static Uri getAttachmentThumbnailUri(Account account, long id, int width, int height)
{
return CONTENT_URI.buildUpon()
.appendPath(account.getUuid() + ".db")
.appendPath(account.getUuid())
.appendPath(Long.toString(id))
.appendPath(FORMAT_THUMBNAIL)
.appendPath(Integer.toString(width))
@ -53,7 +58,7 @@ public class AttachmentProvider extends ContentProvider
.build();
}
public static Uri getAttachmentUri(String db, long id)
private static Uri getAttachmentUri(String db, long id)
{
return CONTENT_URI.buildUpon()
.appendPath(db)
@ -113,43 +118,17 @@ public class AttachmentProvider extends ContentProvider
}
else
{
String path = getContext().getDatabasePath(dbName).getAbsolutePath();
SQLiteDatabase db = null;
Cursor cursor = null;
final Account account = Preferences.getPreferences(getContext()).getAccount(dbName);
try
{
db = SQLiteDatabase.openDatabase(path, null, 0);
cursor = db.query(
"attachments",
new String[] { "mime_type", "name" },
"id = ?",
new String[] { id },
null,
null,
null);
cursor.moveToFirst();
String type = cursor.getString(0);
String name = cursor.getString(1);
cursor.close();
db.close();
if (MimeUtility.DEFAULT_ATTACHMENT_MIME_TYPE.equals(type))
{
type = MimeUtility.getMimeTypeByExtension(name);
}
return type;
final LocalStore localStore = LocalStore.getLocalInstance(account, K9.app);
return localStore.getAttachmentType(id);
}
finally
catch (MessagingException e)
{
if (cursor != null)
{
cursor.close();
}
if (db != null)
{
db.close();
}
Log.e(K9.LOG_TAG, "Unable to retrieve LocalStore for " + account, e);
return null;
}
}
}
@ -159,15 +138,14 @@ public class AttachmentProvider extends ContentProvider
{
try
{
File attachmentsDir = getContext().getDatabasePath(dbName + "_att");
File file = new File(attachmentsDir, id);
final Account account = Preferences.getPreferences(getContext()).getAccount(dbName);
final File attachmentsDir;
attachmentsDir = StorageManager.getInstance(K9.app).getAttachmentDirectory(dbName,
account.getLocalStorageProviderId());
final File file = new File(attachmentsDir, id);
if (!file.exists())
{
file = new File(Environment.getExternalStorageDirectory() + attachmentsDir.getCanonicalPath().substring("/data".length()), id);
if (!file.exists())
{
throw new FileNotFoundException();
}
throw new FileNotFoundException(file.getAbsolutePath());
}
return file;
}
@ -182,7 +160,7 @@ public class AttachmentProvider extends ContentProvider
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException
{
List<String> segments = uri.getPathSegments();
String dbName = segments.get(0);
String dbName = segments.get(0); // "/sdcard/..." is URL-encoded and makes up only 1 segment
String id = segments.get(1);
String format = segments.get(2);
if (FORMAT_THUMBNAIL.equals(format))
@ -190,6 +168,11 @@ public class AttachmentProvider extends ContentProvider
int width = Integer.parseInt(segments.get(3));
int height = Integer.parseInt(segments.get(4));
String filename = "thmb_" + dbName + "_" + id + ".tmp";
int index = dbName.lastIndexOf('/');
if (index >= 0)
{
filename = /*dbName.substring(0, index + 1) + */"thmb_" + dbName.substring(index + 1) + "_" + id + ".tmp";
}
File dir = getContext().getCacheDir();
File file = new File(dir, filename);
if (!file.exists())
@ -199,12 +182,21 @@ public class AttachmentProvider extends ContentProvider
try
{
FileInputStream in = new FileInputStream(getFile(dbName, id));
Bitmap thumbnail = createThumbnail(type, in);
thumbnail = Bitmap.createScaledBitmap(thumbnail, width, height, true);
FileOutputStream out = new FileOutputStream(file);
thumbnail.compress(Bitmap.CompressFormat.PNG, 100, out);
out.close();
in.close();
try
{
Bitmap thumbnail = createThumbnail(type, in);
if (thumbnail != null)
{
thumbnail = Bitmap.createScaledBitmap(thumbnail, width, height, true);
FileOutputStream out = new FileOutputStream(file);
thumbnail.compress(Bitmap.CompressFormat.PNG, 100, out);
out.close();
}
}
finally
{
in.close();
}
}
catch (IOException ioe)
{
@ -251,39 +243,16 @@ public class AttachmentProvider extends ContentProvider
String dbName = segments.get(0);
String id = segments.get(1);
//String format = segments.get(2);
String path = getContext().getDatabasePath(dbName).getAbsolutePath();
String name = null;
int size = -1;
SQLiteDatabase db = null;
Cursor cursor = null;
final AttachmentInfo attachmentInfo;
try
{
db = SQLiteDatabase.openDatabase(path, null, 0);
cursor = db.query(
"attachments",
new String[] { "name", "size" },
"id = ?",
new String[] { id },
null,
null,
null);
if (!cursor.moveToFirst())
{
return null;
}
name = cursor.getString(0);
size = cursor.getInt(1);
final Account account = Preferences.getPreferences(getContext()).getAccount(dbName);
attachmentInfo = LocalStore.getLocalInstance(account, K9.app).getAttachmentInfo(id);
}
finally
catch (MessagingException e)
{
if (cursor != null)
{
cursor.close();
}
if (db != null)
{
db.close();
}
Log.e(K9.LOG_TAG, "Uname to retrieve attachment info from local store for ID: " + id, e);
return null;
}
MatrixCursor ret = new MatrixCursor(projection);
@ -301,11 +270,11 @@ public class AttachmentProvider extends ContentProvider
}
else if (AttachmentProviderColumns.DISPLAY_NAME.equals(column))
{
values[i] = name;
values[i] = attachmentInfo.name;
}
else if (AttachmentProviderColumns.SIZE.equals(column))
{
values[i] = size;
values[i] = attachmentInfo.size;
}
}
ret.addRow(values);

View File

@ -426,7 +426,7 @@ public class MessageProvider extends ContentProvider
Object[] values = new Object[2];
for (Account account : Preferences.getPreferences(getContext()).getAccounts())
for (Account account : Preferences.getPreferences(getContext()).getAvailableAccounts())
{
if (account.getAccountNumber()==accountNumber)
{
@ -435,7 +435,15 @@ public class MessageProvider extends ContentProvider
{
myAccountStats = account.getStats(getContext());
values[0] = myAccount.getDescription();
values[1] = myAccountStats.unreadMessageCount;
if (myAccountStats == null)
{
values[1] = 0;
}
else
{
values[1] = myAccountStats.unreadMessageCount;
}
ret.addRow(values);
}
catch (MessagingException e)
@ -1024,6 +1032,11 @@ public class MessageProvider extends ContentProvider
if (account.getAccountNumber() == accountId)
{
myAccount = account;
if (!account.isAvailable(getContext()))
{
Log.w(K9.LOG_TAG, "not deleting messages because account is unavailable at the moment");
return 0;
}
}
}

View File

@ -30,27 +30,28 @@ public class BootReceiver extends CoreReceiver
if (K9.DEBUG)
Log.i(K9.LOG_TAG, "BootReceiver.onReceive" + intent);
if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction()))
final String action = intent.getAction();
if (Intent.ACTION_BOOT_COMPLETED.equals(action))
{
//K9.setServicesEnabled(context, tmpWakeLockId);
//tmpWakeLockId = null;
}
else if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(intent.getAction()))
else if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(action))
{
MailService.actionCancel(context, tmpWakeLockId);
tmpWakeLockId = null;
}
else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(intent.getAction()))
else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action))
{
MailService.actionReset(context, tmpWakeLockId);
tmpWakeLockId = null;
}
else if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction()))
else if (ConnectivityManager.CONNECTIVITY_ACTION.equals(action))
{
MailService.connectivityChange(context, tmpWakeLockId);
tmpWakeLockId = null;
}
else if (AutoSyncHelper.SYNC_CONN_STATUS_CHANGE.equals(intent.getAction()))
else if (AutoSyncHelper.SYNC_CONN_STATUS_CHANGE.equals(action))
{
K9.BACKGROUND_OPS bOps = K9.getBackgroundOps();
if (bOps == K9.BACKGROUND_OPS.WHEN_CHECKED_AUTO_SYNC)
@ -59,7 +60,7 @@ public class BootReceiver extends CoreReceiver
tmpWakeLockId = null;
}
}
else if (ConnectivityManager.ACTION_BACKGROUND_DATA_SETTING_CHANGED.equals(intent.getAction()))
else if (ConnectivityManager.ACTION_BACKGROUND_DATA_SETTING_CHANGED.equals(action))
{
K9.BACKGROUND_OPS bOps = K9.getBackgroundOps();
if (bOps == K9.BACKGROUND_OPS.WHEN_CHECKED || bOps == K9.BACKGROUND_OPS.WHEN_CHECKED_AUTO_SYNC)
@ -68,7 +69,7 @@ public class BootReceiver extends CoreReceiver
tmpWakeLockId = null;
}
}
else if (FIRE_INTENT.equals(intent.getAction()))
else if (FIRE_INTENT.equals(action))
{
Intent alarmedIntent = intent.getParcelableExtra(ALARMED_INTENT);
String alarmedAction = alarmedIntent.getAction();
@ -81,7 +82,7 @@ public class BootReceiver extends CoreReceiver
context.startService(alarmedIntent);
}
}
else if (SCHEDULE_INTENT.equals(intent.getAction()))
else if (SCHEDULE_INTENT.equals(action))
{
long atTime = intent.getLongExtra(AT_TIME, -1);
Intent alarmedIntent = intent.getParcelableExtra(ALARMED_INTENT);
@ -93,7 +94,7 @@ public class BootReceiver extends CoreReceiver
alarmMgr.set(AlarmManager.RTC_WAKEUP, atTime, pi);
}
else if (CANCEL_INTENT.equals(intent.getAction()))
else if (CANCEL_INTENT.equals(action))
{
Intent alarmedIntent = intent.getParcelableExtra(ALARMED_INTENT);
if (K9.DEBUG)
@ -146,4 +147,24 @@ public class BootReceiver extends CoreReceiver
context.sendBroadcast(i);
}
/**
* Cancel any scheduled alarm.
*
* @param context
*/
public static void purgeSchedule(final Context context)
{
final AlarmManager alarmService = (AlarmManager) context
.getSystemService(Context.ALARM_SERVICE);
alarmService.cancel(PendingIntent.getBroadcast(context, 0, new Intent()
{
@Override
public boolean filterEquals(final Intent other)
{
// we want to match all intents
return true;
}
}, 0));
}
}

View File

@ -432,7 +432,14 @@ public class MailService extends CoreService
{
if (K9.DEBUG)
Log.i(K9.LOG_TAG, "Setting up pushers for account " + account.getDescription());
pushing |= MessagingController.getInstance(getApplication()).setupPushing(account);
if (account.isAvailable(getApplicationContext()))
{
pushing |= MessagingController.getInstance(getApplication()).setupPushing(account);
}
else
{
//TODO: setupPushing of unavailable accounts when they become available (sd-card inserted)
}
}
if (pushing)
{

View File

@ -36,6 +36,7 @@ public class RemoteControlReceiver extends CoreReceiver
String[] descriptions = new String[accounts.length];
for (int i = 0; i < accounts.length; i++)
{
//warning: account may not be isAvailable()
Account account = accounts[i];
uuids[i] = account.getUuid();

View File

@ -86,6 +86,7 @@ public class RemoteControlService extends CoreService
Account[] accounts = preferences.getAccounts();
for (Account account : accounts)
{
//warning: account may not be isAvailable()
if (allAccounts || account.getUuid().equals(uuid))
{

View File

@ -0,0 +1,44 @@
package com.fsck.k9.service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import com.fsck.k9.K9;
/**
* Capture the system shutdown event in order to properly free resources.
*
* <p>
* It is advised not to statically register (from AndroidManifest.xml) this
* receiver in order to avoid unecessary K-9 launch (which would defeat the
* purpose of that receiver). Using AndroidManifest.xml instructs Android to
* launch K-9 if not running, defeating the purpose of this receiver. <br>
* The recommended way is to register this receiver using
* {@link Context#registerReceiver(BroadcastReceiver, android.content.IntentFilter)}
* </p>
*/
public class ShutdownReceiver extends BroadcastReceiver
{
@Override
public void onReceive(final Context context, final Intent intent)
{
if (Intent.ACTION_SHUTDOWN.equals(intent.getAction()))
{
Log.i(K9.LOG_TAG, "System is shutting down, releasing resources");
// prevent any scheduled intent from waking up K-9
BootReceiver.purgeSchedule(context);
/*
* TODO invoke proper shutdown methods (stop any running thread)
*
* 20101111: this can't be done now as we don't have proper
* startup/shutdown sequences
*/
}
}
}

View File

@ -0,0 +1,51 @@
package com.fsck.k9.service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import com.fsck.k9.K9;
import com.fsck.k9.mail.store.StorageManager;
/**
* That BroadcastReceiver is only interested in UNMOUNT events.
*
* <p>
* Code was separated from {@link StorageReceiver} because we don't want that
* receiver to be statically defined in manifest.
* </p>
*/
public class StorageGoneReceiver extends BroadcastReceiver
{
@Override
public void onReceive(final Context context, final Intent intent)
{
final String action = intent.getAction();
final Uri uri = intent.getData();
if (uri == null || uri.getPath() == null)
{
return;
}
if (K9.DEBUG)
{
Log.v(K9.LOG_TAG, "StorageGoneReceiver: " + intent.toString());
}
final String path = uri.getPath();
if (Intent.ACTION_MEDIA_EJECT.equals(action))
{
StorageManager.getInstance(K9.app).onBeforeUnmount(path);
}
else if (Intent.ACTION_MEDIA_UNMOUNTED.equals(action))
{
StorageManager.getInstance(K9.app).onAfterUnmount(path);
}
}
}

View File

@ -0,0 +1,43 @@
package com.fsck.k9.service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import com.fsck.k9.K9;
import com.fsck.k9.mail.store.StorageManager;
/**
* That BroadcastReceiver is only interested in MOUNT events.
*/
public class StorageReceiver extends BroadcastReceiver
{
@Override
public void onReceive(final Context context, final Intent intent)
{
final String action = intent.getAction();
final Uri uri = intent.getData();
if (uri == null || uri.getPath() == null)
{
return;
}
if (K9.DEBUG)
{
Log.v(K9.LOG_TAG, "StorageReceiver: " + intent.toString());
}
final String path = uri.getPath();
if (Intent.ACTION_MEDIA_MOUNTED.equals(action))
{
StorageManager.getInstance(K9.app).onMount(path,
intent.getBooleanExtra("read-only", true));
}
}
}