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