package com.fsck.k9.view; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.List; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.AsyncTask; import android.os.Environment; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; import android.widget.Button; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import com.fsck.k9.Account; import com.fsck.k9.K9; import com.fsck.k9.R; import com.fsck.k9.cache.TemporaryAttachmentStore; import com.fsck.k9.controller.MessagingController; import com.fsck.k9.controller.MessagingListener; import com.fsck.k9.helper.FileHelper; import com.fsck.k9.helper.MediaScannerNotifier; import com.fsck.k9.helper.SizeFormatter; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Part; import com.fsck.k9.mail.internet.MimeHeader; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mailstore.LocalAttachmentBodyPart; import com.fsck.k9.provider.AttachmentProvider; import org.apache.commons.io.IOUtils; public class AttachmentView extends FrameLayout implements OnClickListener, OnLongClickListener { private Context context; private Message message; private LocalAttachmentBodyPart part; private Account account; private MessagingController controller; private MessagingListener listener; private AttachmentFileDownloadCallback callback; private Button viewButton; private Button downloadButton; private String name; private String contentType; private long size; public AttachmentView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); this.context = context; } public AttachmentView(Context context, AttributeSet attrs) { super(context, attrs); this.context = context; } public AttachmentView(Context context) { super(context); this.context = context; } public void setButtonsEnabled(boolean enabled) { viewButton.setEnabled(enabled); downloadButton.setEnabled(enabled); } /** * Populates this view with information about the attachment. *

* This method also decides which attachments are displayed when the "show attachments" button * is pressed, and which attachments are only displayed after the "show more attachments" * button was pressed.
* Inline attachments with content ID and unnamed attachments fall into the second category. *

* * @return {@code true} for a regular attachment. {@code false} for attachments that should be initially hidden. */ public boolean populateFromPart(Part inputPart, Message message, Account account, MessagingController controller, MessagingListener listener) throws MessagingException { part = (LocalAttachmentBodyPart) inputPart; this.message = message; this.account = account; this.controller = controller; this.listener = listener; boolean firstClassAttachment = extractAttachmentInformation(part); displayAttachmentInformation(); return firstClassAttachment; } //TODO: extract this code to a helper class private boolean extractAttachmentInformation(Part part) throws MessagingException { boolean firstClassAttachment = true; contentType = part.getMimeType(); String contentTypeHeader = MimeUtility.unfoldAndDecode(part.getContentType()); String contentDisposition = MimeUtility.unfoldAndDecode(part.getDisposition()); name = MimeUtility.getHeaderParameter(contentTypeHeader, "name"); if (name == null) { name = MimeUtility.getHeaderParameter(contentDisposition, "filename"); } if (name == null) { firstClassAttachment = false; String extension = MimeUtility.getExtensionByMimeType(contentType); name = "noname" + ((extension != null) ? "." + extension : ""); } // Inline parts with a content-id are almost certainly components of an HTML message // not attachments. Only show them if the user pressed the button to show more // attachments. if (contentDisposition != null && MimeUtility.getHeaderParameter(contentDisposition, null).matches("^(?i:inline)") && part.getHeader(MimeHeader.HEADER_CONTENT_ID) != null) { firstClassAttachment = false; } String sizeParam = MimeUtility.getHeaderParameter(contentDisposition, "size"); if (sizeParam != null) { try { size = Integer.parseInt(sizeParam); } catch (NumberFormatException e) { /* ignore */ } } return firstClassAttachment; } private void displayAttachmentInformation() { TextView attachmentName = (TextView) findViewById(R.id.attachment_name); TextView attachmentInfo = (TextView) findViewById(R.id.attachment_info); viewButton = (Button) findViewById(R.id.view); downloadButton = (Button) findViewById(R.id.download); if (size > K9.MAX_ATTACHMENT_DOWNLOAD_SIZE) { viewButton.setVisibility(View.GONE); downloadButton.setVisibility(View.GONE); } viewButton.setOnClickListener(this); downloadButton.setOnClickListener(this); downloadButton.setOnLongClickListener(this); attachmentName.setText(name); attachmentInfo.setText(SizeFormatter.formatSize(context, size)); ImageView thumbnail = (ImageView) findViewById(R.id.attachment_icon); new LoadAndDisplayThumbnailAsyncTask(thumbnail).execute(); } @Override public void onClick(View view) { switch (view.getId()) { case R.id.view: { onViewButtonClicked(); break; } case R.id.download: { onSaveButtonClicked(); break; } } } @Override public boolean onLongClick(View view) { if (view.getId() == R.id.download) { callback.pickDirectoryToSaveAttachmentTo(this); return true; } return false; } private void onViewButtonClicked() { if (message != null) { controller.loadAttachment(account, message, part, new Object[] {false, this}, listener); } } private void onSaveButtonClicked() { boolean isExternalStorageMounted = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED); if (!isExternalStorageMounted) { String message = context.getString(R.string.message_view_status_attachment_not_saved); displayMessageToUser(message); return; } if (message != null) { controller.loadAttachment(account, message, part, new Object[] {true, this}, listener); } } public void writeFile() { writeFile(new File(K9.getAttachmentDefaultPath())); } /** * Saves the attachment as file in the given directory */ public void writeFile(File directory) { try { File file = saveAttachmentWithUniqueFileName(directory); displayAttachmentSavedMessage(file.toString()); MediaScannerNotifier.notify(context, file); } catch (IOException ioe) { if (K9.DEBUG) { Log.e(K9.LOG_TAG, "Error saving attachment", ioe); } displayAttachmentNotSavedMessage(); } } private File saveAttachmentWithUniqueFileName(File directory) throws IOException { String filename = FileHelper.sanitizeFilename(name); File file = FileHelper.createUniqueFile(directory, filename); writeAttachmentToStorage(file); return file; } private void writeAttachmentToStorage(File file) throws IOException { Uri uri = AttachmentProvider.getAttachmentUri(account, part.getAttachmentId()); InputStream in = context.getContentResolver().openInputStream(uri); try { OutputStream out = new FileOutputStream(file); try { IOUtils.copy(in, out); out.flush(); } finally { out.close(); } } finally { in.close(); } } public void showFile() { new ViewAttachmentAsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } private Intent getBestViewIntentAndSaveFileIfNecessary() { String inferredMimeType = MimeUtility.getMimeTypeByExtension(name); IntentAndResolvedActivitiesCount resolvedIntentInfo; if (MimeUtility.isDefaultMimeType(contentType)) { resolvedIntentInfo = getBestViewIntentForMimeType(inferredMimeType); } else { resolvedIntentInfo = getBestViewIntentForMimeType(contentType); if (!resolvedIntentInfo.hasResolvedActivities() && !inferredMimeType.equals(contentType)) { resolvedIntentInfo = getBestViewIntentForMimeType(inferredMimeType); } } if (!resolvedIntentInfo.hasResolvedActivities()) { resolvedIntentInfo = getBestViewIntentForMimeType(MimeUtility.DEFAULT_ATTACHMENT_MIME_TYPE); } Intent viewIntent; if (resolvedIntentInfo.hasResolvedActivities() && resolvedIntentInfo.containsFileUri()) { try { File tempFile = TemporaryAttachmentStore.getFileForWriting(context, name); writeAttachmentToStorage(tempFile); viewIntent = createViewIntentForFileUri(resolvedIntentInfo.getMimeType(), Uri.fromFile(tempFile)); } catch (IOException e) { if (K9.DEBUG) { Log.e(K9.LOG_TAG, "Error while saving attachment to use file:// URI with ACTION_VIEW Intent", e); } viewIntent = createViewIntentForAttachmentProviderUri(MimeUtility.DEFAULT_ATTACHMENT_MIME_TYPE); } } else { viewIntent = resolvedIntentInfo.getIntent(); } return viewIntent; } private IntentAndResolvedActivitiesCount getBestViewIntentForMimeType(String mimeType) { Intent contentUriIntent = createViewIntentForAttachmentProviderUri(mimeType); int contentUriActivitiesCount = getResolvedIntentActivitiesCount(contentUriIntent); if (contentUriActivitiesCount > 0) { return new IntentAndResolvedActivitiesCount(contentUriIntent, contentUriActivitiesCount); } File tempFile = TemporaryAttachmentStore.getFile(context, name); Uri tempFileUri = Uri.fromFile(tempFile); Intent fileUriIntent = createViewIntentForFileUri(mimeType, tempFileUri); int fileUriActivitiesCount = getResolvedIntentActivitiesCount(fileUriIntent); if (fileUriActivitiesCount > 0) { return new IntentAndResolvedActivitiesCount(fileUriIntent, fileUriActivitiesCount); } return new IntentAndResolvedActivitiesCount(contentUriIntent, contentUriActivitiesCount); } private Intent createViewIntentForAttachmentProviderUri(String mimeType) { Uri uri = AttachmentProvider.getAttachmentUriForViewing(account, part.getAttachmentId(), mimeType, name); Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(uri, mimeType); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); addUiIntentFlags(intent); return intent; } private Intent createViewIntentForFileUri(String mimeType, Uri uri) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(uri, mimeType); addUiIntentFlags(intent); return intent; } private void addUiIntentFlags(Intent intent) { intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); } private int getResolvedIntentActivitiesCount(Intent intent) { PackageManager packageManager = context.getPackageManager(); List resolveInfos = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); return resolveInfos.size(); } private void displayAttachmentSavedMessage(final String filename) { String message = context.getString(R.string.message_view_status_attachment_saved, filename); displayMessageToUser(message); } private void displayAttachmentNotSavedMessage() { String message = context.getString(R.string.message_view_status_attachment_not_saved); displayMessageToUser(message); } private void displayMessageToUser(String message) { Toast.makeText(context, message, Toast.LENGTH_LONG).show(); } public void setCallback(AttachmentFileDownloadCallback callback) { this.callback = callback; } public interface AttachmentFileDownloadCallback { /** * This method is called to ask the user to pick a directory to save the attachment to. *

