MessageAttachmentBar.java revision 7638a1004e7c8ed7a85620243da3b051b60217e8
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.FragmentManager;
22import android.content.ActivityNotFoundException;
23import android.content.Context;
24import android.content.Intent;
25import android.net.Uri;
26import android.text.TextUtils;
27import android.util.AttributeSet;
28import android.view.LayoutInflater;
29import android.view.Menu;
30import android.view.MenuItem;
31import android.view.View;
32import android.view.View.OnClickListener;
33import android.view.ViewGroup;
34import android.widget.FrameLayout;
35import android.widget.ImageButton;
36import android.widget.ImageView;
37import android.widget.PopupMenu;
38import android.widget.PopupMenu.OnMenuItemClickListener;
39import android.widget.ProgressBar;
40import android.widget.TextView;
41
42import com.android.mail.R;
43import com.android.mail.providers.Attachment;
44import com.android.mail.providers.UIProvider.AttachmentDestination;
45import com.android.mail.providers.UIProvider.AttachmentState;
46import com.android.mail.utils.AttachmentUtils;
47import com.android.mail.utils.LogTag;
48import com.android.mail.utils.LogUtils;
49import com.android.mail.utils.MimeType;
50import com.android.mail.utils.Utils;
51
52/**
53 * View for a single attachment in conversation view. Shows download status and allows launching
54 * intents to act on an attachment.
55 *
56 */
57public class MessageAttachmentBar extends FrameLayout implements OnClickListener,
58        OnMenuItemClickListener, AttachmentViewInterface {
59
60    private Attachment mAttachment;
61    private TextView mTitle;
62    private TextView mSubTitle;
63    private String mAttachmentSizeText;
64    private String mDisplayType;
65    private ProgressBar mProgress;
66    private ImageButton mCancelButton;
67    private PopupMenu mPopup;
68    private ImageView mOverflowButton;
69
70    private final AttachmentActionHandler mActionHandler;
71    private boolean mSaveClicked;
72    private Uri mAccountUri;
73
74    private final Runnable mUpdateRunnable = new Runnable() {
75            @Override
76        public void run() {
77            updateActionsInternal();
78        }
79    };
80
81    private static final String LOG_TAG = LogTag.getLogTag();
82
83
84    public MessageAttachmentBar(Context context) {
85        this(context, null);
86    }
87
88    public MessageAttachmentBar(Context context, AttributeSet attrs) {
89        super(context, attrs);
90
91        mActionHandler = new AttachmentActionHandler(context, this);
92    }
93
94    public void initialize(FragmentManager fragmentManager) {
95        mActionHandler.initialize(fragmentManager);
96    }
97
98    public static MessageAttachmentBar inflate(LayoutInflater inflater, ViewGroup parent) {
99        MessageAttachmentBar view = (MessageAttachmentBar) inflater.inflate(
100                R.layout.conversation_message_attachment_bar, parent, false);
101        return view;
102    }
103
104    /**
105     * Render or update an attachment's view. This happens immediately upon instantiation, and
106     * repeatedly as status updates stream in, so only properties with new or changed values will
107     * cause sub-views to update.
108     */
109    public void render(Attachment attachment, Uri accountUri, boolean loaderResult) {
110        // get account uri for potential eml viewer usage
111        mAccountUri = accountUri;
112
113        final Attachment prevAttachment = mAttachment;
114        mAttachment = attachment;
115        mActionHandler.setAttachment(mAttachment);
116
117        // reset mSaveClicked if we are not currently downloading
118        // So if the download fails or the download completes, we stop
119        // showing progress, etc
120        mSaveClicked = !attachment.isDownloading() ? false : mSaveClicked;
121
122        LogUtils.d(LOG_TAG, "got attachment list row: name=%s state/dest=%d/%d dled=%d" +
123                " contentUri=%s MIME=%s", attachment.getName(), attachment.state,
124                attachment.destination, attachment.downloadedSize, attachment.contentUri,
125                attachment.getContentType());
126
127        if (prevAttachment == null
128                || !TextUtils.equals(attachment.getName(), prevAttachment.getName())) {
129            mTitle.setText(attachment.getName());
130        }
131
132        if (prevAttachment == null || attachment.size != prevAttachment.size) {
133            mAttachmentSizeText = AttachmentUtils.convertToHumanReadableSize(getContext(),
134                    attachment.size);
135            mDisplayType = AttachmentUtils.getDisplayType(getContext(), attachment);
136            updateSubtitleText();
137        }
138
139        updateActions();
140        mActionHandler.updateStatus(loaderResult);
141    }
142
143    @Override
144    protected void onFinishInflate() {
145        super.onFinishInflate();
146
147        mTitle = (TextView) findViewById(R.id.attachment_title);
148        mSubTitle = (TextView) findViewById(R.id.attachment_subtitle);
149        mProgress = (ProgressBar) findViewById(R.id.attachment_progress);
150        mOverflowButton = (ImageView) findViewById(R.id.overflow);
151        mCancelButton = (ImageButton) findViewById(R.id.cancel_attachment);
152
153        setOnClickListener(this);
154        mOverflowButton.setOnClickListener(this);
155        mCancelButton.setOnClickListener(this);
156    }
157
158    @Override
159    public void onClick(View v) {
160        onClick(v.getId(), v);
161    }
162
163    @Override
164    public boolean onMenuItemClick(MenuItem item) {
165        mPopup.dismiss();
166        return onClick(item.getItemId(), null);
167    }
168
169    private boolean onClick(int res, View v) {
170        switch (res) {
171            case R.id.preview_attachment:
172                previewAttachment();
173                break;
174            case R.id.save_attachment:
175                if (mAttachment.canSave()) {
176                    mActionHandler.startDownloadingAttachment(AttachmentDestination.EXTERNAL);
177                    mSaveClicked = true;
178                }
179                break;
180            case R.id.download_again:
181                if (mAttachment.isPresentLocally()) {
182                    mActionHandler.startRedownloadingAttachment(mAttachment);
183                }
184                break;
185            case R.id.cancel_attachment:
186                mActionHandler.cancelAttachment();
187                mSaveClicked = false;
188                break;
189            case R.id.overflow: {
190                // If no overflow items are visible, just bail out.
191                // We shouldn't be able to get here anyhow since the overflow
192                // button should be hidden.
193                if (!shouldShowOverflow()) {
194                    break;
195                }
196
197                if (mPopup == null) {
198                    mPopup = new PopupMenu(getContext(), v);
199                    mPopup.getMenuInflater().inflate(R.menu.message_footer_overflow_menu,
200                            mPopup.getMenu());
201                    mPopup.setOnMenuItemClickListener(this);
202                }
203
204                final Menu menu = mPopup.getMenu();
205                menu.findItem(R.id.preview_attachment).setVisible(shouldShowPreview());
206                menu.findItem(R.id.save_attachment).setVisible(shouldShowSave());
207                menu.findItem(R.id.download_again).setVisible(shouldShowDownloadAgain());
208
209                mPopup.show();
210                break;
211            }
212            default:
213                // Handles clicking the attachment
214                // in any area that is not the overflow
215                // button or cancel button or one of the
216                // overflow items.
217
218                // If the mimetype is blocked, show the info dialog
219                if (MimeType.isBlocked(mAttachment.getContentType())) {
220                    AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
221                    int dialogMessage = R.string.attachment_type_blocked;
222                    builder.setTitle(R.string.more_info_attachment)
223                           .setMessage(dialogMessage)
224                           .show();
225                }
226                // If we can install, install.
227                else if (MimeType.isInstallable(mAttachment.getContentType())) {
228                    // Save to external because the package manager only handles
229                    // file:// uris not content:// uris. We do the same
230                    // workaround in
231                    // UiProvider#getUiAttachmentsCursorForUIAttachments()
232                    mActionHandler.showAttachment(AttachmentDestination.EXTERNAL);
233                }
234                // If we can view or play with an on-device app,
235                // view or play.
236                else if (MimeType.isViewable(
237                        getContext(), mAttachment.contentUri, mAttachment.getContentType())) {
238                    mActionHandler.showAttachment(AttachmentDestination.CACHE);
239                }
240                // If we can only preview the attachment, preview.
241                else if (mAttachment.canPreview()) {
242                    previewAttachment();
243                }
244                // Otherwise, if we cannot do anything, show the info dialog.
245                else {
246                    AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
247                    int dialogMessage = R.string.no_application_found;
248                    builder.setTitle(R.string.more_info_attachment)
249                           .setMessage(dialogMessage)
250                           .show();
251                }
252                break;
253        }
254
255        return true;
256    }
257
258    private boolean shouldShowPreview() {
259        // state could be anything
260        return mAttachment.canPreview();
261    }
262
263    private boolean shouldShowSave() {
264        return mAttachment.canSave() && !mSaveClicked;
265    }
266
267    private boolean shouldShowDownloadAgain() {
268        // implies state == SAVED || state == FAILED
269        // and the attachment supports re-download
270        return mAttachment.supportsDownloadAgain() && mAttachment.isDownloadFinishedOrFailed();
271    }
272
273    private boolean shouldShowOverflow() {
274        return (shouldShowPreview() || shouldShowSave() || shouldShowDownloadAgain())
275                && !shouldShowCancel();
276    }
277
278    private boolean shouldShowCancel() {
279        return mAttachment.isDownloading() && mSaveClicked;
280    }
281
282    @Override
283    public void viewAttachment() {
284        if (mAttachment.contentUri == null) {
285            LogUtils.e(LOG_TAG, "viewAttachment with null content uri");
286            return;
287        }
288
289        Intent intent = new Intent(Intent.ACTION_VIEW);
290        intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
291                | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
292
293        final String contentType = mAttachment.getContentType();
294        Utils.setIntentDataAndTypeAndNormalize(
295                intent, mAttachment.contentUri, contentType);
296
297        // For EML files, we want to open our dedicated
298        // viewer rather than let any activity open it.
299        if (MimeType.isEmlMimeType(contentType)) {
300            intent.setClass(getContext(), EmlViewerActivity.class);
301            intent.putExtra(EmlViewerActivity.EXTRA_ACCOUNT_URI, mAccountUri);
302        }
303
304        try {
305            getContext().startActivity(intent);
306        } catch (ActivityNotFoundException e) {
307            // couldn't find activity for View intent
308            LogUtils.e(LOG_TAG, e, "Couldn't find Activity for intent");
309        }
310    }
311
312    private void previewAttachment() {
313        if (mAttachment.canPreview()) {
314            final Intent previewIntent =
315                    new Intent(Intent.ACTION_VIEW, mAttachment.previewIntentUri);
316            getContext().startActivity(previewIntent);
317        }
318    }
319
320    private void setButtonVisible(View button, boolean visible) {
321        button.setVisibility(visible ? VISIBLE : GONE);
322    }
323
324    /**
325     * Update all actions based on current downloading state.
326     */
327    private void updateActions() {
328        removeCallbacks(mUpdateRunnable);
329        post(mUpdateRunnable);
330    }
331
332    private void updateActionsInternal() {
333        // If the progress dialog is visible, skip any of the updating
334        if (mActionHandler.isProgressDialogVisible()) {
335            return;
336        }
337
338        // To avoid visibility state transition bugs, every button's visibility should be touched
339        // once by this routine.
340        setButtonVisible(mCancelButton, shouldShowCancel());
341        setButtonVisible(mOverflowButton, shouldShowOverflow());
342    }
343
344    @Override
345    public void onUpdateStatus() {
346        updateSubtitleText();
347    }
348
349    @Override
350    public void updateProgress(boolean showProgress) {
351        if (mAttachment.isDownloading()) {
352            mProgress.setMax(mAttachment.size);
353            mProgress.setProgress(mAttachment.downloadedSize);
354            mProgress.setIndeterminate(!showProgress);
355            mProgress.setVisibility(VISIBLE);
356            mSubTitle.setVisibility(INVISIBLE);
357        } else {
358            mProgress.setVisibility(INVISIBLE);
359            mSubTitle.setVisibility(VISIBLE);
360        }
361    }
362
363    private void updateSubtitleText() {
364        // TODO: make this a formatted resource when we have a UX design.
365        // not worth translation right now.
366        final StringBuilder sb = new StringBuilder();
367        if (mAttachment.state == AttachmentState.FAILED) {
368            sb.append(getResources().getString(R.string.download_failed));
369        } else {
370            if (mAttachment.isSavedToExternal()) {
371                sb.append(getResources().getString(R.string.saved, mAttachmentSizeText));
372            } else {
373                sb.append(mAttachmentSizeText);
374            }
375            sb.append(' ');
376            sb.append(mDisplayType);
377        }
378        mSubTitle.setText(sb.toString());
379    }
380}
381