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