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