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