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