Home | History | Annotate | Download | only in compose
      1 /**
      2  * Copyright (c) 2011, Google Inc.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *     http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.mail.compose;
     17 
     18 import android.content.ContentResolver;
     19 import android.content.Context;
     20 import android.database.Cursor;
     21 import android.database.sqlite.SQLiteException;
     22 import android.net.Uri;
     23 import android.os.ParcelFileDescriptor;
     24 import android.provider.OpenableColumns;
     25 import android.text.TextUtils;
     26 import android.util.AttributeSet;
     27 import android.view.View;
     28 import android.view.ViewGroup;
     29 import android.view.inputmethod.InputMethodManager;
     30 import android.widget.LinearLayout;
     31 
     32 import com.android.mail.R;
     33 import com.android.mail.providers.Account;
     34 import com.android.mail.providers.Attachment;
     35 import com.android.mail.ui.AttachmentTile;
     36 import com.android.mail.ui.AttachmentTile.AttachmentPreview;
     37 import com.android.mail.ui.AttachmentTileGrid;
     38 import com.android.mail.utils.LogTag;
     39 import com.android.mail.utils.LogUtils;
     40 import com.google.common.annotations.VisibleForTesting;
     41 import com.google.common.collect.Lists;
     42 
     43 import java.io.FileNotFoundException;
     44 import java.io.IOException;
     45 import java.util.ArrayList;
     46 
     47 /*
     48  * View for displaying attachments in the compose screen.
     49  */
     50 class AttachmentsView extends LinearLayout {
     51     private static final String LOG_TAG = LogTag.getLogTag();
     52 
     53     private final ArrayList<Attachment> mAttachments;
     54     private AttachmentAddedOrDeletedListener mChangeListener;
     55     private AttachmentTileGrid mTileGrid;
     56     private LinearLayout mAttachmentLayout;
     57 
     58     public AttachmentsView(Context context) {
     59         this(context, null);
     60     }
     61 
     62     public AttachmentsView(Context context, AttributeSet attrs) {
     63         super(context, attrs);
     64         mAttachments = Lists.newArrayList();
     65     }
     66 
     67     @Override
     68     protected void onFinishInflate() {
     69         super.onFinishInflate();
     70 
     71         mTileGrid = (AttachmentTileGrid) findViewById(R.id.attachment_tile_grid);
     72         mAttachmentLayout = (LinearLayout) findViewById(R.id.attachment_bar_list);
     73     }
     74 
     75     public void expandView() {
     76         mTileGrid.setVisibility(VISIBLE);
     77         mAttachmentLayout.setVisibility(VISIBLE);
     78 
     79         InputMethodManager imm = (InputMethodManager) getContext().getSystemService(
     80                 Context.INPUT_METHOD_SERVICE);
     81         if (imm != null) {
     82             imm.hideSoftInputFromWindow(getWindowToken(), 0);
     83         }
     84     }
     85 
     86     /**
     87      * Set a listener for changes to the attachments.
     88      */
     89     public void setAttachmentChangesListener(AttachmentAddedOrDeletedListener listener) {
     90         mChangeListener = listener;
     91     }
     92 
     93     /**
     94      * Adds an attachment and updates the ui accordingly.
     95      */
     96     private void addAttachment(final Attachment attachment) {
     97         mAttachments.add(attachment);
     98 
     99         // If the attachment is inline do not display this attachment.
    100         if (attachment.isInlineAttachment()) {
    101             return;
    102         }
    103 
    104         if (!isShown()) {
    105             setVisibility(View.VISIBLE);
    106         }
    107 
    108         expandView();
    109 
    110         // If we have an attachment that should be shown in a tiled look,
    111         // set up the tile and add it to the tile grid.
    112         if (AttachmentTile.isTiledAttachment(attachment)) {
    113             final ComposeAttachmentTile attachmentTile =
    114                     mTileGrid.addComposeTileFromAttachment(attachment);
    115             attachmentTile.addDeleteListener(new OnClickListener() {
    116                 @Override
    117                 public void onClick(View v) {
    118                     deleteAttachment(attachmentTile, attachment);
    119                 }
    120             });
    121         // Otherwise, use the old bar look and add it to the new
    122         // inner LinearLayout.
    123         } else {
    124             final AttachmentComposeView attachmentView =
    125                 new AttachmentComposeView(getContext(), attachment);
    126 
    127             attachmentView.addDeleteListener(new OnClickListener() {
    128                 @Override
    129                 public void onClick(View v) {
    130                     deleteAttachment(attachmentView, attachment);
    131                 }
    132             });
    133 
    134 
    135             mAttachmentLayout.addView(attachmentView, new LinearLayout.LayoutParams(
    136                     LinearLayout.LayoutParams.MATCH_PARENT,
    137                     LinearLayout.LayoutParams.MATCH_PARENT));
    138         }
    139         if (mChangeListener != null) {
    140             mChangeListener.onAttachmentAdded();
    141         }
    142     }
    143 
    144     @VisibleForTesting
    145     protected void deleteAttachment(final View attachmentView,
    146             final Attachment attachment) {
    147         mAttachments.remove(attachment);
    148         ((ViewGroup) attachmentView.getParent()).removeView(attachmentView);
    149         if (mChangeListener != null) {
    150             mChangeListener.onAttachmentDeleted();
    151         }
    152     }
    153 
    154     /**
    155      * Get all attachments being managed by this view.
    156      * @return attachments.
    157      */
    158     public ArrayList<Attachment> getAttachments() {
    159         return mAttachments;
    160     }
    161 
    162     /**
    163      * Get all attachments previews that have been loaded
    164      * @return attachments previews.
    165      */
    166     public ArrayList<AttachmentPreview> getAttachmentPreviews() {
    167         return mTileGrid.getAttachmentPreviews();
    168     }
    169 
    170     /**
    171      * Call this on restore instance state so previews persist across configuration changes
    172      */
    173     public void setAttachmentPreviews(ArrayList<AttachmentPreview> previews) {
    174         mTileGrid.setAttachmentPreviews(previews);
    175     }
    176 
    177     /**
    178      * Delete all attachments being managed by this view.
    179      */
    180     public void deleteAllAttachments() {
    181         mAttachments.clear();
    182         mTileGrid.removeAllViews();
    183         mAttachmentLayout.removeAllViews();
    184         setVisibility(GONE);
    185     }
    186 
    187     /**
    188      * Get the total size of all attachments currently in this view.
    189      */
    190     private long getTotalAttachmentsSize() {
    191         long totalSize = 0;
    192         for (Attachment attachment : mAttachments) {
    193             totalSize += attachment.size;
    194         }
    195         return totalSize;
    196     }
    197 
    198     /**
    199      * Interface to implement to be notified about changes to the attachments
    200      * explicitly made by the user.
    201      */
    202     public interface AttachmentAddedOrDeletedListener {
    203         public void onAttachmentDeleted();
    204 
    205         public void onAttachmentAdded();
    206     }
    207 
    208     /**
    209      * Generate an {@link Attachment} object for a given local content URI. Attempts to populate
    210      * the {@link Attachment#name}, {@link Attachment#size}, and {@link Attachment#contentType}
    211      * fields using a {@link ContentResolver}.
    212      *
    213      * @param contentUri
    214      * @return an Attachment object
    215      * @throws AttachmentFailureException
    216      */
    217     public Attachment generateLocalAttachment(Uri contentUri) throws AttachmentFailureException {
    218         if (contentUri == null || TextUtils.isEmpty(contentUri.getPath())) {
    219             throw new AttachmentFailureException("Failed to create local attachment");
    220         }
    221 
    222         // FIXME: do not query resolver for type on the UI thread
    223         final ContentResolver contentResolver = getContext().getContentResolver();
    224         String contentType = contentResolver.getType(contentUri);
    225 
    226         if (contentType == null) contentType = "";
    227 
    228         final Attachment attachment = new Attachment();
    229         attachment.uri = null; // URI will be assigned by the provider upon send/save
    230         attachment.setName(null);
    231         attachment.size = 0;
    232         attachment.contentUri = contentUri;
    233         attachment.thumbnailUri = contentUri;
    234 
    235         Cursor metadataCursor = null;
    236         try {
    237             metadataCursor = contentResolver.query(
    238                     contentUri, new String[]{OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE},
    239                     null, null, null);
    240             if (metadataCursor != null) {
    241                 try {
    242                     if (metadataCursor.moveToNext()) {
    243                         attachment.setName(metadataCursor.getString(0));
    244                         attachment.size = metadataCursor.getInt(1);
    245                     }
    246                 } finally {
    247                     metadataCursor.close();
    248                 }
    249             }
    250         } catch (SQLiteException ex) {
    251             // One of the two columns is probably missing, let's make one more attempt to get at
    252             // least one.
    253             // Note that the documentations in Intent#ACTION_OPENABLE and
    254             // OpenableColumns seem to contradict each other about whether these columns are
    255             // required, but it doesn't hurt to fail properly.
    256 
    257             // Let's try to get DISPLAY_NAME
    258             try {
    259                 metadataCursor = getOptionalColumn(contentResolver, contentUri,
    260                         OpenableColumns.DISPLAY_NAME);
    261                 if (metadataCursor != null && metadataCursor.moveToNext()) {
    262                     attachment.setName(metadataCursor.getString(0));
    263                 }
    264             } finally {
    265                 if (metadataCursor != null) metadataCursor.close();
    266             }
    267 
    268             // Let's try to get SIZE
    269             try {
    270                 metadataCursor =
    271                         getOptionalColumn(contentResolver, contentUri, OpenableColumns.SIZE);
    272                 if (metadataCursor != null && metadataCursor.moveToNext()) {
    273                     attachment.size = metadataCursor.getInt(0);
    274                 } else {
    275                     // Unable to get the size from the metadata cursor. Open the file and seek.
    276                     attachment.size = getSizeFromFile(contentUri, contentResolver);
    277                 }
    278             } finally {
    279                 if (metadataCursor != null) metadataCursor.close();
    280             }
    281         } catch (SecurityException e) {
    282             throw new AttachmentFailureException("Security Exception from attachment uri", e);
    283         }
    284 
    285         if (attachment.getName() == null) {
    286             attachment.setName(contentUri.getLastPathSegment());
    287         }
    288         if (attachment.size == 0) {
    289             // if the attachment is not a content:// for example, a file:// URI
    290             attachment.size = getSizeFromFile(contentUri, contentResolver);
    291         }
    292 
    293         attachment.setContentType(contentType);
    294         return attachment;
    295     }
    296 
    297     /**
    298      * Adds an attachment of either local or remote origin, checking to see if the attachment
    299      * exceeds file size limits.
    300      * @param account
    301      * @param attachment the attachment to be added.
    302      *
    303      * @return size of the attachment added.
    304      * @throws AttachmentFailureException if an error occurs adding the attachment.
    305      */
    306     public long addAttachment(Account account, Attachment attachment)
    307             throws AttachmentFailureException {
    308         final int maxSize = account.settings.getMaxAttachmentSize();
    309 
    310         // Error getting the size or the size was too big.
    311         if (attachment.size == -1 || attachment.size > maxSize) {
    312             throw new AttachmentFailureException(
    313                     "Attachment too large to attach", R.string.too_large_to_attach_single);
    314         } else if ((getTotalAttachmentsSize()
    315                 + attachment.size) > maxSize) {
    316             throw new AttachmentFailureException(
    317                     "Attachment too large to attach", R.string.too_large_to_attach_additional);
    318         } else {
    319             addAttachment(attachment);
    320         }
    321 
    322         return attachment.size;
    323     }
    324 
    325     private static int getSizeFromFile(Uri uri, ContentResolver contentResolver) {
    326         int size = -1;
    327         ParcelFileDescriptor file = null;
    328         try {
    329             file = contentResolver.openFileDescriptor(uri, "r");
    330             size = (int) file.getStatSize();
    331         } catch (FileNotFoundException e) {
    332             LogUtils.w(LOG_TAG, e, "Error opening file to obtain size.");
    333         } finally {
    334             try {
    335                 if (file != null) {
    336                     file.close();
    337                 }
    338             } catch (IOException e) {
    339                 LogUtils.w(LOG_TAG, "Error closing file opened to obtain size.");
    340             }
    341         }
    342         // We only want to return a non-negative value. (ParcelFileDescriptor#getStatSize() will
    343         // return -1 if the fd is not a file
    344         return Math.max(size, 0);
    345     }
    346 
    347     /**
    348      * @return a cursor to the requested column or null if an exception occurs while trying
    349      * to query it.
    350      */
    351     private static Cursor getOptionalColumn(ContentResolver contentResolver, Uri uri,
    352             String columnName) {
    353         Cursor result = null;
    354         try {
    355             result = contentResolver.query(uri, new String[]{columnName}, null, null, null);
    356         } catch (SQLiteException ex) {
    357             // ignore, leave result null
    358         }
    359         return result;
    360     }
    361 
    362     public void focusLastAttachment() {
    363         Attachment lastAttachment = mAttachments.get(mAttachments.size() - 1);
    364         View lastView = null;
    365         int last = 0;
    366         if (AttachmentTile.isTiledAttachment(lastAttachment)) {
    367             last = mTileGrid.getChildCount() - 1;
    368             if (last > 0) {
    369                 lastView = mTileGrid.getChildAt(last);
    370             }
    371         } else {
    372             last = mAttachmentLayout.getChildCount() - 1;
    373             if (last > 0) {
    374                 lastView = mAttachmentLayout.getChildAt(last);
    375             }
    376         }
    377         if (lastView != null) {
    378             lastView.requestFocus();
    379         }
    380     }
    381 
    382     /**
    383      * Class containing information about failures when adding attachments.
    384      */
    385     static class AttachmentFailureException extends Exception {
    386         private static final long serialVersionUID = 1L;
    387         private final int errorRes;
    388 
    389         public AttachmentFailureException(String detailMessage) {
    390             super(detailMessage);
    391             this.errorRes = R.string.generic_attachment_problem;
    392         }
    393 
    394         public AttachmentFailureException(String error, int errorRes) {
    395             super(error);
    396             this.errorRes = errorRes;
    397         }
    398 
    399         public AttachmentFailureException(String detailMessage, Throwable throwable) {
    400             super(detailMessage, throwable);
    401             this.errorRes = R.string.generic_attachment_problem;
    402         }
    403 
    404         /**
    405          * Get the error string resource that corresponds to this attachment failure. Always a valid
    406          * string resource.
    407          */
    408         public int getErrorRes() {
    409             return errorRes;
    410         }
    411     }
    412 }
    413