diff --git a/src/com/android/email/Controller.java b/src/com/android/email/Controller.java index 3f8e2cafd..2dcbc8cd3 100644 --- a/src/com/android/email/Controller.java +++ b/src/com/android/email/Controller.java @@ -18,6 +18,7 @@ package com.android.email; import com.android.email.mail.MessagingException; import com.android.email.mail.Store; +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; @@ -38,6 +39,7 @@ import android.net.Uri; import android.os.RemoteException; import android.util.Log; +import java.io.File; import java.util.HashSet; /** @@ -364,24 +366,29 @@ public class Controller { } /** - * Request that an attachment be loaded + * Request that an attachment be loaded. It will be stored at a location controlled + * by the AttachmentProvider. * - * @param save If true, attachment will be saved into a well-known place e.g. sdcard * @param attachmentId the attachment to load * @param messageId the owner message + * @param accountId the owner account * @param callback the Controller callback by which results will be reported */ - public void loadAttachment(boolean save, long attachmentId, long messageId, + public void loadAttachment(long attachmentId, long messageId, long accountId, final Result callback, Object tag) { Attachment attachInfo = Attachment.restoreAttachmentWithId(mProviderContext, attachmentId); + File saveToFile = AttachmentProvider.getAttachmentFilename(mContext, + accountId, attachmentId); + // Split here for target type (Service or MessagingController) IEmailService service = getServiceForMessage(messageId); if (service != null) { // Service implementation try { - service.loadAttachment(attachInfo.mId, null, + service.loadAttachment(attachInfo.mId, saveToFile.getAbsolutePath(), + AttachmentProvider.getAttachmentUri(accountId, attachmentId).toString(), new LoadAttachmentCallback(callback, tag)); } catch (RemoteException e) { // TODO Change exception handling to be consistent with however this method diff --git a/src/com/android/email/activity/MessageView.java b/src/com/android/email/activity/MessageView.java index 8b866966b..4b862554d 100644 --- a/src/com/android/email/activity/MessageView.java +++ b/src/com/android/email/activity/MessageView.java @@ -29,12 +29,15 @@ import com.android.email.mail.Message.RecipientType; import com.android.email.mail.internet.EmailHtmlUtil; import com.android.email.mail.internet.MimeUtility; import com.android.email.mail.store.LocalStore.LocalMessage; +import com.android.email.provider.AttachmentProvider; 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.BodyColumns; import com.android.email.provider.EmailContent.Message; +import org.apache.commons.io.IOUtils; + import android.app.Activity; import android.app.ProgressDialog; import android.content.ActivityNotFoundException; @@ -43,6 +46,7 @@ import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.drawable.Drawable; import android.media.MediaScannerConnection; import android.media.MediaScannerConnection.MediaScannerConnectionClient; @@ -54,7 +58,6 @@ import android.os.Handler; import android.provider.Contacts; import android.provider.Contacts.Intents; import android.provider.Contacts.People; -//import android.provider.Contacts.Presence; import android.text.util.Regex; import android.util.Log; import android.view.LayoutInflater; @@ -71,6 +74,10 @@ import android.widget.TextView; import android.widget.Toast; import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.ArrayList; import java.util.Date; import java.util.regex.Matcher; @@ -231,23 +238,7 @@ public class MessageView extends Activity case MSG_FINISH_LOAD_ATTACHMENT: boolean save = msg.arg1 != 0; long attachmentId = (Long)msg.obj; - if (save) { - Attachment attachment = - Attachment.restoreAttachmentWithId(MessageView.this, attachmentId); - if (attachment.mContentUri.startsWith("file://")) { - File file = - new File(attachment.mContentUri.substring("file://".length())); - String leaf = file.getName(); - Toast.makeText(MessageView.this, String.format( - getString(R.string.message_view_status_attachment_saved), leaf), - Toast.LENGTH_LONG).show(); - new MediaScannerNotifier(MessageView.this, file, mHandler); - } else { - // TODO content// uri - } - } else { - // TODO: view - } + doFinishLoadAttachment(save, attachmentId); break; default: super.handleMessage(msg); @@ -684,21 +675,15 @@ public class MessageView extends Activity LoadAttachTag tag = new LoadAttachTag(true, attachment.name); - Controller.getInstance(getApplication()).loadAttachment(true, attachment.attachmentId, - mMessageId, mControllerCallback, tag); + Controller.getInstance(getApplication()).loadAttachment(attachment.attachmentId, + mMessageId, mAccountId, mControllerCallback, tag); } private void onViewAttachment(AttachmentInfo attachment) { - // TODO: Until EAS can download into temp mem *and* AttachmentProvider is rewritten - // to use the new database, "view" is really just "save" - onDownloadAttachment(attachment); + LoadAttachTag tag = new LoadAttachTag(false, attachment.name); -// MessagingController.getInstance(getApplication()).loadAttachment( -// mAccount, -// mOldMessage, -// attachment.part, -// new Object[] { false, attachment }, -// mListener); + Controller.getInstance(getApplication()).loadAttachment(attachment.attachmentId, + mMessageId, mAccountId, mControllerCallback, tag); } private void onShowPictures() { @@ -793,14 +778,12 @@ public class MessageView extends Activity private Bitmap getPreviewIcon(AttachmentInfo attachment) { try { - // TODO write the new call to the attachment provider using ID, not part -// return BitmapFactory.decodeStream( -// getContentResolver().openInputStream( -// AttachmentProvider.getAttachmentThumbnailUri(mAccount, -// attachment.part.getAttachmentId(), -// 62, -// 62))); - return null; + return BitmapFactory.decodeStream( + getContentResolver().openInputStream( + AttachmentProvider.getAttachmentThumbnailUri( + mAccountId, attachment.attachmentId, + 62, + 62))); } catch (Exception e) { /* @@ -1520,6 +1503,54 @@ public class MessageView extends Activity } } + /** + * Back in the UI thread, handle the final steps of downloading an attachment (view or save). + * + * @param save If true, save to SD card. If false, send view intent + * @param attachmentId the attachment that was just downloaded + */ + private void doFinishLoadAttachment(boolean save, long attachmentId) { + Attachment attachment = + Attachment.restoreAttachmentWithId(MessageView.this, attachmentId); + Uri attachmentUri = AttachmentProvider.getAttachmentUri(mAccountId, attachment.mId); + Uri contentUri = + AttachmentProvider.resolveAttachmentIdToContentUri(getContentResolver(), attachmentUri); + + if (save) { + try { + File file = createUniqueFile(Environment.getExternalStorageDirectory(), + attachment.mFileName); + InputStream in = getContentResolver().openInputStream(contentUri); + OutputStream out = new FileOutputStream(file); + IOUtils.copy(in, out); + out.flush(); + out.close(); + in.close(); + + Toast.makeText(MessageView.this, String.format( + getString(R.string.message_view_status_attachment_saved), file.getName()), + Toast.LENGTH_LONG).show(); + + new MediaScannerNotifier(this, file, mHandler); + } catch (IOException ioe) { + Toast.makeText(MessageView.this, + getString(R.string.message_view_status_attachment_not_saved), + Toast.LENGTH_LONG).show(); + } + } else { + try { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(contentUri); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + startActivity(intent); + } catch (ActivityNotFoundException e) { + mHandler.attachmentViewError(); + // TODO: Add a proper warning message (and lots of upstream cleanup to prevent + // it from happening) in the next release. + } + } + } + /** * This notifier is created after an attachment completes downloaded. It attaches to the * media scanner and waits to handle the completion of the scan. At that point it tries diff --git a/src/com/android/email/provider/AttachmentProvider.java b/src/com/android/email/provider/AttachmentProvider.java index f1ac26b36..5617162d1 100644 --- a/src/com/android/email/provider/AttachmentProvider.java +++ b/src/com/android/email/provider/AttachmentProvider.java @@ -24,6 +24,7 @@ import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; +import android.content.Context; import android.database.Cursor; import android.database.MatrixCursor; import android.graphics.Bitmap; @@ -46,6 +47,15 @@ import java.util.List; * * And for access to thumbnails: * content://com.android.email.attachmentprovider/acct#/attach#/THUMBNAIL/width#/height# + * + * The on-disk (storage) schema is as follows. + * + * Attachments are stored at: /account#.db_att/item# + * Thumbnails are stored at: /thmb_account#_item# + * + * Using the standard application context, account #10 and attachment # 20, this would be: + * /data/data/com.android.email/databases/10.db_att/20 + * /data/data/com.android.email/cache/thmb_10_20 */ public class AttachmentProvider extends ContentProvider { @@ -74,10 +84,10 @@ public class AttachmentProvider extends ContentProvider { .build(); } - public static Uri getAttachmentThumbnailUri(EmailContent.Account account, long id, + public static Uri getAttachmentThumbnailUri(long accountId, long id, int width, int height) { return CONTENT_URI.buildUpon() - .appendPath(Long.toString(account.mId)) + .appendPath(Long.toString(accountId)) .appendPath(Long.toString(id)) .appendPath(FORMAT_THUMBNAIL) .appendPath(Integer.toString(width)) @@ -85,6 +95,18 @@ public class AttachmentProvider extends ContentProvider { .build(); } + /** + * Return the filename for a given attachment. This should be used by any code that is + * going to *write* attachments. + * + * This does not create or write the file, or even the directories. It simply builds + * the filename that should be used. + */ + public static File getAttachmentFilename(Context context, long accountId, long attachmentId) { + return new File( + context.getDatabasePath(accountId + ".db_att"), Long.toString(attachmentId)); + } + @Override public boolean onCreate() { /* diff --git a/src/com/android/email/service/EmailServiceProxy.java b/src/com/android/email/service/EmailServiceProxy.java index fc8690d97..b3459b6be 100644 --- a/src/com/android/email/service/EmailServiceProxy.java +++ b/src/com/android/email/service/EmailServiceProxy.java @@ -128,12 +128,12 @@ public class EmailServiceProxy implements IEmailService { } } - public void loadAttachment(final long attachmentId, final String directory, - final IEmailServiceCallback cb) throws RemoteException { + public void loadAttachment(final long attachmentId, final String destinationFile, + final String contentUriString, final IEmailServiceCallback cb) throws RemoteException { setTask(new Runnable () { public void run() { try { - mService.loadAttachment(attachmentId, directory, cb); + mService.loadAttachment(attachmentId, destinationFile, contentUriString, cb); } catch (RemoteException e) { } } diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java index 0d339d6a7..a9e45e0d3 100644 --- a/src/com/android/exchange/EasSyncService.java +++ b/src/com/android/exchange/EasSyncService.java @@ -268,7 +268,6 @@ public class EasSyncService extends InteractiveSyncService { * 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) throws IOException { @@ -291,8 +290,15 @@ public class EasSyncService extends InteractiveSyncService { Log.v(TAG, "Attachment code: " + status + ", Length: " + len + ", Type: " + type); } InputStream is = res.getEntity().getContent(); - File f = createUniqueFileInternal(req.dir, att.mFileName); + File f = (req.destination != null) + ? new File(req.destination) + : createUniqueFileInternal(req.destination, att.mFileName); if (f != null) { + // Ensure that the target directory exists + File destDir = f.getParentFile(); + if (!destDir.exists()) { + destDir.mkdirs(); + } FileOutputStream os = new FileOutputStream(f); if (len > 0) { try { @@ -318,8 +324,11 @@ public class EasSyncService extends InteractiveSyncService { // EmailProvider will throw an exception if we try to update an unsaved attachment if (att.isSaved()) { + String contentUriString = (req.contentUriString != null) + ? req.contentUriString + : "file://" + f.getAbsolutePath(); ContentValues cv = new ContentValues(); - cv.put(AttachmentColumns.CONTENT_URI, "file://" + f.getAbsolutePath()); + cv.put(AttachmentColumns.CONTENT_URI, contentUriString); cv.put(AttachmentColumns.MIME_TYPE, type); att.update(mContext, cv); doStatusCallback(callback, msg.mId, att.mId, EmailServiceStatus.SUCCESS); diff --git a/src/com/android/exchange/IEmailService.aidl b/src/com/android/exchange/IEmailService.aidl index 8cf9f0925..f0f294af5 100644 --- a/src/com/android/exchange/IEmailService.aidl +++ b/src/com/android/exchange/IEmailService.aidl @@ -27,7 +27,8 @@ interface IEmailService { void stopSync(long mailboxId); void loadMore(long messageId, IEmailServiceCallback cb); - void loadAttachment(long attachmentId, String directory, IEmailServiceCallback cb); + void loadAttachment(long attachmentId, String destinationFile, String contentUriString, + IEmailServiceCallback cb); void updateFolderList(long accountId); diff --git a/src/com/android/exchange/PartRequest.java b/src/com/android/exchange/PartRequest.java index 616d8a251..65b3521bf 100644 --- a/src/com/android/exchange/PartRequest.java +++ b/src/com/android/exchange/PartRequest.java @@ -32,7 +32,8 @@ public class PartRequest { public long timeStamp; public long emailId; public Attachment att; - public String dir; + public String destination; + public String contentUriString; public String loc; public IEmailServiceCallback callback; @@ -63,9 +64,11 @@ public class PartRequest { callback = sCallback; } - public PartRequest(Attachment _att, String _dir, IEmailServiceCallback _callback) { + public PartRequest(Attachment _att, String _destination, String _contentUriString, + IEmailServiceCallback _callback) { this(_att); - dir = _dir; + destination = _destination; + contentUriString = _contentUriString; callback = _callback; } } diff --git a/src/com/android/exchange/SyncManager.java b/src/com/android/exchange/SyncManager.java index bbbf570e7..e642e02b1 100644 --- a/src/com/android/exchange/SyncManager.java +++ b/src/com/android/exchange/SyncManager.java @@ -121,10 +121,10 @@ public class SyncManager extends Service implements Runnable { stopManualSync(mailboxId); } - public void loadAttachment(long attachmentId, String directory, IEmailServiceCallback cb) - throws RemoteException { + public void loadAttachment(long attachmentId, String destinationFile, + String contentUriString, IEmailServiceCallback cb) throws RemoteException { Attachment att = Attachment.restoreAttachmentWithId(SyncManager.this, attachmentId); - partRequest(new PartRequest(att, directory, cb)); + partRequest(new PartRequest(att, destinationFile, contentUriString, cb)); } public void updateFolderList(long accountId) throws RemoteException { diff --git a/tests/src/com/android/email/provider/AttachmentProviderTests.java b/tests/src/com/android/email/provider/AttachmentProviderTests.java index 1bbf2e46c..57955cb3d 100644 --- a/tests/src/com/android/email/provider/AttachmentProviderTests.java +++ b/tests/src/com/android/email/provider/AttachmentProviderTests.java @@ -230,10 +230,10 @@ public class AttachmentProviderTests extends ProviderTestCase2