* After the user has selected a directory, the implementation of this interface has to call * {@link #writeFile(File)} on the object supplied as argument in order for the attachment to be saved. */ public void pickDirectoryToSaveAttachmentTo(AttachmentView caller); } private static class IntentAndResolvedActivitiesCount { private Intent intent; private int activitiesCount; IntentAndResolvedActivitiesCount(Intent intent, int activitiesCount) { this.intent = intent; this.activitiesCount = activitiesCount; } public Intent getIntent() { return intent; } public boolean hasResolvedActivities() { return activitiesCount > 0; } public String getMimeType() { return intent.getType(); } public boolean containsFileUri() { return "file".equals(intent.getData().getScheme()); } } private class LoadAndDisplayThumbnailAsyncTask extends AsyncTask { private final ImageView thumbnail; public LoadAndDisplayThumbnailAsyncTask(ImageView thumbnail) { this.thumbnail = thumbnail; } protected Bitmap doInBackground(Void... asyncTaskArgs) { return getPreviewIcon(); } private Bitmap getPreviewIcon() { Bitmap icon = null; try { InputStream input = context.getContentResolver().openInputStream( AttachmentProvider.getAttachmentThumbnailUri(account, part.getAttachmentId(), 62, 62)); icon = BitmapFactory.decodeStream(input); input.close(); } catch (Exception e) { // We don't care what happened, we just return null for the preview icon. } return icon; } protected void onPostExecute(Bitmap previewIcon) { if (previewIcon != null) { thumbnail.setImageBitmap(previewIcon); } else { thumbnail.setImageResource(R.drawable.attached_image_placeholder); } } } private class ViewAttachmentAsyncTask extends AsyncTask { @Override protected void onPreExecute() { viewButton.setEnabled(false); } @Override protected Intent doInBackground(Void... params) { return getBestViewIntentAndSaveFileIfNecessary(); } @Override protected void onPostExecute(Intent intent) { viewAttachment(intent); viewButton.setEnabled(true); } private void viewAttachment(Intent intent) { try { context.startActivity(intent); } catch (ActivityNotFoundException e) { Log.e(K9.LOG_TAG, "Could not display attachment of type " + contentType, e); String message = context.getString(R.string.message_view_no_viewer, contentType); displayMessageToUser(message); } } } }