Finish Attachment support for EAS accounts.

* Change service API to allow caller to supply complete target path/file
* Also allow caller to supply the final content_uri
* In MessageView, use full integration with EAS service API and
  attachments content provider to enable:
  * Save: Only works on SD card
  * View: Works w/o SD card using content provider & intents
  * Thumbnail previews
This commit is contained in:
Andrew Stadler 2009-07-27 19:52:21 -07:00
parent 3d25a519ab
commit a98de7e55e
9 changed files with 133 additions and 60 deletions

View File

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

View File

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

View File

@ -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: <database-path>/account#.db_att/item#
* Thumbnails are stored at: <cache-path>/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() {
/*

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -230,10 +230,10 @@ public class AttachmentProviderTests extends ProviderTestCase2<AttachmentProvide
assertEquals("text/plain", type);
// Check the returned filetypes for the thumbnails
uri = AttachmentProvider.getAttachmentThumbnailUri(account1, attachment2Id, 62, 62);
uri = AttachmentProvider.getAttachmentThumbnailUri(account1.mId, attachment2Id, 62, 62);
type = mMockResolver.getType(uri);
assertEquals("image/png", type);
uri = AttachmentProvider.getAttachmentThumbnailUri(account1, attachment3Id, 62, 62);
uri = AttachmentProvider.getAttachmentThumbnailUri(account1.mId, attachment3Id, 62, 62);
type = mMockResolver.getType(uri);
assertEquals("image/png", type);
}
@ -329,9 +329,9 @@ public class AttachmentProviderTests extends ProviderTestCase2<AttachmentProvide
// attachment we add will be id=1 and the 2nd will have id=2. This could fail on
// a legitimate implementation. Asserts below will catch this and fail the test
// if necessary.
Uri thumb1Uri = AttachmentProvider.getAttachmentThumbnailUri(account1, attachment1Id,
Uri thumb1Uri = AttachmentProvider.getAttachmentThumbnailUri(account1.mId, attachment1Id,
62, 62);
Uri thumb2Uri = AttachmentProvider.getAttachmentThumbnailUri(account1, attachment2Id,
Uri thumb2Uri = AttachmentProvider.getAttachmentThumbnailUri(account1.mId, attachment2Id,
62, 62);
// Test with no attached database - should return null (used to throw SQLiteException)