AttachmentsView.java revision 1ddcf0f2bf44d3c9db89112ef52510d9b2433ac4
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 com.android.mail.R;
19import com.android.mail.providers.Account;
20import com.android.mail.providers.Attachment;
21import com.android.mail.providers.Message;
22import com.android.mail.providers.UIProvider;
23import com.android.mail.utils.LogUtils;
24import com.google.common.annotations.VisibleForTesting;
25import com.google.common.collect.Lists;
26
27import android.content.ContentResolver;
28import android.content.Context;
29import android.database.Cursor;
30import android.database.sqlite.SQLiteException;
31import android.net.Uri;
32import android.os.ParcelFileDescriptor;
33import android.provider.OpenableColumns;
34import android.text.TextUtils;
35import android.util.AttributeSet;
36import android.view.Gravity;
37import android.view.View;
38import android.widget.LinearLayout;
39import android.widget.Toast;
40
41import java.io.FileNotFoundException;
42import java.io.IOException;
43import java.util.ArrayList;
44
45/*
46 * View for displaying attachments in the compose screen.
47 */
48class AttachmentsView extends LinearLayout {
49    private static final String LOG_TAG = new LogUtils().getLogTag();
50    private ArrayList<Attachment> mAttachments;
51    private AttachmentDeletedListener mChangeListener;
52
53    public AttachmentsView(Context context) {
54        this(context, null);
55    }
56
57    public AttachmentsView(Context context, AttributeSet attrs) {
58        super(context, attrs);
59        mAttachments = Lists.newArrayList();
60    }
61
62    /**
63     * Set a listener for changes to the attachments.
64     * @param listener
65     */
66    public void setAttachmentChangesListener(AttachmentDeletedListener listener) {
67        mChangeListener = listener;
68    }
69
70    /**
71     * Add an attachment and update the ui accordingly.
72     * @param attachment
73     */
74    public void addAttachment(final Attachment attachment) {
75        if (!isShown()) {
76            setVisibility(View.VISIBLE);
77        }
78        mAttachments.add(attachment);
79
80        final AttachmentComposeView attachmentView =
81            new AttachmentComposeView(getContext(), attachment);
82
83        attachmentView.addDeleteListener(new OnClickListener() {
84            @Override
85            public void onClick(View v) {
86                deleteAttachment(attachmentView, attachment);
87            }
88        });
89
90
91        addView(attachmentView, new LinearLayout.LayoutParams(
92                LinearLayout.LayoutParams.MATCH_PARENT,
93                LinearLayout.LayoutParams.MATCH_PARENT));
94    }
95
96    @VisibleForTesting
97    protected void deleteAttachment(final AttachmentComposeView attachmentView,
98            final Attachment attachment) {
99        mAttachments.remove(attachment);
100        removeView(attachmentView);
101        if (mChangeListener != null) {
102            mChangeListener.onAttachmentDeleted();
103        }
104        if (mAttachments.size() == 0) {
105            setVisibility(View.GONE);
106        }
107    }
108
109    /**
110     * Get all attachments being managed by this view.
111     * @return attachments.
112     */
113    public ArrayList<Attachment> getAttachments() {
114        return mAttachments;
115    }
116
117    /**
118     * Delete all attachments being managed by this view.
119     */
120    public void deleteAllAttachments() {
121        mAttachments.clear();
122        removeAllViews();
123    }
124
125    /**
126     * See if all the attachments in this view are synced.
127     */
128    public boolean areAttachmentsSynced() {
129        for (Attachment a : mAttachments) {
130            if (a.isSynced) {
131                return true;
132            }
133        }
134        return false;
135    }
136
137    /**
138     * Get the total size of all attachments currently in this view.
139     */
140    public long getTotalAttachmentsSize() {
141        long totalSize = 0;
142        for (Attachment attachment : mAttachments) {
143            totalSize += attachment.size;
144        }
145        return totalSize;
146    }
147
148    /**
149     * Interface to implement to be notified about changes to the attachments.
150     * @author mindyp@google.com
151     *
152     */
153    public interface AttachmentDeletedListener {
154        public void onAttachmentDeleted();
155    }
156
157    /**
158     * When an attachment is too large to be added to a message, show a toast.
159     * This method also updates the position of the toast so that it is shown
160     * clearly above they keyboard if it happens to be open.
161     */
162    private void showAttachmentTooBigToast() {
163        Toast t = Toast.makeText(getContext(), R.string.generic_attachment_problem,
164                Toast.LENGTH_LONG);
165        t.setText(R.string.too_large_to_attach);
166        t.setGravity(Gravity.CENTER_HORIZONTAL, 0,
167                getResources().getDimensionPixelSize(R.dimen.attachment_toast_yoffset));
168        t.show();
169    }
170
171    /**
172     * Adds an attachment
173     * @param uri the uri to attach
174     * @param contentType the type of the resource pointed to by the URI or null if the type is
175     *   unknown
176     * @param doSave whether the message should be saved
177     *
178     * @return int size of the attachment added.
179     * @throws AttachmentFailureException if an error occurs adding the attachment.
180     */
181    public long addAttachment(Account account, Uri uri, boolean doSave, boolean isLocal)
182            throws AttachmentFailureException {
183        final ContentResolver contentResolver = getContext().getContentResolver();
184        String contentType = contentResolver.getType(uri);
185        if (uri == null || TextUtils.isEmpty(uri.getPath())) {
186            showAttachmentTooBigToast();
187            return -1;
188        }
189
190        if (contentType == null) contentType = "";
191
192        Attachment attachment = new Attachment();
193        // partId will be assigned by the engine.
194        attachment.name = null;
195        attachment.mimeType = contentType;
196        attachment.size = 0;
197        attachment.contentUri = uri.toString();
198        attachment.origin = isLocal ? Attachment.LOCAL_FILE : Attachment.SERVER_ATTACHMENT;
199        attachment.originExtras = uri.toString();
200
201        Cursor metadataCursor = null;
202        try {
203            metadataCursor = contentResolver.query(
204                    uri, new String[]{OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE},
205                    null, null, null);
206            if (metadataCursor != null) {
207                try {
208                    if (metadataCursor.moveToNext()) {
209                        attachment.name = metadataCursor.getString(0);
210                        attachment.size = metadataCursor.getInt(1);
211                    }
212                } finally {
213                    metadataCursor.close();
214                }
215            }
216        } catch (SQLiteException ex) {
217            // One of the two columns is probably missing, let's make one more attempt to get at
218            // least one.
219            // Note that the documentations in Intent#ACTION_OPENABLE and
220            // OpenableColumns seem to contradict each other about whether these columns are
221            // required, but it doesn't hurt to fail properly.
222
223            // Let's try to get DISPLAY_NAME
224            try {
225                metadataCursor =
226                        getOptionalColumn(contentResolver, uri, OpenableColumns.DISPLAY_NAME);
227                if (metadataCursor != null && metadataCursor.moveToNext()) {
228                    attachment.name = metadataCursor.getString(0);
229                }
230            } finally {
231                if (metadataCursor != null) metadataCursor.close();
232            }
233
234            // Let's try to get SIZE
235            try {
236                metadataCursor =
237                        getOptionalColumn(contentResolver, uri, OpenableColumns.SIZE);
238                if (metadataCursor != null && metadataCursor.moveToNext()) {
239                    attachment.size = metadataCursor.getInt(0);
240                } else {
241                    // Unable to get the size from the metadata cursor. Open the file and seek.
242                    attachment.size = getSizeFromFile(uri, contentResolver);
243                }
244            } finally {
245                if (metadataCursor != null) metadataCursor.close();
246            }
247        } catch (SecurityException e) {
248            // We received a security exception when attempting to add an
249            // attachment.  Warn the user.
250            // TODO(pwestbro): determine if we need more specific text in the toast.
251            Toast.makeText(getContext(),
252                    R.string.generic_attachment_problem, Toast.LENGTH_LONG).show();
253            throw new AttachmentFailureException("Security Exception from attachment uri", e);
254        }
255
256        if (attachment.name == null) {
257            attachment.name = uri.getLastPathSegment();
258        }
259
260        int maxSize = UIProvider.getMailMaxAttachmentSize(account.name);
261
262        // Error getting the size or the size was too big.
263        if (attachment.size == -1 || attachment.size > maxSize) {
264            showAttachmentTooBigToast();
265            throw new AttachmentFailureException("Attachment too large to attach");
266        } else if ((getTotalAttachmentsSize()
267                + attachment.size) > maxSize) {
268            showAttachmentTooBigToast();
269            throw new AttachmentFailureException("Attachment too large to attach");
270        } else {
271            addAttachment(attachment);
272        }
273
274        return attachment.size;
275    }
276
277
278    public void addAttachments(Account account, Message refMessage) {
279        boolean hasAttachments = refMessage.hasAttachments;
280        if (hasAttachments) {
281            String attachmentQuery = refMessage.attachmentListUri;
282            Cursor attachmentCursor = null;
283            try {
284                attachmentCursor = getContext().getContentResolver().query(
285                        Uri.parse(attachmentQuery), UIProvider.ATTACHMENT_PROJECTION, null, null,
286                        null);
287                String attachmentUri;
288                while (attachmentCursor.moveToNext()) {
289                    attachmentUri = attachmentCursor.getString(UIProvider.ATTACHMENT_URI_COLUMN);
290                    if (!TextUtils.isEmpty(attachmentUri)) {
291                        addAttachment(account, Uri.parse(attachmentUri), false, false);
292                    }
293                }
294            } catch (AttachmentFailureException e) {
295                // A toast has already been shown to the user, no need to do
296                // anything.
297                LogUtils.e(LOG_TAG, e, "Error adding attachment");
298            } finally {
299                if (attachmentCursor != null) {
300                    attachmentCursor.close();
301                }
302            }
303        }
304    }
305
306    @VisibleForTesting
307    protected int getSizeFromFile(Uri uri, ContentResolver contentResolver) {
308        int size = -1;
309        ParcelFileDescriptor file = null;
310        try {
311            file = contentResolver.openFileDescriptor(uri, "r");
312            size = (int) file.getStatSize();
313        } catch (FileNotFoundException e) {
314            LogUtils.w(LOG_TAG, "Error opening file to obtain size.");
315        } finally {
316            try {
317                if (file != null) {
318                    file.close();
319                }
320            } catch (IOException e) {
321                LogUtils.w(LOG_TAG, "Error closing file opened to obtain size.");
322            }
323        }
324        return size;
325    }
326
327    /**
328     * @return a cursor to the requested column or null if an exception occurs while trying
329     * to query it.
330     */
331    private Cursor getOptionalColumn(ContentResolver contentResolver, Uri uri, String columnName) {
332        Cursor result = null;
333        try {
334            result = contentResolver.query(uri, new String[]{columnName}, null, null, null);
335        } catch (SQLiteException ex) {
336            // ignore, leave result null
337        }
338        return result;
339    }
340
341    /**
342     * Class containing information about failures when adding attachments.
343     */
344    static class AttachmentFailureException extends Exception {
345        private static final long serialVersionUID = 1L;
346
347        public AttachmentFailureException(String error) {
348            super(error);
349        }
350        public AttachmentFailureException(String detailMessage, Throwable throwable) {
351            super(detailMessage, throwable);
352        }
353    }
354}
355