• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
18 package com.android.mail.browse;
19 
20 import android.app.AlertDialog;
21 import android.app.FragmentManager;
22 import android.content.ActivityNotFoundException;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.net.Uri;
26 import android.text.TextUtils;
27 import android.util.AttributeSet;
28 import android.view.LayoutInflater;
29 import android.view.Menu;
30 import android.view.MenuItem;
31 import android.view.View;
32 import android.view.View.OnClickListener;
33 import android.view.ViewGroup;
34 import android.widget.FrameLayout;
35 import android.widget.ImageButton;
36 import android.widget.ImageView;
37 import android.widget.PopupMenu;
38 import android.widget.PopupMenu.OnMenuItemClickListener;
39 import android.widget.ProgressBar;
40 import android.widget.TextView;
41 
42 import com.android.mail.R;
43 import com.android.mail.analytics.Analytics;
44 import com.android.mail.providers.Attachment;
45 import com.android.mail.providers.UIProvider.AttachmentDestination;
46 import com.android.mail.providers.UIProvider.AttachmentState;
47 import com.android.mail.utils.AttachmentUtils;
48 import com.android.mail.utils.LogTag;
49 import com.android.mail.utils.LogUtils;
50 import com.android.mail.utils.MimeType;
51 import 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  */
58 public 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 
MessageAttachmentBar(Context context)85     public MessageAttachmentBar(Context context) {
86         this(context, null);
87     }
88 
MessageAttachmentBar(Context context, AttributeSet attrs)89     public MessageAttachmentBar(Context context, AttributeSet attrs) {
90         super(context, attrs);
91 
92         mActionHandler = new AttachmentActionHandler(context, this);
93     }
94 
initialize(FragmentManager fragmentManager)95     public void initialize(FragmentManager fragmentManager) {
96         mActionHandler.initialize(fragmentManager);
97     }
98 
inflate(LayoutInflater inflater, ViewGroup parent)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      */
render(Attachment attachment, Uri accountUri, boolean loaderResult)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
onFinishInflate()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
onClick(View v)162     public void onClick(View v) {
163         onClick(v.getId(), v);
164     }
165 
166     @Override
onMenuItemClick(MenuItem item)167     public boolean onMenuItemClick(MenuItem item) {
168         mPopup.dismiss();
169         return onClick(item.getItemId(), null);
170     }
171 
onClick(final int res, final View v)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 
shouldShowPreview()289     private boolean shouldShowPreview() {
290         // state could be anything
291         return mAttachment.canPreview();
292     }
293 
shouldShowSave()294     private boolean shouldShowSave() {
295         return mAttachment.canSave() && !mSaveClicked;
296     }
297 
shouldShowDownloadAgain()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 
shouldShowOverflow()304     private boolean shouldShowOverflow() {
305         return (shouldShowPreview() || shouldShowSave() || shouldShowDownloadAgain())
306                 && !shouldShowCancel();
307     }
308 
shouldShowCancel()309     private boolean shouldShowCancel() {
310         return mAttachment.isDownloading() && mSaveClicked;
311     }
312 
313     @Override
viewAttachment()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 
previewAttachment()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 
setButtonVisible(View button, boolean visible)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      */
updateActions()362     private void updateActions() {
363         removeCallbacks(mUpdateRunnable);
364         post(mUpdateRunnable);
365     }
366 
updateActionsInternal()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
onUpdateStatus()380     public void onUpdateStatus() {
381         updateSubtitleText();
382     }
383 
384     @Override
updateProgress(boolean showProgress)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 
updateSubtitleText()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