diff --git a/src/com/android/email/LegacyConversions.java b/src/com/android/email/LegacyConversions.java index c6acece7e..9def43e93 100644 --- a/src/com/android/email/LegacyConversions.java +++ b/src/com/android/email/LegacyConversions.java @@ -374,6 +374,7 @@ public class LegacyConversions { localAttachment.mMessageKey = localMessage.mId; localAttachment.mLocation = partId; localAttachment.mEncoding = "B"; // TODO - convert other known encodings + localAttachment.mAccountKey = localMessage.mAccountKey; if (DEBUG_ATTACHMENTS) { Log.d(Email.LOG_TAG, "Add attachment " + localAttachment); diff --git a/src/com/android/email/provider/EmailContent.java b/src/com/android/email/provider/EmailContent.java index 67f8ac9bf..6c0f5f24b 100644 --- a/src/com/android/email/provider/EmailContent.java +++ b/src/com/android/email/provider/EmailContent.java @@ -1871,6 +1871,8 @@ public abstract class EmailContent { public static final String FLAGS = "flags"; // Content that is actually contained in the Attachment row public static final String CONTENT_BYTES = "content_bytes"; + // A foreign key into the Account table (for the message owning this attachment) + public static final String ACCOUNT_KEY = "accountKey"; } public static final class Attachment extends EmailContent implements AttachmentColumns { @@ -1892,6 +1894,7 @@ public abstract class EmailContent { public String mContent; // Not currently used public int mFlags; public byte[] mContentBytes; + public long mAccountKey; public static final int CONTENT_ID_COLUMN = 0; public static final int CONTENT_FILENAME_COLUMN = 1; @@ -1905,11 +1908,13 @@ public abstract class EmailContent { public static final int CONTENT_CONTENT_COLUMN = 9; // Not currently used public static final int CONTENT_FLAGS_COLUMN = 10; public static final int CONTENT_CONTENT_BYTES_COLUMN = 11; + public static final int CONTENT_ACCOUNT_KEY_COLUMN = 12; public static final String[] CONTENT_PROJECTION = new String[] { RECORD_ID, AttachmentColumns.FILENAME, AttachmentColumns.MIME_TYPE, AttachmentColumns.SIZE, AttachmentColumns.CONTENT_ID, AttachmentColumns.CONTENT_URI, AttachmentColumns.MESSAGE_KEY, AttachmentColumns.LOCATION, AttachmentColumns.ENCODING, - AttachmentColumns.CONTENT, AttachmentColumns.FLAGS, AttachmentColumns.CONTENT_BYTES + AttachmentColumns.CONTENT, AttachmentColumns.FLAGS, AttachmentColumns.CONTENT_BYTES, + AttachmentColumns.ACCOUNT_KEY }; // Bits used in mFlags @@ -2025,6 +2030,7 @@ public abstract class EmailContent { mContent = cursor.getString(CONTENT_CONTENT_COLUMN); mFlags = cursor.getInt(CONTENT_FLAGS_COLUMN); mContentBytes = cursor.getBlob(CONTENT_CONTENT_BYTES_COLUMN); + mAccountKey = cursor.getLong(CONTENT_ACCOUNT_KEY_COLUMN); return this; } @@ -2042,6 +2048,7 @@ public abstract class EmailContent { values.put(AttachmentColumns.CONTENT, mContent); values.put(AttachmentColumns.FLAGS, mFlags); values.put(AttachmentColumns.CONTENT_BYTES, mContentBytes); + values.put(AttachmentColumns.ACCOUNT_KEY, mAccountKey); return values; } @@ -2062,6 +2069,7 @@ public abstract class EmailContent { dest.writeString(mEncoding); dest.writeString(mContent); dest.writeInt(mFlags); + dest.writeLong(mAccountKey); if (mContentBytes == null) { dest.writeInt(-1); } else { @@ -2083,6 +2091,7 @@ public abstract class EmailContent { mEncoding = in.readString(); mContent = in.readString(); mFlags = in.readInt(); + mAccountKey = in.readLong(); final int contentBytesLen = in.readInt(); if (contentBytesLen == -1) { mContentBytes = null; @@ -2107,7 +2116,7 @@ public abstract class EmailContent { public String toString() { return "[" + mFileName + ", " + mMimeType + ", " + mSize + ", " + mContentId + ", " + mContentUri + ", " + mMessageKey + ", " + mLocation + ", " + mEncoding + ", " - + mFlags + ", " + mContentBytes + "]"; + + mFlags + ", " + mContentBytes + ", " + mAccountKey + "]"; } } diff --git a/src/com/android/email/provider/EmailProvider.java b/src/com/android/email/provider/EmailProvider.java index 54bf96eb1..6fe81898d 100644 --- a/src/com/android/email/provider/EmailProvider.java +++ b/src/com/android/email/provider/EmailProvider.java @@ -106,7 +106,8 @@ public class EmailProvider extends ContentProvider { // Version 13: Add messageCount to Mailbox table. // Version 14: Add snippet to Message table // Version 15: Fix upgrade problem in version 14. - public static final int DATABASE_VERSION = 15; + // Version 16: Add accountKey to Attachment table + public static final int DATABASE_VERSION = 16; // Any changes to the database format *must* include update-in-place code. // Original version: 2 @@ -574,7 +575,8 @@ public class EmailProvider extends ContentProvider { + AttachmentColumns.ENCODING + " text, " + AttachmentColumns.CONTENT + " text, " + AttachmentColumns.FLAGS + " integer, " - + AttachmentColumns.CONTENT_BYTES + " blob" + + AttachmentColumns.CONTENT_BYTES + " blob, " + + AttachmentColumns.ACCOUNT_KEY + " integer" + ");"; db.execSQL("create table " + Attachment.TABLE_NAME + s); db.execSQL(createIndex(Attachment.TABLE_NAME, AttachmentColumns.MESSAGE_KEY)); @@ -896,6 +898,22 @@ public class EmailProvider extends ContentProvider { } oldVersion = 15; } + if (oldVersion == 15) { + try { + db.execSQL("alter table " + Attachment.TABLE_NAME + + " add column " + Attachment.ACCOUNT_KEY +" integer" + ";"); + // Update all existing attachments to add the accountKey data + db.execSQL("update " + Attachment.TABLE_NAME + " set " + + Attachment.ACCOUNT_KEY + "= (SELECT " + Message.TABLE_NAME + "." + + Message.ACCOUNT_KEY + " from " + Message.TABLE_NAME + " where " + + Message.TABLE_NAME + "." + Message.RECORD_ID + " = " + + Attachment.TABLE_NAME + "." + Attachment.MESSAGE_KEY + ")"); + } catch (SQLException e) { + // Shouldn't be needed unless we're debugging and interrupt the process + Log.w(TAG, "Exception upgrading EmailProvider.db from 15 to 16 " + e); + } + oldVersion = 16; + } } @Override @@ -1150,10 +1168,12 @@ public class EmailProvider extends ContentProvider { throw new IllegalArgumentException("Unknown URL " + uri); } if (match == ATTACHMENT) { + int flags = 0; if (values.containsKey(Attachment.FLAGS)) { - int flags = values.getAsInteger(Attachment.FLAGS); - AttachmentDownloadService.attachmentChanged(id, flags); + flags = values.getAsInteger(Attachment.FLAGS); } + // Report all new attachments to the download service + AttachmentDownloadService.attachmentChanged(id, flags); } break; case MAILBOX_ID: diff --git a/src/com/android/email/service/AttachmentDownloadService.java b/src/com/android/email/service/AttachmentDownloadService.java index 291f0b25b..61892911a 100644 --- a/src/com/android/email/service/AttachmentDownloadService.java +++ b/src/com/android/email/service/AttachmentDownloadService.java @@ -25,9 +25,11 @@ import com.android.email.provider.AttachmentProvider; import com.android.email.provider.EmailContent; import com.android.email.provider.EmailContent.Account; import com.android.email.provider.EmailContent.Attachment; +import com.android.email.provider.EmailContent.AttachmentColumns; import com.android.email.provider.EmailContent.Message; import com.android.exchange.ExchangeService; +import android.accounts.AccountManager; import android.app.AlarmManager; import android.app.PendingIntent; import android.app.Service; @@ -36,6 +38,7 @@ import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.Cursor; +import android.net.Uri; import android.os.IBinder; import android.os.RemoteException; import android.text.format.DateUtils; @@ -69,12 +72,20 @@ public class AttachmentDownloadService extends Service implements Runnable { // High priority is for user requests private static final int PRIORITY_HIGH = 2; + // Minimum free storage in order to perform prefetch (25% of total memory) + private static final float PREFETCH_MINIMUM_STORAGE_AVAILABLE = 0.25F; + // Maximum prefetch storage (also 25% of total memory) + private static final float PREFETCH_MAXIMUM_ATTACHMENT_STORAGE = 0.25F; + // We can try various values here; I think 2 is completely reasonable as a first pass private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2; // Limit on the number of simultaneous downloads per account // Note that a limit of 1 is currently enforced by both Services (MailService and Controller) private static final int MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT = 1; + private static final Uri SINGLE_ATTACHMENT_URI = + EmailContent.uriWithLimit(Attachment.CONTENT_URI, 1); + /*package*/ static AttachmentDownloadService sRunningService = null; /*package*/ Context mContext; @@ -82,10 +93,46 @@ public class AttachmentDownloadService extends Service implements Runnable { private final HashMap> mAccountServiceMap = new HashMap>(); + // A map of attachment storage used per account + // NOTE: This map is not kept current in terms of deletions (i.e. it stores the last calculated + // amount plus the size of any new attachments laoded). If and when we reach the per-account + // limit, we recalculate the actual usage + /*package*/ final HashMap mAttachmentStorageMap = new HashMap(); private final ServiceCallback mServiceCallback = new ServiceCallback(); + private final Object mLock = new Object(); private volatile boolean mStop = false; + /*package*/ AccountManagerStub mAccountManagerStub; + + /** + * We only use the getAccounts() call from AccountManager, so this class wraps that call and + * allows us to build a mock account manager stub in the unit tests + */ + /*package*/ static class AccountManagerStub { + private int mNumberOfAccounts; + private final AccountManager mAccountManager; + + AccountManagerStub(Context context) { + if (context != null) { + mAccountManager = AccountManager.get(context); + } else { + mAccountManager = null; + } + } + + /*package*/ int getNumberOfAccounts() { + if (mAccountManager != null) { + return mAccountManager.getAccounts().length; + } else { + return mNumberOfAccounts; + } + } + + /*package*/ void setNumberOfAccounts(int numberOfAccounts) { + mNumberOfAccounts = numberOfAccounts; + } + } /** * Watchdog alarm receiver; responsible for making sure that downloads in progress are not @@ -160,7 +207,6 @@ public class AttachmentDownloadService extends Service implements Runnable { res = (req1.time > req2.time) ? -1 : 1; } } - //Log.d(TAG, "Compare " + req1.attachmentId + " to " + req2.attachmentId + " = " + res); return res; } } @@ -257,17 +303,44 @@ public class AttachmentDownloadService extends Service implements Runnable { while (iterator.hasNext() && (mDownloadsInProgress.size() < MAX_SIMULTANEOUS_DOWNLOADS)) { DownloadRequest req = iterator.next(); + // Enforce per-account limit here + if (downloadsForAccount(req.accountId) >= MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT) { + if (Email.DEBUG) { + Log.d(TAG, "== Skip #" + req.attachmentId + "; maxed for acct #" + + req.accountId); + } + continue; + } + if (!req.inProgress) { mDownloadSet.tryStartDownload(req); } } // Then, try opportunistic download of appropriate attachments int backgroundDownloads = MAX_SIMULTANEOUS_DOWNLOADS - mDownloadsInProgress.size(); - if (backgroundDownloads > 0) { - // TODO Code for background downloads here - if (Email.DEBUG) { - Log.d(TAG, "== We'd look for up to " + backgroundDownloads + - " background download(s) now..."); + // Always leave one slot for user requested download + if (backgroundDownloads > (MAX_SIMULTANEOUS_DOWNLOADS - 1)) { + // We'll take the most recent unloaded attachment + // TODO It would be more correct to look at other attachments if we are prevented + // from preloading due to per-account storage constraints, but this would be a + // very unusual case. + Long prefetchId = Utility.getFirstRowLong(mContext, + SINGLE_ATTACHMENT_URI, + Attachment.ID_PROJECTION, + AttachmentColumns.CONTENT_URI + " isnull AND " + Attachment.FLAGS + "=0", + null, + Attachment.RECORD_ID + " DESC", + Attachment.ID_PROJECTION_COLUMN); + if (prefetchId != null) { + if (Email.DEBUG) { + Log.d(TAG, ">> Prefetch attachment " + prefetchId); + } + Attachment att = Attachment.restoreAttachmentWithId(mContext, prefetchId); + if (att != null && canPrefetchForAccount(att.mAccountKey, + AttachmentProvider.getAttachmentDirectory(mContext, att.mAccountKey))) { + DownloadRequest req = new DownloadRequest(mContext, att); + mDownloadSet.tryStartDownload(req); + } } } } @@ -294,8 +367,7 @@ public class AttachmentDownloadService extends Service implements Runnable { long timeSinceCallback = now - req.lastCallbackTime; if (timeSinceCallback > CALLBACK_TIMEOUT) { if (Email.DEBUG) { - Log.d(TAG, "== , Download of " + req.attachmentId + - " timed out"); + Log.d(TAG, "== Download of " + req.attachmentId + " timed out"); } cancelDownload(req); // STOPSHIP Remove this before ship @@ -348,6 +420,10 @@ public class AttachmentDownloadService extends Service implements Runnable { mWatchdogPendingIntent); } + private synchronized DownloadRequest getDownloadInProgress(long attachmentId) { + return mDownloadsInProgress.get(attachmentId); + } + /** * Attempt to execute the DownloadRequest, enforcing the maximum downloads per account * parameter @@ -355,14 +431,6 @@ public class AttachmentDownloadService extends Service implements Runnable { * @return whether or not the download was started */ /*package*/ synchronized boolean tryStartDownload(DownloadRequest req) { - // Enforce per-account limit - if (downloadsForAccount(req.accountId) >= MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT) { - if (Email.DEBUG) { - Log.d(TAG, "== Skip #" + req.attachmentId + "; maxed for acct #" + - req.accountId); - } - return false; - } Class serviceClass = getServiceClassForAccount(req.accountId); if (serviceClass == null) return false; try { @@ -420,6 +488,13 @@ public class AttachmentDownloadService extends Service implements Runnable { Attachment attachment = Attachment.restoreAttachmentWithId(mContext, attachmentId); if (attachment != null) { + long accountId = attachment.mAccountKey; + // Update our attachment storage for this account + Long currentStorage = mAttachmentStorageMap.get(accountId); + if (currentStorage == null) { + currentStorage = 0L; + } + mAttachmentStorageMap.put(accountId, currentStorage + attachment.mSize); boolean deleted = false; if ((attachment.mFlags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) { if (statusCode == EmailServiceStatus.ATTACHMENT_NOT_FOUND) { @@ -491,24 +566,22 @@ public class AttachmentDownloadService extends Service implements Runnable { private class ServiceCallback extends IEmailServiceCallback.Stub { public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode, int progress) { - if (Email.DEBUG) { - String code; - switch(statusCode) { - case EmailServiceStatus.SUCCESS: - code = "Success"; - break; - case EmailServiceStatus.IN_PROGRESS: - code = "In progress"; - break; - default: - code = Integer.toString(statusCode); - } - Log.d(TAG, "loadAttachmentStatus, id = " + attachmentId + " code = "+ code + - ", " + progress + "%"); - } // Record status and progress - DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId); + DownloadRequest req = mDownloadSet.getDownloadInProgress(attachmentId); if (req != null) { + if (Email.DEBUG) { + String code; + switch(statusCode) { + case EmailServiceStatus.SUCCESS: code = "Success"; break; + case EmailServiceStatus.IN_PROGRESS: code = "In progress"; break; + default: code = Integer.toString(statusCode); break; + } + if (statusCode != EmailServiceStatus.IN_PROGRESS) { + Log.d(TAG, ">> Attachment " + attachmentId + ": " + code); + } else if (progress >= (req.lastProgress + 15)) { + Log.d(TAG, ">> Attachment " + attachmentId + ": " + progress + "%"); + } + } req.lastStatusCode = statusCode; req.lastProgress = progress; req.lastCallbackTime = System.currentTimeMillis(); @@ -650,8 +723,56 @@ public class AttachmentDownloadService extends Service implements Runnable { }}); } + /** + * Determine whether an attachment can be prefetched for the given account + * @return true if download is allowed, false otherwise + */ + /*package*/ boolean canPrefetchForAccount(long accountId, File dir) { + long totalStorage = dir.getTotalSpace(); + long usableStorage = dir.getUsableSpace(); + long minAvailable = (long)(totalStorage * PREFETCH_MINIMUM_STORAGE_AVAILABLE); + + // If there's not enough overall storage available, stop now + if (usableStorage < minAvailable) { + return false; + } + + int numberOfAccounts = mAccountManagerStub.getNumberOfAccounts(); + long perAccountMaxStorage = + (long)(totalStorage * PREFETCH_MAXIMUM_ATTACHMENT_STORAGE / numberOfAccounts); + + // Retrieve our idea of currently used attachment storage; since we don't track deletions, + // this number is the "worst case". If the number is greater than what's allowed per + // account, we walk the directory to determine the actual number + Long accountStorage = mAttachmentStorageMap.get(accountId); + if (accountStorage == null || (accountStorage > perAccountMaxStorage)) { + // Calculate the exact figure for attachment storage for this account + accountStorage = 0L; + File[] files = dir.listFiles(); + if (files != null) { + for (File file : files) { + accountStorage += file.length(); + } + } + // Cache the value + mAttachmentStorageMap.put(accountId, accountStorage); + } + + // Return true if we're using less than the maximum per account + if (accountStorage < perAccountMaxStorage) { + return true; + } else { + if (Email.DEBUG) { + Log.d(TAG, ">> Prefetch not allowed for account " + accountId + "; used " + + accountStorage + ", limit " + perAccountMaxStorage); + } + return false; + } + } + public void run() { mContext = this; + mAccountManagerStub = new AccountManagerStub(this); // Run through all attachments in the database that require download and add them to // the queue int mask = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST; diff --git a/src/com/android/exchange/AbstractSyncService.java b/src/com/android/exchange/AbstractSyncService.java index c676cce82..4fa03b4a7 100644 --- a/src/com/android/exchange/AbstractSyncService.java +++ b/src/com/android/exchange/AbstractSyncService.java @@ -303,6 +303,9 @@ public abstract class AbstractSyncService implements Runnable { public boolean hasPendingRequests() { return !mRequestQueue.isEmpty(); -} + } + public void clearRequests() { + mRequestQueue.clear(); + } } diff --git a/src/com/android/exchange/adapter/EmailSyncAdapter.java b/src/com/android/exchange/adapter/EmailSyncAdapter.java index b005bcb29..d4af3ce34 100644 --- a/src/com/android/exchange/adapter/EmailSyncAdapter.java +++ b/src/com/android/exchange/adapter/EmailSyncAdapter.java @@ -28,7 +28,6 @@ import com.android.email.mail.internet.MimeMessage; import com.android.email.mail.internet.MimeUtility; import com.android.email.provider.AttachmentProvider; import com.android.email.provider.EmailContent; -import com.android.email.provider.EmailProvider; import com.android.email.provider.EmailContent.Account; import com.android.email.provider.EmailContent.AccountColumns; import com.android.email.provider.EmailContent.Attachment; @@ -37,6 +36,7 @@ import com.android.email.provider.EmailContent.Mailbox; import com.android.email.provider.EmailContent.Message; import com.android.email.provider.EmailContent.MessageColumns; import com.android.email.provider.EmailContent.SyncColumns; +import com.android.email.provider.EmailProvider; import com.android.email.service.MailService; import com.android.exchange.Eas; import com.android.exchange.EasSyncService; @@ -114,6 +114,7 @@ public class EmailSyncAdapter extends AbstractSyncAdapter { Message.MAILBOX_KEY + "=" + mMailbox.mId, null); mContentResolver.delete(Message.UPDATED_CONTENT_URI, Message.MAILBOX_KEY + "=" + mMailbox.mId, null); + mService.clearRequests(); // Delete attachments... AttachmentProvider.deleteAllMailboxAttachmentFiles(mContext, mAccount.mId, mMailbox.mId); } @@ -533,6 +534,7 @@ public class EmailSyncAdapter extends AbstractSyncAdapter { att.mFileName = fileName; att.mLocation = location; att.mMimeType = getMimeTypeFromFileName(fileName); + att.mAccountKey = mService.mAccount.mId; atts.add(att); msg.mFlagAttachment = true; } diff --git a/tests/src/com/android/email/LegacyConversionsTests.java b/tests/src/com/android/email/LegacyConversionsTests.java index fd4be31f5..fc0f9bd00 100644 --- a/tests/src/com/android/email/LegacyConversionsTests.java +++ b/tests/src/com/android/email/LegacyConversionsTests.java @@ -21,14 +21,14 @@ import com.android.email.mail.Body; import com.android.email.mail.BodyPart; import com.android.email.mail.Flag; import com.android.email.mail.Folder; -import com.android.email.mail.Message; -import com.android.email.mail.MessageTestUtils; -import com.android.email.mail.MessagingException; -import com.android.email.mail.Part; import com.android.email.mail.Folder.OpenMode; +import com.android.email.mail.Message; import com.android.email.mail.Message.RecipientType; +import com.android.email.mail.MessageTestUtils; import com.android.email.mail.MessageTestUtils.MessageBuilder; import com.android.email.mail.MessageTestUtils.MultipartBuilder; +import com.android.email.mail.MessagingException; +import com.android.email.mail.Part; import com.android.email.mail.internet.MimeBodyPart; import com.android.email.mail.internet.MimeHeader; import com.android.email.mail.internet.MimeMessage; @@ -37,10 +37,10 @@ import com.android.email.mail.internet.TextBody; import com.android.email.mail.store.LocalStore; import com.android.email.mail.store.LocalStoreUnitTests; import com.android.email.provider.EmailContent; -import com.android.email.provider.EmailProvider; -import com.android.email.provider.ProviderTestUtils; import com.android.email.provider.EmailContent.Attachment; import com.android.email.provider.EmailContent.Mailbox; +import com.android.email.provider.EmailProvider; +import com.android.email.provider.ProviderTestUtils; import android.content.ContentUris; import android.content.Context; @@ -243,9 +243,11 @@ public class LegacyConversionsTests extends ProviderTestCase2 { while (c.moveToNext()) { Attachment attachment = Attachment.getContent(c, Attachment.class); if ("101".equals(attachment.mLocation)) { - checkAttachment("attachment1Part", attachments.get(0), attachment); + checkAttachment("attachment1Part", attachments.get(0), attachment, + localMessage.mAccountKey); } else if ("102".equals(attachment.mLocation)) { - checkAttachment("attachment2Part", attachments.get(1), attachment); + checkAttachment("attachment2Part", attachments.get(1), attachment, + localMessage.mAccountKey); } else { fail("Unexpected attachment with location " + attachment.mLocation); } @@ -332,7 +334,7 @@ public class LegacyConversionsTests extends ProviderTestCase2 { } assertTrue(fromPart != null); // 2. Check values - checkAttachment(attachment.mFileName, fromPart, attachment); + checkAttachment(attachment.mFileName, fromPart, attachment, accountId); } } finally { c.close(); @@ -421,8 +423,8 @@ public class LegacyConversionsTests extends ProviderTestCase2 { * TODO content URI should only be set if we also saved a file * TODO other data encodings */ - private void checkAttachment(String tag, Part expected, EmailContent.Attachment actual) - throws MessagingException { + private void checkAttachment(String tag, Part expected, EmailContent.Attachment actual, + long accountKey) throws MessagingException { String contentType = MimeUtility.unfoldAndDecode(expected.getContentType()); String contentTypeName = MimeUtility.getHeaderParameter(contentType, "name"); assertEquals(tag, expected.getMimeType(), actual.mMimeType); @@ -459,6 +461,7 @@ public class LegacyConversionsTests extends ProviderTestCase2 { } assertEquals(tag, expectedPartId, actual.mLocation); assertEquals(tag, "B", actual.mEncoding); + assertEquals(tag, accountKey, actual.mAccountKey); } /** diff --git a/tests/src/com/android/email/provider/ProviderTestUtils.java b/tests/src/com/android/email/provider/ProviderTestUtils.java index 7c56b5338..eee103e41 100644 --- a/tests/src/com/android/email/provider/ProviderTestUtils.java +++ b/tests/src/com/android/email/provider/ProviderTestUtils.java @@ -239,6 +239,7 @@ public class ProviderTestUtils extends Assert { att.mContent = "content " + fileName; att.mFlags = flags; att.mContentBytes = Utility.toUtf8("content " + fileName); + att.mAccountKey = messageId + 0x1000; if (saveIt) { att.save(context); } @@ -420,6 +421,7 @@ public class ProviderTestUtils extends Assert { assertEquals(caller + " mFlags", expect.mFlags, actual.mFlags); MoreAsserts.assertEquals(caller + " mContentBytes", expect.mContentBytes, actual.mContentBytes); + assertEquals(caller + " mAccountKey", expect.mAccountKey, actual.mAccountKey); } /** diff --git a/tests/src/com/android/email/service/AttachmentDownloadServiceTests.java b/tests/src/com/android/email/service/AttachmentDownloadServiceTests.java index a4215f5bf..a1398b35d 100644 --- a/tests/src/com/android/email/service/AttachmentDownloadServiceTests.java +++ b/tests/src/com/android/email/service/AttachmentDownloadServiceTests.java @@ -18,16 +18,17 @@ package com.android.email.service; import com.android.email.AccountTestCase; import com.android.email.ExchangeUtils.NullEmailService; -import com.android.email.provider.ProviderTestUtils; import com.android.email.provider.EmailContent.Account; import com.android.email.provider.EmailContent.Attachment; import com.android.email.provider.EmailContent.Mailbox; import com.android.email.provider.EmailContent.Message; +import com.android.email.provider.ProviderTestUtils; import com.android.email.service.AttachmentDownloadService.DownloadRequest; import com.android.email.service.AttachmentDownloadService.DownloadSet; import android.content.Context; +import java.io.File; import java.util.Iterator; /** @@ -43,6 +44,9 @@ public class AttachmentDownloadServiceTests extends AccountTestCase { private Mailbox mMailbox; private long mAccountId; private long mMailboxId; + private AttachmentDownloadService.AccountManagerStub mAccountManagerStub; + private MockDirectory mMockDirectory; + private DownloadSet mDownloadSet; @Override @@ -62,7 +66,11 @@ public class AttachmentDownloadServiceTests extends AccountTestCase { mService = new AttachmentDownloadService(); mService.mContext = mMockContext; mService.addServiceClass(mAccountId, NullEmailService.class); + mAccountManagerStub = new AttachmentDownloadService.AccountManagerStub(null); + mService.mAccountManagerStub = mAccountManagerStub; mDownloadSet = mService.mDownloadSet; + mMockDirectory = + new MockDirectory(mService.mContext.getCacheDir().getAbsolutePath()); } @Override @@ -143,4 +151,89 @@ public class AttachmentDownloadServiceTests extends AccountTestCase { assertTrue(req.inProgress); assertTrue(mDownloadSet.mDownloadsInProgress.containsKey(att4.mId)); } + + /** + * A mock file directory containing a single (Mock)File. The total space, usable space, and + * length of the single file can be set + */ + static class MockDirectory extends File { + private static final long serialVersionUID = 1L; + private long mTotalSpace; + private long mUsableSpace; + private MockFile[] mFiles; + private final MockFile mMockFile = new MockFile(); + + + public MockDirectory(String path) { + super(path); + mFiles = new MockFile[1]; + mFiles[0] = mMockFile; + } + + private void setTotalAndUsableSpace(long total, long usable) { + mTotalSpace = total; + mUsableSpace = usable; + } + + public long getTotalSpace() { + return mTotalSpace; + } + + public long getUsableSpace() { + return mUsableSpace; + } + + public void setFileLength(long length) { + mMockFile.mLength = length; + } + + public File[] listFiles() { + return mFiles; + } + } + + /** + * A mock file that reports back a pre-set length + */ + static class MockFile extends File { + private static final long serialVersionUID = 1L; + private long mLength = 0; + + public MockFile() { + super("_mock"); + } + + public long length() { + return mLength; + } + } + + public void testCanPrefetchForAccount() { + // First, test our "global" limits (based on free storage) + // Mock storage @ 100 total and 26 available + // Note that all file lengths in this test are in arbitrary units + mMockDirectory.setTotalAndUsableSpace(100L, 26L); + // Mock 2 accounts in total + mAccountManagerStub.setNumberOfAccounts(2); + // With 26% available, we should be ok to prefetch + assertTrue(mService.canPrefetchForAccount(1, mMockDirectory)); + // Now change to 24 available + mMockDirectory.setTotalAndUsableSpace(100L, 24L); + // With 24% available, we should NOT be ok to prefetch + assertFalse(mService.canPrefetchForAccount(1, mMockDirectory)); + + // Now, test per-account storage + // Mock storage @ 100 total and 50 available + mMockDirectory.setTotalAndUsableSpace(100L, 50L); + // Mock a file of length 24, but need to uncache previous amount first + mService.mAttachmentStorageMap.remove(1L); + mMockDirectory.setFileLength(24); + // We can prefetch since 24 < half of 50 + assertTrue(mService.canPrefetchForAccount(1, mMockDirectory)); + // Mock a file of length 26, but need to uncache previous amount first + mService.mAttachmentStorageMap.remove(1L); + mMockDirectory.setFileLength(26); + // We can't prefetch since 26 > half of 50 + assertFalse(mService.canPrefetchForAccount(1, mMockDirectory)); + } }