/** * Copyright (c) 2011, Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.mail.compose; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteException; import android.net.Uri; import android.os.ParcelFileDescriptor; import android.provider.OpenableColumns; import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.LinearLayout; import com.android.mail.R; import com.android.mail.providers.Account; import com.android.mail.providers.Attachment; import com.android.mail.ui.AttachmentTile; import com.android.mail.ui.AttachmentTile.AttachmentPreview; import com.android.mail.ui.AttachmentTileGrid; import com.android.mail.utils.LogTag; import com.android.mail.utils.LogUtils; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Lists; import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; /* * View for displaying attachments in the compose screen. */ class AttachmentsView extends LinearLayout { private static final String LOG_TAG = LogTag.getLogTag(); private final ArrayList mAttachments; private AttachmentAddedOrDeletedListener mChangeListener; private AttachmentTileGrid mTileGrid; private LinearLayout mAttachmentLayout; public AttachmentsView(Context context) { this(context, null); } public AttachmentsView(Context context, AttributeSet attrs) { super(context, attrs); mAttachments = Lists.newArrayList(); } @Override protected void onFinishInflate() { super.onFinishInflate(); mTileGrid = (AttachmentTileGrid) findViewById(R.id.attachment_tile_grid); mAttachmentLayout = (LinearLayout) findViewById(R.id.attachment_bar_list); } public void expandView() { mTileGrid.setVisibility(VISIBLE); mAttachmentLayout.setVisibility(VISIBLE); InputMethodManager imm = (InputMethodManager) getContext().getSystemService( Context.INPUT_METHOD_SERVICE); if (imm != null) { imm.hideSoftInputFromWindow(getWindowToken(), 0); } } /** * Set a listener for changes to the attachments. */ public void setAttachmentChangesListener(AttachmentAddedOrDeletedListener listener) { mChangeListener = listener; } /** * Adds an attachment and updates the ui accordingly. */ private void addAttachment(final Attachment attachment) { mAttachments.add(attachment); // If the attachment is inline do not display this attachment. if (attachment.isInlineAttachment()) { return; } if (!isShown()) { setVisibility(View.VISIBLE); } expandView(); // If we have an attachment that should be shown in a tiled look, // set up the tile and add it to the tile grid. if (AttachmentTile.isTiledAttachment(attachment)) { final ComposeAttachmentTile attachmentTile = mTileGrid.addComposeTileFromAttachment(attachment); attachmentTile.addDeleteListener(new OnClickListener() { @Override public void onClick(View v) { deleteAttachment(attachmentTile, attachment); } }); // Otherwise, use the old bar look and add it to the new // inner LinearLayout. } else { final AttachmentComposeView attachmentView = new AttachmentComposeView(getContext(), attachment); attachmentView.addDeleteListener(new OnClickListener() { @Override public void onClick(View v) { deleteAttachment(attachmentView, attachment); } }); mAttachmentLayout.addView(attachmentView, new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT)); } if (mChangeListener != null) { mChangeListener.onAttachmentAdded(); } } @VisibleForTesting protected void deleteAttachment(final View attachmentView, final Attachment attachment) { mAttachments.remove(attachment); ((ViewGroup) attachmentView.getParent()).removeView(attachmentView); if (mChangeListener != null) { mChangeListener.onAttachmentDeleted(); } } /** * Get all attachments being managed by this view. * @return attachments. */ public ArrayList getAttachments() { return mAttachments; } /** * Get all attachments previews that have been loaded * @return attachments previews. */ public ArrayList getAttachmentPreviews() { return mTileGrid.getAttachmentPreviews(); } /** * Call this on restore instance state so previews persist across configuration changes */ public void setAttachmentPreviews(ArrayList previews) { mTileGrid.setAttachmentPreviews(previews); } /** * Delete all attachments being managed by this view. */ public void deleteAllAttachments() { mAttachments.clear(); mTileGrid.removeAllViews(); mAttachmentLayout.removeAllViews(); setVisibility(GONE); } /** * Get the total size of all attachments currently in this view. */ private long getTotalAttachmentsSize() { long totalSize = 0; for (Attachment attachment : mAttachments) { totalSize += attachment.size; } return totalSize; } /** * Interface to implement to be notified about changes to the attachments * explicitly made by the user. */ public interface AttachmentAddedOrDeletedListener { public void onAttachmentDeleted(); public void onAttachmentAdded(); } /** * Generate an {@link Attachment} object for a given local content URI. Attempts to populate * the {@link Attachment#name}, {@link Attachment#size}, and {@link Attachment#contentType} * fields using a {@link ContentResolver}. * * @param contentUri * @return an Attachment object * @throws AttachmentFailureException */ public Attachment generateLocalAttachment(Uri contentUri) throws AttachmentFailureException { if (contentUri == null || TextUtils.isEmpty(contentUri.getPath())) { throw new AttachmentFailureException("Failed to create local attachment"); } // FIXME: do not query resolver for type on the UI thread final ContentResolver contentResolver = getContext().getContentResolver(); String contentType = contentResolver.getType(contentUri); if (contentType == null) contentType = ""; final Attachment attachment = new Attachment(); attachment.uri = null; // URI will be assigned by the provider upon send/save attachment.setName(null); attachment.size = 0; attachment.contentUri = contentUri; attachment.thumbnailUri = contentUri; Cursor metadataCursor = null; try { metadataCursor = contentResolver.query( contentUri, new String[]{OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE}, null, null, null); if (metadataCursor != null) { try { if (metadataCursor.moveToNext()) { attachment.setName(metadataCursor.getString(0)); attachment.size = metadataCursor.getInt(1); } } finally { metadataCursor.close(); } } } catch (SQLiteException ex) { // One of the two columns is probably missing, let's make one more attempt to get at // least one. // Note that the documentations in Intent#ACTION_OPENABLE and // OpenableColumns seem to contradict each other about whether these columns are // required, but it doesn't hurt to fail properly. // Let's try to get DISPLAY_NAME try { metadataCursor = getOptionalColumn(contentResolver, contentUri, OpenableColumns.DISPLAY_NAME); if (metadataCursor != null && metadataCursor.moveToNext()) { attachment.setName(metadataCursor.getString(0)); } } finally { if (metadataCursor != null) metadataCursor.close(); } // Let's try to get SIZE try { metadataCursor = getOptionalColumn(contentResolver, contentUri, OpenableColumns.SIZE); if (metadataCursor != null && metadataCursor.moveToNext()) { attachment.size = metadataCursor.getInt(0); } else { // Unable to get the size from the metadata cursor. Open the file and seek. attachment.size = getSizeFromFile(contentUri, contentResolver); } } finally { if (metadataCursor != null) metadataCursor.close(); } } catch (SecurityException e) { throw new AttachmentFailureException("Security Exception from attachment uri", e); } if (attachment.getName() == null) { attachment.setName(contentUri.getLastPathSegment()); } if (attachment.size == 0) { // if the attachment is not a content:// for example, a file:// URI attachment.size = getSizeFromFile(contentUri, contentResolver); } attachment.setContentType(contentType); return attachment; } /** * Adds an attachment of either local or remote origin, checking to see if the attachment * exceeds file size limits. * @param account * @param attachment the attachment to be added. * * @return size of the attachment added. * @throws AttachmentFailureException if an error occurs adding the attachment. */ public long addAttachment(Account account, Attachment attachment) throws AttachmentFailureException { final int maxSize = account.settings.getMaxAttachmentSize(); // Error getting the size or the size was too big. if (attachment.size == -1 || attachment.size > maxSize) { throw new AttachmentFailureException( "Attachment too large to attach", R.string.too_large_to_attach_single); } else if ((getTotalAttachmentsSize() + attachment.size) > maxSize) { throw new AttachmentFailureException( "Attachment too large to attach", R.string.too_large_to_attach_additional); } else { addAttachment(attachment); } return attachment.size; } private static int getSizeFromFile(Uri uri, ContentResolver contentResolver) { int size = -1; ParcelFileDescriptor file = null; try { file = contentResolver.openFileDescriptor(uri, "r"); size = (int) file.getStatSize(); } catch (FileNotFoundException e) { LogUtils.w(LOG_TAG, e, "Error opening file to obtain size."); } finally { try { if (file != null) { file.close(); } } catch (IOException e) { LogUtils.w(LOG_TAG, "Error closing file opened to obtain size."); } } // We only want to return a non-negative value. (ParcelFileDescriptor#getStatSize() will // return -1 if the fd is not a file return Math.max(size, 0); } /** * @return a cursor to the requested column or null if an exception occurs while trying * to query it. */ private static Cursor getOptionalColumn(ContentResolver contentResolver, Uri uri, String columnName) { Cursor result = null; try { result = contentResolver.query(uri, new String[]{columnName}, null, null, null); } catch (SQLiteException ex) { // ignore, leave result null } return result; } public void focusLastAttachment() { Attachment lastAttachment = mAttachments.get(mAttachments.size() - 1); View lastView = null; int last = 0; if (AttachmentTile.isTiledAttachment(lastAttachment)) { last = mTileGrid.getChildCount() - 1; if (last > 0) { lastView = mTileGrid.getChildAt(last); } } else { last = mAttachmentLayout.getChildCount() - 1; if (last > 0) { lastView = mAttachmentLayout.getChildAt(last); } } if (lastView != null) { lastView.requestFocus(); } } /** * Class containing information about failures when adding attachments. */ static class AttachmentFailureException extends Exception { private static final long serialVersionUID = 1L; private final int errorRes; public AttachmentFailureException(String detailMessage) { super(detailMessage); this.errorRes = R.string.generic_attachment_problem; } public AttachmentFailureException(String error, int errorRes) { super(error); this.errorRes = errorRes; } public AttachmentFailureException(String detailMessage, Throwable throwable) { super(detailMessage, throwable); this.errorRes = R.string.generic_attachment_problem; } /** * Get the error string resource that corresponds to this attachment failure. Always a valid * string resource. */ public int getErrorRes() { return errorRes; } } }