MessageAttachmentTile.java revision 15b43f21f67e425bdfc2c401b3ac4367075c2dd1
1/*
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.mail.browse;
19
20import android.app.AlertDialog;
21import android.app.ProgressDialog;
22import android.content.ActivityNotFoundException;
23import android.content.AsyncQueryHandler;
24import android.content.ContentValues;
25import android.content.Context;
26import android.content.DialogInterface;
27import android.content.Intent;
28import android.content.res.AssetFileDescriptor;
29import android.graphics.Bitmap;
30import android.graphics.BitmapFactory;
31import android.net.Uri;
32import android.os.AsyncTask;
33import android.text.TextUtils;
34import android.util.AttributeSet;
35import android.view.LayoutInflater;
36import android.view.MenuItem;
37import android.view.View;
38import android.view.View.OnClickListener;
39import android.view.ViewGroup;
40import android.widget.Button;
41import android.widget.ImageView;
42import android.widget.LinearLayout;
43import android.widget.PopupMenu.OnMenuItemClickListener;
44import android.widget.ProgressBar;
45import android.widget.TextView;
46
47import com.android.mail.R;
48import com.android.mail.photo.Intents;
49import com.android.mail.photo.Intents.PhotoViewIntentBuilder;
50import com.android.mail.photo.util.MediaStoreUtils;
51import com.android.mail.providers.Attachment;
52import com.android.mail.providers.UIProvider.AttachmentColumns;
53import com.android.mail.providers.UIProvider.AttachmentDestination;
54import com.android.mail.providers.UIProvider.AttachmentState;
55import com.android.mail.utils.AttachmentUtils;
56import com.android.mail.utils.LogUtils;
57import com.android.mail.utils.MimeType;
58import com.android.mail.utils.Utils;
59
60import java.io.IOException;
61
62/**
63 * View for a single attachment in conversation view. Shows download status and allows launching
64 * intents to act on an attachment.
65 *
66 */
67public class MessageAttachmentTile extends LinearLayout implements OnClickListener,
68        OnMenuItemClickListener, DialogInterface.OnCancelListener,
69        DialogInterface.OnDismissListener {
70
71    private Attachment mAttachment;
72    private ImageView mIcon;
73    private ImageView.ScaleType mIconScaleType;
74    private int mPhotoIndex;
75    private TextView mTitle;
76    private TextView mSubTitle;
77    private String mAttachmentSizeText;
78    private String mDisplayType;
79    private Uri mAttachmentsListUri;
80    private ProgressDialog mViewProgressDialog;
81    private AttachmentCommandHandler mCommandHandler;
82    private ProgressBar mProgress;
83    private Button mPreviewButton;
84    private Button mViewButton;
85    private Button mSaveButton;
86    private Button mInfoButton;
87    private Button mPlayButton;
88    private Button mInstallButton;
89    private Button mCancelButton;
90
91    private ThumbnailLoadTask mThumbnailTask;
92
93    private static final String LOG_TAG = new LogUtils().getLogTag();
94
95    private class AttachmentCommandHandler extends AsyncQueryHandler {
96
97        public AttachmentCommandHandler() {
98            super(getContext().getContentResolver());
99        }
100
101        /**
102         * Asynchronously begin an update() on a ContentProvider.
103         *
104         */
105        public void sendCommand(ContentValues params) {
106            startUpdate(0, null, mAttachment.uri, params, null, null);
107        }
108
109    }
110
111    private class ThumbnailLoadTask extends AsyncTask<Uri, Void, Bitmap> {
112
113        private final int mWidth;
114        private final int mHeight;
115
116        public ThumbnailLoadTask(int width, int height) {
117            mWidth = width;
118            mHeight = height;
119        }
120
121        @Override
122        protected Bitmap doInBackground(Uri... params) {
123            final Uri thumbnailUri = params[0];
124
125            AssetFileDescriptor fd = null;
126            Bitmap result = null;
127
128            try {
129                fd = getContext().getContentResolver().openAssetFileDescriptor(thumbnailUri, "r");
130                if (isCancelled() || fd == null) {
131                    return null;
132                }
133
134                final BitmapFactory.Options opts = new BitmapFactory.Options();
135                opts.inJustDecodeBounds = true;
136
137                BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, opts);
138                if (isCancelled() || opts.outWidth == -1 || opts.outHeight == -1) {
139                    return null;
140                }
141
142                opts.inJustDecodeBounds = false;
143
144                LogUtils.d(LOG_TAG, "in background, src w/h=%d/%d dst w/h=%d/%d, divider=%d",
145                        opts.outWidth, opts.outHeight, mWidth, mHeight, opts.inSampleSize);
146
147                result = BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, opts);
148
149            } catch (Throwable t) {
150                LogUtils.e(LOG_TAG, t, "Unable to decode thumbnail %s", thumbnailUri);
151            } finally {
152                if (fd != null) {
153                    try {
154                        fd.close();
155                    } catch (IOException e) {
156                        LogUtils.e(LOG_TAG, e, "");
157                    }
158                }
159            }
160
161            return result;
162        }
163
164        @Override
165        protected void onPostExecute(Bitmap result) {
166            if (result == null) {
167                LogUtils.d(LOG_TAG, "back in UI thread, decode failed");
168                setThumbnailToDefault();
169                return;
170            }
171
172            LogUtils.d(LOG_TAG, "back in UI thread, decode success, w/h=%d/%d", result.getWidth(),
173                    result.getHeight());
174            mIcon.setImageBitmap(result);
175            mIcon.setScaleType(mIconScaleType);
176        }
177
178    }
179
180    public MessageAttachmentTile(Context context) {
181        super(context);
182    }
183
184    public MessageAttachmentTile(Context context, AttributeSet attrs) {
185        super(context, attrs);
186
187        mCommandHandler = new AttachmentCommandHandler();
188    }
189
190    public static MessageAttachmentTile inflate(LayoutInflater inflater, ViewGroup parent) {
191        MessageAttachmentTile view = (MessageAttachmentTile) inflater.inflate(
192                R.layout.conversation_message_attachment, parent, false);
193        return view;
194    }
195
196    /**
197     * Render or update an attachment's view. This happens immediately upon instantiation, and
198     * repeatedly as status updates stream in, so only properties with new or changed values will
199     * cause sub-views to update.
200     *
201     */
202    public void render(Attachment attachment, Uri attachmentsListUri, int index) {
203        if (attachment == null) {
204            setVisibility(View.INVISIBLE);
205            return;
206        }
207
208        final Attachment prevAttachment = mAttachment;
209        mAttachment = attachment;
210        mAttachmentsListUri = attachmentsListUri;
211        mPhotoIndex = index;
212
213        LogUtils.d(LOG_TAG, "got attachment list row: name=%s state/dest=%d/%d dled=%d" +
214                " contentUri=%s MIME=%s", attachment.name, attachment.state,
215                attachment.destination, attachment.downloadedSize, attachment.contentUri,
216                attachment.contentType);
217
218        if (prevAttachment == null || TextUtils.equals(attachment.name, prevAttachment.name)) {
219            mTitle.setText(attachment.name);
220        }
221
222        if (prevAttachment == null || attachment.size != prevAttachment.size) {
223            mAttachmentSizeText = AttachmentUtils.convertToHumanReadableSize(getContext(),
224                    attachment.size);
225            mDisplayType = AttachmentUtils.getDisplayType(getContext(), attachment);
226            updateSubtitleText(null);
227        }
228
229        final Uri imageUri = attachment.getImageUri();
230        final Uri prevImageUri = (prevAttachment == null) ? null : prevAttachment.getImageUri();
231        // begin loading a thumbnail if this is an image and either the thumbnail or the original
232        // content is ready (and different from any existing image)
233        if (imageUri != null && (prevImageUri == null || !imageUri.equals(prevImageUri))) {
234            // cancel/dispose any existing task and start a new one
235            if (mThumbnailTask != null) {
236                mThumbnailTask.cancel(true);
237            }
238            mThumbnailTask = new ThumbnailLoadTask(mIcon.getWidth(), mIcon.getHeight());
239            mThumbnailTask.execute(imageUri);
240        } else if (imageUri == null) {
241            // not an image, or no thumbnail exists. fall back to default.
242            // async image load must separately ensure the default appears upon load failure.
243            setThumbnailToDefault();
244        }
245
246        if (mProgress != null) {
247            mProgress.setMax(attachment.size);
248        }
249
250        updateActions();
251        updateStatus();
252    }
253
254    private void setThumbnailToDefault() {
255        mIcon.setImageResource(R.drawable.ic_menu_attachment_holo_light);
256        mIcon.setScaleType(ImageView.ScaleType.CENTER);
257    }
258
259    /**
260     * Update progress-related views. Will also trigger a view intent if a progress dialog was
261     * previously brought up (by tapping 'View') and the download has now finished.
262     */
263    private void updateStatus() {
264        final boolean showProgress = mAttachment.size > 0 && mAttachment.downloadedSize > 0
265                && mAttachment.downloadedSize < mAttachment.size;
266
267        if (mViewProgressDialog != null && mViewProgressDialog.isShowing()) {
268            mViewProgressDialog.setProgress(mAttachment.downloadedSize);
269            mViewProgressDialog.setIndeterminate(!showProgress);
270
271            if (!mAttachment.isDownloading()) {
272                mViewProgressDialog.dismiss();
273            }
274
275            if (mAttachment.state == AttachmentState.SAVED) {
276                sendViewIntent();
277            }
278        } else {
279
280            if (mAttachment.isDownloading()) {
281                mProgress.setProgress(mAttachment.downloadedSize);
282                setProgressVisible(true);
283                mProgress.setIndeterminate(!showProgress);
284            } else {
285                setProgressVisible(false);
286            }
287
288        }
289
290        if (mAttachment.state == AttachmentState.FAILED) {
291            mSubTitle.setText(getResources().getString(R.string.download_failed));
292        } else {
293            updateSubtitleText(mAttachment.isSavedToExternal() ?
294                    getResources().getString(R.string.saved) : null);
295        }
296    }
297
298    private void setProgressVisible(boolean visible) {
299        if (visible) {
300            mProgress.setVisibility(VISIBLE);
301            mSubTitle.setVisibility(INVISIBLE);
302        } else {
303            mProgress.setVisibility(GONE);
304            mSubTitle.setVisibility(VISIBLE);
305        }
306    }
307
308    private void updateSubtitleText(String prefix) {
309        // TODO: make this a formatted resource when we have a UX design.
310        // not worth translation right now.
311        StringBuilder sb = new StringBuilder();
312        if (prefix != null) {
313            sb.append(prefix);
314        }
315        sb.append(mAttachmentSizeText);
316        sb.append(' ');
317        sb.append(mDisplayType);
318        mSubTitle.setText(sb.toString());
319    }
320
321    @Override
322    protected void onFinishInflate() {
323        super.onFinishInflate();
324
325        mIcon = (ImageView) findViewById(R.id.attachment_tile_image);
326        mTitle = (TextView) findViewById(R.id.attachment_tile_title);
327        mSubTitle = (TextView) findViewById(R.id.attachment_tile_subtitle);
328        mProgress = (ProgressBar) findViewById(R.id.attachment_progress);
329
330//        mPreviewButton = (Button) findViewById(R.id.preview_attachment);
331//        mViewButton = (Button) findViewById(R.id.view_attachment);
332        mSaveButton = (Button) findViewById(R.id.attachment_tile_secondary_button);
333//        mInfoButton = (Button) findViewById(R.id.info_attachment);
334//        mPlayButton = (Button) findViewById(R.id.play_attachment);
335//        mInstallButton = (Button) findViewById(R.id.install_attachment);
336//        mCancelButton = (Button) findViewById(R.id.cancel_attachment);
337
338        setOnClickListener(this);
339//        mPreviewButton.setOnClickListener(this);
340//        mViewButton.setOnClickListener(this);
341        mSaveButton.setOnClickListener(this);
342//        mInfoButton.setOnClickListener(this);
343//        mPlayButton.setOnClickListener(this);
344//        mInstallButton.setOnClickListener(this);
345//        mCancelButton.setOnClickListener(this);
346
347        mIconScaleType = mIcon.getScaleType();
348    }
349
350    @Override
351    public void onClick(View v) {
352        onClick(v.getId(), v);
353    }
354
355    @Override
356    public boolean onMenuItemClick(MenuItem item) {
357        return onClick(item.getItemId(), null);
358    }
359
360    private boolean onClick(int res, View v) {
361        switch (res) {
362            case R.id.preview_attachment:
363                getContext().startActivity(mAttachment.previewIntent);
364                break;
365            case R.id.view_attachment:
366            case R.id.play_attachment:
367            case R.id.attachment_tile:
368                showAttachment(AttachmentDestination.CACHE);
369                break;
370            case R.id.save_attachment:
371            case R.id.attachment_tile_secondary_button:
372                if (mAttachment.canSave()) {
373                    startDownloadingAttachment(AttachmentDestination.EXTERNAL);
374                }
375                break;
376            case R.id.info_attachment:
377                AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
378                int dialogMessage = MimeType.isBlocked(mAttachment.contentType)
379                        ? R.string.attachment_type_blocked : R.string.no_application_found;
380                builder.setTitle(R.string.more_info_attachment).setMessage(dialogMessage).show();
381                break;
382            case R.id.install_attachment:
383                showAttachment(AttachmentDestination.EXTERNAL);
384                break;
385            case R.id.cancel_attachment:
386                cancelAttachment();
387                break;
388            default:
389                break;
390        }
391        return true;
392    }
393
394    private void showAttachment(int destination) {
395        if (mAttachment.isPresentLocally()) {
396            sendViewIntent();
397        } else {
398            showDownloadingDialog();
399            startDownloadingAttachment(destination);
400        }
401    }
402
403    private void startDownloadingAttachment(int destination) {
404        final ContentValues params = new ContentValues(2);
405        params.put(AttachmentColumns.STATE, AttachmentState.DOWNLOADING);
406        params.put(AttachmentColumns.DESTINATION, destination);
407
408        mCommandHandler.sendCommand(params);
409    }
410
411    private void cancelAttachment() {
412        final ContentValues params = new ContentValues(1);
413        params.put(AttachmentColumns.STATE, AttachmentState.NOT_SAVED);
414
415        mCommandHandler.sendCommand(params);
416    }
417
418    private void setButtonVisible(View button, boolean visible) {
419        if (button != null) {
420            button.setVisibility(visible ? VISIBLE : GONE);
421        }
422    }
423
424    /**
425     * Update all action buttons based on current downloading state.
426     */
427    private void updateActions() {
428        // To avoid visibility state transition bugs, every button's visibility should be touched
429        // once by this routine.
430
431        final boolean isDownloading = mAttachment.isDownloading();
432
433        setButtonVisible(mCancelButton, isDownloading);
434
435        final boolean canInstall = MimeType.isInstallable(mAttachment.contentType);
436        setButtonVisible(mInstallButton, canInstall && !isDownloading);
437
438        if (!canInstall) {
439
440            final boolean canPreview = (mAttachment.previewIntent != null);
441            final boolean canView = MimeType.isViewable(getContext(), mAttachment.contentType);
442            final boolean canPlay = MimeType.isPlayable(mAttachment.contentType);
443
444            setButtonVisible(mPreviewButton, canPreview);
445            setButtonVisible(mPlayButton, canView && canPlay && !isDownloading);
446            setButtonVisible(mViewButton, canView && !canPlay && !isDownloading);
447            setButtonVisible(mSaveButton, canView && mAttachment.canSave() && !isDownloading);
448            setButtonVisible(mInfoButton, !(canPreview || canView));
449
450        } else {
451
452            setButtonVisible(mPreviewButton, false);
453            setButtonVisible(mPlayButton, false);
454            setButtonVisible(mViewButton, false);
455            setButtonVisible(mSaveButton, false);
456            setButtonVisible(mInfoButton, false);
457
458        }
459    }
460
461    /**
462     * View an attachment by an application on device.
463     */
464    private void sendViewIntent() {
465        if (MediaStoreUtils.isImageMimeType(Utils.normalizeMimeType(mAttachment.contentType))) {
466            final PhotoViewIntentBuilder builder =
467                    Intents.newPhotoViewActivityIntentBuilder(getContext());
468            builder.setAlbumName(mAttachment.name)
469                .setPhotosUri(mAttachmentsListUri.toString())
470                .setPhotoIndex(mPhotoIndex);
471
472            getContext().startActivity(builder.build());
473            return;
474        }
475
476        Intent intent = new Intent(Intent.ACTION_VIEW);
477        intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
478                | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
479        Utils.setIntentDataAndTypeAndNormalize(intent, mAttachment.contentUri,
480                mAttachment.contentType);
481        try {
482            getContext().startActivity(intent);
483        } catch (ActivityNotFoundException e) {
484            // couldn't find activity for View intent
485            LogUtils.e(LOG_TAG, "Coun't find Activity for intent", e);
486        }
487    }
488
489    /**
490     * Displays a loading dialog to be used for downloading attachments.
491     * Must be called on the UI thread.
492     */
493    private void showDownloadingDialog() {
494        mViewProgressDialog = new ProgressDialog(getContext());
495        mViewProgressDialog.setTitle(R.string.fetching_attachment);
496        mViewProgressDialog.setMessage(getResources().getString(R.string.please_wait));
497        mViewProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
498        mViewProgressDialog.setMax(mAttachment.size);
499        mViewProgressDialog.setOnDismissListener(this);
500        mViewProgressDialog.setOnCancelListener(this);
501        mViewProgressDialog.show();
502
503        // The progress number format needs to be set after the dialog is shown.  See bug: 5149918
504        mViewProgressDialog.setProgressNumberFormat(null);
505    }
506
507    @Override
508    public void onDismiss(DialogInterface dialog) {
509        mViewProgressDialog = null;
510    }
511
512    @Override
513    public void onCancel(DialogInterface dialog) {
514        cancelAttachment();
515    }
516
517}
518