From 976f92908dd2f69f21f62690632ff24b08d9f5d3 Mon Sep 17 00:00:00 2001 From: Marc Blank Date: Wed, 15 Jul 2009 15:08:53 -0700 Subject: [PATCH] Add support for attachments in EmailProvider and (preliminary) EAS * EmailProvider now saves Attachment records atomically with Message (and Body, of course) if an ArrayList is stored in mAttachments * Update EAS code to support attachment discovery * Update EAS code to support attachment download via service API (preliminary) * Add test for atomic attachment save * Add test for unique file creation (external) --- .../android/email/provider/EmailContent.java | 50 ++++++++- .../android/exchange/AbstractSyncService.java | 3 +- src/com/android/exchange/EasSyncService.java | 84 +++++++++++--- src/com/android/exchange/EmailContent.java | 60 ++++++++-- src/com/android/exchange/PartRequest.java | 47 +++++--- src/com/android/exchange/SyncManager.java | 6 +- .../exchange/adapter/EasEmailSyncAdapter.java | 23 +++- .../android/email/provider/ProviderTests.java | 104 +++++++++++++++++- 8 files changed, 324 insertions(+), 53 deletions(-) diff --git a/src/com/android/email/provider/EmailContent.java b/src/com/android/email/provider/EmailContent.java index 2dc0a028c..93eaedfb6 100644 --- a/src/com/android/email/provider/EmailContent.java +++ b/src/com/android/email/provider/EmailContent.java @@ -26,10 +26,12 @@ import android.content.Context; import android.content.OperationApplicationException; import android.database.Cursor; import android.net.Uri; +import android.os.Environment; import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; +import java.io.File; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; @@ -638,7 +640,7 @@ public abstract class EmailContent { public void addSaveOps(ArrayList ops) { // First, save the message - ContentProviderOperation.Builder b = getSaveOrUpdateBuilder(true, mBaseUri, mId); + ContentProviderOperation.Builder b = getSaveOrUpdateBuilder(true, mBaseUri, -1); ops.add(b.withValues(toContentValues()).build()); // Create and save the body @@ -652,8 +654,19 @@ public abstract class EmailContent { b = getSaveOrUpdateBuilder(true, Body.CONTENT_URI, 0); b.withValues(cv); ContentValues backValues = new ContentValues(); - backValues.put(Body.MESSAGE_KEY, ops.size() - 1); + int messageBackValue = ops.size() - 1; + backValues.put(Body.MESSAGE_KEY, messageBackValue); ops.add(b.withValueBackReferences(backValues).build()); + + // Create the attaachments, if any + if (mAttachments != null) { + for (Attachment att: mAttachments) { + ops.add(getSaveOrUpdateBuilder(true, Attachment.CONTENT_URI, -1) + .withValues(att.toContentValues()) + .withValueBackReference(Attachment.MESSAGE_KEY, messageBackValue) + .build()); + } + } } // Text and Html information are stored as ;;; @@ -1482,6 +1495,39 @@ public abstract class EmailContent { } } + /** + * Creates a unique file in the external store by appending a hyphen + * and a number to the given filename. + * @param filename + * @return a new File object, or null if one could not be created + */ + public static File createUniqueFile(String filename) { + // TODO Handle internal storage, as required + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + File directory = Environment.getExternalStorageDirectory(); + File file = new File(directory, filename); + if (!file.exists()) { + return file; + } + // Get the extension of the file, if any. + int index = filename.lastIndexOf('.'); + String name = filename; + String extension = ""; + if (index != -1) { + name = filename.substring(0, index); + extension = filename.substring(index); + } + for (int i = 2; i < Integer.MAX_VALUE; i++) { + file = new File(directory, name + '-' + i + extension); + if (!file.exists()) { + return file; + } + } + return null; + } + return null; + } + @Override @SuppressWarnings("unchecked") public EmailContent.Attachment restore(Cursor cursor) { diff --git a/src/com/android/exchange/AbstractSyncService.java b/src/com/android/exchange/AbstractSyncService.java index 11c2d0fdd..fee98cd68 100644 --- a/src/com/android/exchange/AbstractSyncService.java +++ b/src/com/android/exchange/AbstractSyncService.java @@ -69,8 +69,8 @@ public abstract class AbstractSyncService implements Runnable { protected String mMailboxName; public Account mAccount; protected Context mContext; - protected long mRequestTime = 0; + protected volatile long mRequestTime = 0; protected ArrayList mPartRequests = new ArrayList(); protected PartRequest mPendingPartRequest = null; @@ -285,6 +285,7 @@ public abstract class AbstractSyncService implements Runnable { public void addPartRequest(PartRequest req) { synchronized (mPartRequests) { mPartRequests.add(req); + mRequestTime = System.currentTimeMillis(); } } diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java index 59e591e55..2bdc792f0 100644 --- a/src/com/android/exchange/EasSyncService.java +++ b/src/com/android/exchange/EasSyncService.java @@ -66,6 +66,7 @@ import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; +import android.os.RemoteException; import android.util.Log; public class EasSyncService extends InteractiveSyncService { @@ -76,8 +77,11 @@ public class EasSyncService extends InteractiveSyncService { private static final String WHERE_SYNC_FREQUENCY_PING = Mailbox.SYNC_FREQUENCY + '=' + Account.CHECK_INTERVAL_PING; private static final String SYNC_FREQUENCY_PING = - MailboxColumns.SYNC_FREQUENCY + '=' + Account.CHECK_INTERVAL_PING; + MailboxColumns.SYNC_FREQUENCY + " IN (" + Account.CHECK_INTERVAL_PING + + ',' + Account.CHECK_INTERVAL_PUSH + ')'; + static private final int CHUNK_SIZE = 16 * 1024; + // Reasonable default String mProtocolVersion = "2.5"; static String mDeviceId = null; @@ -195,7 +199,7 @@ public class EasSyncService extends InteractiveSyncService { // TODO Auto-generated method stub } - protected HttpURLConnection sendEASPostCommand(String cmd, String data) throws IOException { + protected HttpURLConnection sendEASPostCommand(String cmd, String data) throws IOException { HttpURLConnection uc = setupEASCommand("POST", cmd); if (uc != null) { uc.setRequestProperty("Content-Length", Integer.toString(data.length() + 2)); @@ -208,11 +212,36 @@ public class EasSyncService extends InteractiveSyncService { return uc; } - static private final int CHUNK_SIZE = 16 * 1024; + private void doStatusCallback(IEmailServiceCallback callback, int status) { + try { + callback.status(status, 0); + } catch (RemoteException e2) { + // No danger if the client is no longer around + } + } - protected void getAttachment(PartRequest req) throws IOException { + private void doProgressCallback(IEmailServiceCallback callback, int progress) { + try { + callback.status(EmailServiceStatus.IN_PROGRESS, progress); + } catch (RemoteException e2) { + // No danger if the client is no longer around + } + } + + /** + * Loads an attachment, based on the PartRequest passed in. The PartRequest is basically our + * wrapper for Attachment + * @param req the part (attachment) to be retrieved + * @param external whether the attachment should be loaded to external storage + * @throws IOException + */ + protected void getAttachment(PartRequest req, boolean external) throws IOException { + // TODO Implement internal storage as required + IEmailServiceCallback callback = req.callback; + Attachment att = req.att; + doProgressCallback(callback, 0); DefaultHttpClient client = new DefaultHttpClient(); - String us = makeUriString("GetAttachment", "&AttachmentName=" + req.att.mLocation); + String us = makeUriString("GetAttachment", "&AttachmentName=" + att.mLocation); HttpPost method = new HttpPost(URI.create(us)); method.setHeader("Authorization", mAuthString); @@ -226,8 +255,7 @@ public class EasSyncService extends InteractiveSyncService { Log.v(TAG, "Attachment code: " + status + ", Length: " + len + ", Type: " + type); } InputStream is = res.getEntity().getContent(); - // TODO Use the request data, when it's defined. For now, stubbed out - File f = null; // Attachment.openAttachmentFile(req); + File f = Attachment.createUniqueFile(att.mFileName); if (f != null) { FileOutputStream os = new FileOutputStream(f); if (len > 0) { @@ -241,10 +269,8 @@ public class EasSyncService extends InteractiveSyncService { int read = is.read(bytes, 0, n); os.write(bytes, 0, read); len -= read; - if (req.handler != null) { - long pct = ((length - len) * 100 / length); - req.handler.sendEmptyMessage((int)pct); - } + int pct = ((length - len) * 100 / length); + doProgressCallback(callback, pct); } } finally { mPendingPartRequest = null; @@ -254,12 +280,17 @@ public class EasSyncService extends InteractiveSyncService { os.flush(); os.close(); - ContentValues cv = new ContentValues(); - cv.put(AttachmentColumns.CONTENT_URI, f.getAbsolutePath()); - cv.put(AttachmentColumns.MIME_TYPE, type); - req.att.update(mContext, cv); - // TODO Inform UI that we're done + // EmailProvider will throw an exception if we try to update an unsaved attachment + if (att.isSaved()) { + ContentValues cv = new ContentValues(); + cv.put(AttachmentColumns.CONTENT_URI, f.getAbsolutePath()); + cv.put(AttachmentColumns.MIME_TYPE, type); + att.update(mContext, cv); + doStatusCallback(callback, EmailServiceStatus.SUCCESS); + } } + } else { + doStatusCallback(callback, EmailServiceStatus.MESSAGE_NOT_FOUND); } } @@ -408,11 +439,14 @@ public class EasSyncService extends InteractiveSyncService { } // Wait for push notifications. + String threadName = Thread.currentThread().getName(); try { runPingLoop(); } catch (StaleFolderListException e) { // We break out if we get told about a stale folder list userLog("Ping interrupted; folder list requires sync..."); + } finally { + Thread.currentThread().setName(threadName); } } } @@ -471,6 +505,7 @@ public class EasSyncService extends InteractiveSyncService { // If we have some number that are ready for push, send Ping to the server s.end("PingFolders").end("Ping").end(); uc = sendEASPostCommand("Ping", s.toString()); + Thread.currentThread().setName(mAccount.mDisplayName + ": Ping"); userLog("Sending ping, timeout: " + uc.getReadTimeout() / 1000 + "s"); code = uc.getResponseCode(); userLog("Ping response: " + code); @@ -521,7 +556,7 @@ public class EasSyncService extends InteractiveSyncService { mBindArguments[0] = Long.toString(mAccount.mId); ArrayList syncList = pp.getSyncList(); for (String serverId: syncList) { - mBindArguments[1] = serverId; + mBindArguments[1] = serverId; Cursor c = cr.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, WHERE_ACCOUNT_KEY_AND_SERVER_ID, mBindArguments, null); try { @@ -626,6 +661,21 @@ public class EasSyncService extends InteractiveSyncService { runAwake(); waitForConnectivity(); + while (true) { + PartRequest req = null; + synchronized (mPartRequests) { + if (mPartRequests.isEmpty()) { + break; + } else { + req = mPartRequests.get(0); + } + } + getAttachment(req, true); + synchronized(mPartRequests) { + mPartRequests.remove(req); + } + } + EasSerializer s = new EasSerializer(); if (mailbox.mSyncKey == null) { userLog("Mailbox syncKey RESET"); diff --git a/src/com/android/exchange/EmailContent.java b/src/com/android/exchange/EmailContent.java index 3642b020f..2065e1b30 100644 --- a/src/com/android/exchange/EmailContent.java +++ b/src/com/android/exchange/EmailContent.java @@ -13,15 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.android.exchange; /** * This is a local copy of com.android.email.EmailProvider * -* Last copied from com.android.email.EmailProvider on 7/15/09 +* Last copied from com.android.email.EmailProvider on 7/16/09 */ -package com.android.exchange; - import com.android.email.R; import com.android.email.provider.EmailProvider; @@ -33,10 +32,12 @@ import android.content.Context; import android.content.OperationApplicationException; import android.database.Cursor; import android.net.Uri; +import android.os.Environment; import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; +import java.io.File; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; @@ -647,10 +648,10 @@ public abstract class EmailContent { } public void addSaveOps(ArrayList ops) { - // First, save the message - ContentProviderOperation.Builder b = getSaveOrUpdateBuilder(true, mBaseUri, mId); + // First, save the message + ContentProviderOperation.Builder b = getSaveOrUpdateBuilder(true, mBaseUri, -1); ops.add(b.withValues(toContentValues()).build()); - + // Create and save the body ContentValues cv = new ContentValues(); if (mText != null) { @@ -662,9 +663,20 @@ public abstract class EmailContent { b = getSaveOrUpdateBuilder(true, Body.CONTENT_URI, 0); b.withValues(cv); ContentValues backValues = new ContentValues(); - backValues.put(Body.MESSAGE_KEY, ops.size() - 1); + int messageBackValue = ops.size() - 1; + backValues.put(Body.MESSAGE_KEY, messageBackValue); ops.add(b.withValueBackReferences(backValues).build()); - } + + // Create the attaachments, if any + if (mAttachments != null) { + for (Attachment att: mAttachments) { + ops.add(getSaveOrUpdateBuilder(true, Attachment.CONTENT_URI, -1) + .withValues(att.toContentValues()) + .withValueBackReference(Attachment.MESSAGE_KEY, messageBackValue) + .build()); + } + } + } // Text and Html information are stored as ;;; // charset: U = us-ascii; 8 = utf-8; I = iso-8559-1; others literally (e.g. KOI8-R) @@ -1492,6 +1504,38 @@ public abstract class EmailContent { } } + /** + * Creates a unique file in the external store by appending a hyphen + * and a number to the given filename. + * @param filename + * @return a new File object, or null if one could not be created + */ + public static File createUniqueFile(String filename) { + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + File directory = Environment.getExternalStorageDirectory(); + File file = new File(directory, filename); + if (!file.exists()) { + return file; + } + // Get the extension of the file, if any. + int index = filename.lastIndexOf('.'); + String name = filename; + String extension = ""; + if (index != -1) { + name = filename.substring(0, index); + extension = filename.substring(index); + } + for (int i = 2; i < Integer.MAX_VALUE; i++) { + file = new File(directory, name + '-' + i + extension); + if (!file.exists()) { + return file; + } + } + return null; + } + return null; + } + @Override @SuppressWarnings("unchecked") public EmailContent.Attachment restore(Cursor cursor) { diff --git a/src/com/android/exchange/PartRequest.java b/src/com/android/exchange/PartRequest.java index e1cdd7e31..2fc703636 100644 --- a/src/com/android/exchange/PartRequest.java +++ b/src/com/android/exchange/PartRequest.java @@ -17,30 +17,51 @@ package com.android.exchange; -import com.android.email.provider.EmailContent; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; -import android.os.Handler; +import com.android.exchange.EmailContent.Attachment; +/** + * PartRequest is the EAS wrapper for attachment loading requests. In addition to information about + * the attachment to be loaded, it also contains the callback to be used for status/progress + * updates to the UI. + */ public class PartRequest { - public long id; + public long timeStamp; public long emailId; - public EmailContent.Attachment att; + public Attachment att; public String loc; - public long size; - public long loaded; - public Handler handler; + public IEmailServiceCallback callback; - public PartRequest (long _emailId, EmailContent.Attachment _att) { - id = System.currentTimeMillis(); + static IEmailServiceCallback sCallback = new IEmailServiceCallback () { + + /* (non-Javadoc) + * @see com.android.exchange.IEmailServiceCallback#status(int, int) + */ + public void status(int statusCode, int progress) throws RemoteException { + // This is a placeholder, so that all PartRequests have a callback (prevents a lot of + // useless checking in the sync service). When debugging, logs the status and progress + // of the download. + if (Eas.TEST_DEBUG) { + Log.d("Status: ", "Code = " + statusCode + ", progress = " + progress); + } + } + + public IBinder asBinder() { return null; } + }; + + public PartRequest(long _emailId, Attachment _att) { + timeStamp = System.currentTimeMillis(); emailId = _emailId; att = _att; loc = att.mLocation; - size = att.mSize; - loaded = 0; + callback = sCallback; } - public PartRequest (long _emailId, EmailContent.Attachment _att, Handler _handler) { + public PartRequest(long _emailId, Attachment _att, IEmailServiceCallback _callback) { this(_emailId, _att); - handler = _handler; + callback = _callback; } } diff --git a/src/com/android/exchange/SyncManager.java b/src/com/android/exchange/SyncManager.java index 67e06fc38..41edd9261 100644 --- a/src/com/android/exchange/SyncManager.java +++ b/src/com/android/exchange/SyncManager.java @@ -568,9 +568,10 @@ public class SyncManager extends Service implements Runnable { } public void run() { - log("Running"); mStop = false; + //Debug.waitForDebugger(); // DON'T CHECK IN WITH THIS + runAwake(-1); ContentResolver resolver = getContentResolver(); @@ -762,7 +763,6 @@ public class SyncManager extends Service implements Runnable { } if (service != null) { - service.mRequestTime = System.currentTimeMillis(); service.addPartRequest(req); kick(); } @@ -776,7 +776,6 @@ public class SyncManager extends Service implements Runnable { long mailboxId = msg.mMailboxKey; AbstractSyncService service = INSTANCE.mServiceMap.get(mailboxId); if (service != null) { - service.mRequestTime = System.currentTimeMillis(); return service.hasPartRequest(emailId, part); } return null; @@ -790,7 +789,6 @@ public class SyncManager extends Service implements Runnable { long mailboxId = msg.mMailboxKey; AbstractSyncService service = INSTANCE.mServiceMap.get(mailboxId); if (service != null) { - service.mRequestTime = System.currentTimeMillis(); service.cancelPartRequest(emailId, part); } } diff --git a/src/com/android/exchange/adapter/EasEmailSyncAdapter.java b/src/com/android/exchange/adapter/EasEmailSyncAdapter.java index 5df3b7c46..3a9b8b588 100644 --- a/src/com/android/exchange/adapter/EasEmailSyncAdapter.java +++ b/src/com/android/exchange/adapter/EasEmailSyncAdapter.java @@ -103,9 +103,7 @@ public class EasEmailSyncAdapter extends EasSyncAdapter { while (nextTag(EasTags.SYNC_APPLICATION_DATA) != END) { switch (tag) { case EasTags.EMAIL_ATTACHMENTS: - break; - case EasTags.EMAIL_ATTACHMENT: - attachmentParser(atts, msg); + attachmentsParser(atts, msg); break; case EasTags.EMAIL_TO: to = getValue(); @@ -194,7 +192,7 @@ public class EasEmailSyncAdapter extends EasSyncAdapter { public void attachmentParser(ArrayList atts, Message msg) throws IOException { String fileName = null; String length = null; - String lvl = null; + String location = null; while (nextTag(EasTags.EMAIL_ATTACHMENT) != END) { switch (tag) { @@ -202,7 +200,7 @@ public class EasEmailSyncAdapter extends EasSyncAdapter { fileName = getValue(); break; case EasTags.EMAIL_ATT_NAME: - lvl = getValue(); + location = getValue(); break; case EasTags.EMAIL_ATT_SIZE: length = getValue(); @@ -212,16 +210,29 @@ public class EasEmailSyncAdapter extends EasSyncAdapter { } } - if (fileName != null && length != null && lvl != null) { + if (fileName != null && length != null && location != null) { Attachment att = new Attachment(); att.mEncoding = "base64"; att.mSize = Long.parseLong(length); att.mFileName = fileName; + att.mLocation = location; atts.add(att); msg.mFlagAttachment = true; } } + public void attachmentsParser(ArrayList atts, Message msg) throws IOException { + while (nextTag(EasTags.EMAIL_ATTACHMENTS) != END) { + switch (tag) { + case EasTags.EMAIL_ATTACHMENT: + attachmentParser(atts, msg); + break; + default: + skipTag(); + } + } + } + private Cursor getServerIdCursor(String serverId, String[] projection) { bindArguments[0] = serverId; bindArguments[1] = mMailboxIdAsString; diff --git a/tests/src/com/android/email/provider/ProviderTests.java b/tests/src/com/android/email/provider/ProviderTests.java index 796e2bef3..59f79b750 100644 --- a/tests/src/com/android/email/provider/ProviderTests.java +++ b/tests/src/com/android/email/provider/ProviderTests.java @@ -16,7 +16,13 @@ package com.android.email.provider; +import java.io.File; +import java.io.IOException; + +import java.util.ArrayList; + import com.android.email.provider.EmailContent.Account; +import com.android.email.provider.EmailContent.Attachment; import com.android.email.provider.EmailContent.Body; import com.android.email.provider.EmailContent.Mailbox; import com.android.email.provider.EmailContent.Message; @@ -28,6 +34,7 @@ import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.net.Uri; +import android.os.Environment; import android.test.ProviderTestCase2; /** @@ -87,10 +94,22 @@ public class ProviderTests extends ProviderTestCase2 { ProviderTestUtils.assertMailboxEqual("testMailboxSave", box1, box2); } + private static Attachment setupAttachment(String fileName, long length) { + Attachment att = new Attachment(); + att.mFileName = fileName; + att.mLocation = "location" + length; + att.mSize = length; + return att; + } + + public static String[] expectedAttachmentNames = + new String[] {"attachment1.doc", "attachment2.xls", "attachment3"}; + // The lengths need to be kept in ascending order + public static long[] expectedAttachmentSizes = new long[] {31415L, 97701L, 151213L}; + /** * Test simple message save/retrieve * - * TODO: attachments * TODO: serverId vs. serverIntId */ public void testMessageSave() { @@ -137,7 +156,45 @@ public class ProviderTests extends ProviderTestCase2 { assertEquals("body text", text2, body2.mTextContent); assertEquals("body html", html2, body2.mHtmlContent); } finally { - if (c != null) c.close(); + c.close(); + } + + Message message3 = ProviderTestUtils.setupMessage("message3", account1Id, box1Id, true, + false, mMockContext); + ArrayList atts = new ArrayList(); + for (int i = 0; i < 3; i++) { + atts.add(setupAttachment(expectedAttachmentNames[i], expectedAttachmentSizes[i])); + } + message3.mAttachments = atts; + message3.saveOrUpdate(mMockContext); + long message3Id = message3.mId; + + // Now check the attachments; there should be three and they should match name and size + c = null; + try { + // Note that there is NO guarantee of the order of returned records in the general case, + // so we specifically ask for ordering by size. The expectedAttachmentSizes array must + // be kept sorted by size (ascending) for this test to work properly + c = mMockContext.getContentResolver().query( + Attachment.CONTENT_URI, + Attachment.CONTENT_PROJECTION, + Attachment.MESSAGE_KEY + "=?", + new String[] { + String.valueOf(message3Id) + }, + Attachment.SIZE); + int numAtts = c.getCount(); + assertEquals(3, numAtts); + int i = 0; + while (c.moveToNext()) { + assertEquals(expectedAttachmentNames[i], + c.getString(Attachment.CONTENT_FILENAME_COLUMN)); + assertEquals(expectedAttachmentSizes[i], + c.getLong(Attachment.CONTENT_SIZE_COLUMN)); + i++; + } + } finally { + c.close(); } } @@ -487,4 +544,47 @@ public class ProviderTests extends ProviderTestCase2 { * TODO: attachments */ + /** + * Test that our unique file name algorithm works as expected. Since this test requires an + * SD card, we check the environment first, and return immediately if none is mounted. + * @throws IOException + */ + public void testCreateUniqueFile() throws IOException { + // Delete existing files, if they exist + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + return; + } + try { + String fileName = "A11achm3n1.doc"; + File uniqueFile = Attachment.createUniqueFile(fileName); + assertEquals(fileName, uniqueFile.getName()); + if (uniqueFile.createNewFile()) { + uniqueFile = Attachment.createUniqueFile(fileName); + assertEquals("A11achm3n1-2.doc", uniqueFile.getName()); + if (uniqueFile.createNewFile()) { + uniqueFile = Attachment.createUniqueFile(fileName); + assertEquals("A11achm3n1-3.doc", uniqueFile.getName()); + } + } + fileName = "A11achm3n1"; + uniqueFile = Attachment.createUniqueFile(fileName); + assertEquals(fileName, uniqueFile.getName()); + if (uniqueFile.createNewFile()) { + uniqueFile = Attachment.createUniqueFile(fileName); + assertEquals("A11achm3n1-2", uniqueFile.getName()); + } + } finally { + File directory = Environment.getExternalStorageDirectory(); + // These are the files that should be created earlier in the test. Make sure + // they are deleted for the next go-around + String[] fileNames = new String[] {"A11achm3n1.doc", "A11achm3n1-2.doc", "A11achm3n1"}; + int length = fileNames.length; + for (int i = 0; i < length; i++) { + File file = new File(directory, fileNames[i]); + if (file.exists()) { + file.delete(); + } + } + } + } }