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