Implement prefetch of IMAP/EAS attachments

* Load attachments in the background for IMAP/EAS messages
* Download an attachment from account X if:
  1) 25% of total storage free
  2) Attachments for X use < 1/N of 25% of total storage, where N is
     the number of AccountManager accounts
* Add accountKey to Attachment table for performance

Change-Id: I913aa710f34f48fcc4210ddf77393ab38323fe59
This commit is contained in:
Marc Blank 2010-12-08 17:11:04 -08:00
parent f946ff0019
commit 75a873be84
9 changed files with 306 additions and 52 deletions

View File

@ -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);

View File

@ -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 + "]";
}
}

View File

@ -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:

View File

@ -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<Long, Class<? extends Service>> mAccountServiceMap =
new HashMap<Long, Class<? extends Service>>();
// 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<Long, Long> mAttachmentStorageMap = new HashMap<Long, Long>();
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<? extends Service> 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;

View File

@ -303,6 +303,9 @@ public abstract class AbstractSyncService implements Runnable {
public boolean hasPendingRequests() {
return !mRequestQueue.isEmpty();
}
}
public void clearRequests() {
mRequestQueue.clear();
}
}

View File

@ -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;
}

View File

@ -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<EmailProvider> {
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<EmailProvider> {
}
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<EmailProvider> {
* 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<EmailProvider> {
}
assertEquals(tag, expectedPartId, actual.mLocation);
assertEquals(tag, "B", actual.mEncoding);
assertEquals(tag, accountKey, actual.mAccountKey);
}
/**

View File

@ -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);
}
/**

View File

@ -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));
}
}