AttachmentsView.java revision 237129ddf57a1e25469d8990322fb7913e18ae20
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 */
16package com.android.mail.compose;
17
18import android.content.ContentResolver;
19import android.content.Context;
20import android.database.Cursor;
21import android.database.sqlite.SQLiteException;
22import android.net.Uri;
23import android.os.ParcelFileDescriptor;
24import android.provider.OpenableColumns;
25import android.text.TextUtils;
26import android.util.AttributeSet;
27import android.view.Gravity;
28import android.view.View;
29import android.view.ViewGroup;
30import android.widget.LinearLayout;
31import android.widget.Toast;
32
33import com.android.mail.R;
34import com.android.mail.providers.Account;
35import com.android.mail.providers.Attachment;
36import com.android.mail.providers.Message;
37import com.android.mail.providers.UIProvider;
38import com.android.mail.ui.AttachmentTile;
39import com.android.mail.ui.AttachmentTileGrid;
40import com.android.mail.utils.LogUtils;
41import com.google.common.annotations.VisibleForTesting;
42import com.google.common.collect.Lists;
43
44import java.io.FileNotFoundException;
45import java.io.IOException;
46import java.util.ArrayList;
47
48/*
49 * View for displaying attachments in the compose screen.
50 */
51class AttachmentsView extends LinearLayout {
52    private static final String LOG_TAG = new LogUtils().getLogTag();
53    private ArrayList<Attachment> mAttachments;
54    private AttachmentDeletedListener 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    /**
76     * Set a listener for changes to the attachments.
77     * @param listener
78     */
79    public void setAttachmentChangesListener(AttachmentDeletedListener listener) {
80        mChangeListener = listener;
81    }
82
83    /**
84     * Add an attachment and update the ui accordingly.
85     * @param attachment
86     */
87    public void addAttachment(final Attachment attachment) {
88        if (!isShown()) {
89            setVisibility(View.VISIBLE);
90        }
91        mAttachments.add(attachment);
92
93        // If we have an attachment that should be shown in a tiled look,
94        // set up the tile and add it to the tile grid.
95        if (AttachmentTile.isTiledAttachment(attachment)) {
96            final ComposeAttachmentTile attachmentTile =
97                    mTileGrid.addComposeTileFromAttachment(attachment);
98            attachmentTile.addDeleteListener(new OnClickListener() {
99                @Override
100                public void onClick(View v) {
101                    deleteAttachment(attachmentTile, attachment);
102                }
103            });
104        // Otherwise, use the old bar look and add it to the new
105        // inner LinearLayout.
106        } else {
107            final AttachmentComposeView attachmentView =
108                new AttachmentComposeView(getContext(), attachment);
109
110            attachmentView.addDeleteListener(new OnClickListener() {
111                @Override
112                public void onClick(View v) {
113                    deleteAttachment(attachmentView, attachment);
114                }
115            });
116
117
118            mAttachmentLayout.addView(attachmentView, new LinearLayout.LayoutParams(
119                    LinearLayout.LayoutParams.MATCH_PARENT,
120                    LinearLayout.LayoutParams.MATCH_PARENT));
121        }
122    }
123
124    @VisibleForTesting
125    protected void deleteAttachment(final View attachmentView,
126            final Attachment attachment) {
127        mAttachments.remove(attachment);
128        ((ViewGroup) attachmentView.getParent()).removeView(attachmentView);
129        if (mChangeListener != null) {
130            mChangeListener.onAttachmentDeleted();
131        }
132        if (mAttachments.size() == 0) {
133            setVisibility(View.GONE);
134        }
135    }
136
137    /**
138     * Get all attachments being managed by this view.
139     * @return attachments.
140     */
141    public ArrayList<Attachment> getAttachments() {
142        return mAttachments;
143    }
144
145    /**
146     * Delete all attachments being managed by this view.
147     */
148    public void deleteAllAttachments() {
149        mAttachments.clear();
150        mTileGrid.removeAllViews();
151        mAttachmentLayout.removeAllViews();
152    }
153
154    /**
155     * Get the total size of all attachments currently in this view.
156     */
157    public long getTotalAttachmentsSize() {
158        long totalSize = 0;
159        for (Attachment attachment : mAttachments) {
160            totalSize += attachment.size;
161        }
162        return totalSize;
163    }
164
165    /**
166     * Interface to implement to be notified about changes to the attachments.
167     *
168     */
169    public interface AttachmentDeletedListener {
170        public void onAttachmentDeleted();
171    }
172
173    /**
174     * When an attachment is too large to be added to a message, show a toast.
175     * This method also updates the position of the toast so that it is shown
176     * clearly above they keyboard if it happens to be open.
177     */
178    private void showAttachmentTooBigToast() {
179        showErrorToast(R.string.too_large_to_attach);
180    }
181
182    private void showGenericAttachmentError() {
183        showErrorToast(R.string.generic_attachment_problem);
184    }
185
186    private void showErrorToast(int resId) {
187        Toast t = Toast.makeText(getContext(), resId,
188                Toast.LENGTH_LONG);
189        t.setText(resId);
190        t.setGravity(Gravity.CENTER_HORIZONTAL, 0,
191                getResources().getDimensionPixelSize(R.dimen.attachment_toast_yoffset));
192        t.show();
193    }
194
195    /**
196     * Generate an {@link Attachment} object for a given local content URI. Attempts to populate
197     * the {@link Attachment#name}, {@link Attachment#size}, and {@link Attachment#contentType}
198     * fields using a {@link ContentResolver}.
199     *
200     * @param contentUri
201     * @return an Attachment object
202     * @throws AttachmentFailureException
203     */
204    public Attachment generateLocalAttachment(Uri contentUri) throws AttachmentFailureException {
205        // FIXME: do not query resolver for type on the UI thread
206        final ContentResolver contentResolver = getContext().getContentResolver();
207        String contentType = contentResolver.getType(contentUri);
208        if (contentUri == null || TextUtils.isEmpty(contentUri.getPath())) {
209            showGenericAttachmentError();
210            throw new AttachmentFailureException("Attachment too large to attach");
211        }
212
213        if (contentType == null) contentType = "";
214
215        final Attachment attachment = new Attachment();
216        attachment.uri = null; // URI will be assigned by the provider upon send/save
217        attachment.name = null;
218        attachment.contentType = contentType;
219        attachment.size = 0;
220        attachment.contentUri = contentUri;
221
222        Cursor metadataCursor = null;
223        try {
224            metadataCursor = contentResolver.query(
225                    contentUri, new String[]{OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE},
226                    null, null, null);
227            if (metadataCursor != null) {
228                try {
229                    if (metadataCursor.moveToNext()) {
230                        attachment.name = metadataCursor.getString(0);
231                        attachment.size = metadataCursor.getInt(1);
232                    }
233                } finally {
234                    metadataCursor.close();
235                }
236            }
237        } catch (SQLiteException ex) {
238            // One of the two columns is probably missing, let's make one more attempt to get at
239            // least one.
240            // Note that the documentations in Intent#ACTION_OPENABLE and
241            // OpenableColumns seem to contradict each other about whether these columns are
242            // required, but it doesn't hurt to fail properly.
243
244            // Let's try to get DISPLAY_NAME
245            try {
246                metadataCursor = getOptionalColumn(contentResolver, contentUri,
247                        OpenableColumns.DISPLAY_NAME);
248                if (metadataCursor != null && metadataCursor.moveToNext()) {
249                    attachment.name = metadataCursor.getString(0);
250                }
251            } finally {
252                if (metadataCursor != null) metadataCursor.close();
253            }
254
255            // Let's try to get SIZE
256            try {
257                metadataCursor =
258                        getOptionalColumn(contentResolver, contentUri, OpenableColumns.SIZE);
259                if (metadataCursor != null && metadataCursor.moveToNext()) {
260                    attachment.size = metadataCursor.getInt(0);
261                } else {
262                    // Unable to get the size from the metadata cursor. Open the file and seek.
263                    attachment.size = getSizeFromFile(contentUri, contentResolver);
264                }
265            } finally {
266                if (metadataCursor != null) metadataCursor.close();
267            }
268        } catch (SecurityException e) {
269            // We received a security exception when attempting to add an
270            // attachment.  Warn the user.
271            // TODO(pwestbro): determine if we need more specific text in the toast.
272            Toast.makeText(getContext(),
273                    R.string.generic_attachment_problem, Toast.LENGTH_LONG).show();
274            throw new AttachmentFailureException("Security Exception from attachment uri", e);
275        }
276
277        if (attachment.name == null) {
278            attachment.name = contentUri.getLastPathSegment();
279        }
280
281        return attachment;
282    }
283
284    /**
285     * Adds a local attachment by file path.
286     * @param account
287     * @param contentUri the uri of the local file path
288     *
289     * @return size of the attachment added.
290     * @throws AttachmentFailureException if an error occurs adding the attachment.
291     */
292    public long addAttachment(Account account, Uri contentUri)
293            throws AttachmentFailureException {
294        return addAttachment(account, generateLocalAttachment(contentUri));
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        int maxSize = UIProvider.getMailMaxAttachmentSize(account.name);
309
310        // Error getting the size or the size was too big.
311        // FIXME: exceptions should not be used to direct control flow
312        if (attachment.size == -1 || attachment.size > maxSize) {
313            showAttachmentTooBigToast();
314            throw new AttachmentFailureException("Attachment too large to attach");
315        } else if ((getTotalAttachmentsSize()
316                + attachment.size) > maxSize) {
317            showAttachmentTooBigToast();
318            throw new AttachmentFailureException("Attachment too large to attach");
319        } else {
320            addAttachment(attachment);
321        }
322
323        return attachment.size;
324    }
325
326
327    public void addAttachments(Account account, Message refMessage) {
328        if (refMessage.hasAttachments) {
329            try {
330                for (Attachment a : refMessage.getAttachments()) {
331                    addAttachment(account, a);
332                }
333            } catch (AttachmentFailureException e) {
334                // A toast has already been shown to the user, no need to do
335                // anything.
336                LogUtils.e(LOG_TAG, e, "Error adding attachment");
337            }
338        }
339    }
340
341    @VisibleForTesting
342    protected int getSizeFromFile(Uri uri, ContentResolver contentResolver) {
343        int size = -1;
344        ParcelFileDescriptor file = null;
345        try {
346            file = contentResolver.openFileDescriptor(uri, "r");
347            size = (int) file.getStatSize();
348        } catch (FileNotFoundException e) {
349            LogUtils.w(LOG_TAG, "Error opening file to obtain size.");
350        } finally {
351            try {
352                if (file != null) {
353                    file.close();
354                }
355            } catch (IOException e) {
356                LogUtils.w(LOG_TAG, "Error closing file opened to obtain size.");
357            }
358        }
359        return size;
360    }
361
362    /**
363     * @return a cursor to the requested column or null if an exception occurs while trying
364     * to query it.
365     */
366    private Cursor getOptionalColumn(ContentResolver contentResolver, Uri uri, String columnName) {
367        Cursor result = null;
368        try {
369            result = contentResolver.query(uri, new String[]{columnName}, null, null, null);
370        } catch (SQLiteException ex) {
371            // ignore, leave result null
372        }
373        return result;
374    }
375
376    /**
377     * Class containing information about failures when adding attachments.
378     */
379    static class AttachmentFailureException extends Exception {
380        private static final long serialVersionUID = 1L;
381
382        public AttachmentFailureException(String error) {
383            super(error);
384        }
385        public AttachmentFailureException(String detailMessage, Throwable throwable) {
386            super(detailMessage, throwable);
387        }
388    }
389}
390