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:
parent
3d25a519ab
commit
a98de7e55e
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
/*
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue