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.support.v4.text.BidiFormatter;
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.Account;
45 import com.android.mail.providers.Attachment;
46 import com.android.mail.providers.UIProvider.AttachmentDestination;
47 import com.android.mail.providers.UIProvider.AttachmentState;
48 import com.android.mail.ui.AccountFeedbackActivity;
49 import com.android.mail.utils.AttachmentUtils;
50 import com.android.mail.utils.LogTag;
51 import com.android.mail.utils.LogUtils;
52 import com.android.mail.utils.MimeType;
53 import 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  */
60 public 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 
MessageAttachmentBar(Context context)94     public MessageAttachmentBar(Context context) {
95         this(context, null);
96     }
97 
MessageAttachmentBar(Context context, AttributeSet attrs)98     public MessageAttachmentBar(Context context, AttributeSet attrs) {
99         super(context, attrs);
100 
101         mActionHandler = new AttachmentActionHandler(context, this);
102     }
103 
initialize(FragmentManager fragmentManager)104     public void initialize(FragmentManager fragmentManager) {
105         mActionHandler.initialize(fragmentManager);
106     }
107 
inflate(LayoutInflater inflater, ViewGroup parent)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      */
render(Attachment attachment, Account account, ConversationMessage message, boolean loaderResult, BidiFormatter bidiFormatter)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         if (mAccount != null) {
127             mActionHandler.setAccount(mAccount.getEmailAddress());
128         }
129         mActionHandler.setMessage(message);
130         mActionHandler.setAttachment(mAttachment);
131         mHideExtraOptionOne = message.getConversation() == null;
132 
133         // reset mSaveClicked if we are not currently downloading
134         // So if the download fails or the download completes, we stop
135         // showing progress, etc
136         mSaveClicked = !attachment.isDownloading() ? false : mSaveClicked;
137 
138         LogUtils.d(LOG_TAG, "got attachment list row: name=%s state/dest=%d/%d dled=%d" +
139                 " contentUri=%s MIME=%s flags=%d", attachment.getName(), attachment.state,
140                 attachment.destination, attachment.downloadedSize, attachment.contentUri,
141                 attachment.getContentType(), attachment.flags);
142 
143         final String attachmentName = attachment.getName();
144         if ((attachment.flags & Attachment.FLAG_DUMMY_ATTACHMENT) != 0) {
145             mTitle.setText(R.string.load_attachment);
146         } else if (prevAttachment == null
147                 || !TextUtils.equals(attachmentName, prevAttachment.getName())) {
148             mTitle.setText(attachmentName);
149         }
150 
151         if (prevAttachment == null || attachment.size != prevAttachment.size) {
152             mAttachmentSizeText = bidiFormatter.unicodeWrap(
153                     AttachmentUtils.convertToHumanReadableSize(getContext(), attachment.size));
154             mDisplayType = bidiFormatter.unicodeWrap(
155                     AttachmentUtils.getDisplayType(getContext(), attachment));
156             updateSubtitleText();
157         }
158 
159         updateActions();
160         mActionHandler.updateStatus(loaderResult);
161     }
162 
163     @Override
onFinishInflate()164     protected void onFinishInflate() {
165         super.onFinishInflate();
166 
167         mTitle = (TextView) findViewById(R.id.attachment_title);
168         mSubTitle = (TextView) findViewById(R.id.attachment_subtitle);
169         mProgress = (ProgressBar) findViewById(R.id.attachment_progress);
170         mOverflowButton = (ImageView) findViewById(R.id.overflow);
171         mCancelButton = (ImageButton) findViewById(R.id.cancel_attachment);
172 
173         setOnClickListener(this);
174         mOverflowButton.setOnClickListener(this);
175         mCancelButton.setOnClickListener(this);
176     }
177 
178     @Override
onClick(View v)179     public void onClick(View v) {
180         onClick(v.getId(), v);
181     }
182 
183     @Override
onMenuItemClick(MenuItem item)184     public boolean onMenuItemClick(MenuItem item) {
185         mPopup.dismiss();
186         return onClick(item.getItemId(), null);
187     }
188 
onClick(final int res, final View v)189     private boolean onClick(final int res, final View v) {
190         if (res == R.id.preview_attachment) {
191             previewAttachment();
192         } else if (res == R.id.save_attachment) {
193             if (mAttachment.canSave()) {
194                 mActionHandler.startDownloadingAttachment(AttachmentDestination.EXTERNAL);
195                 mSaveClicked = true;
196 
197                 Analytics.getInstance().sendEvent(
198                         "save_attachment", Utils.normalizeMimeType(mAttachment.getContentType()),
199                         "attachment_bar", mAttachment.size);
200             }
201         } else if (res == R.id.download_again) {
202             if (mAttachment.isPresentLocally()) {
203                 mActionHandler.showDownloadingDialog();
204                 mActionHandler.startRedownloadingAttachment(mAttachment);
205 
206                 Analytics.getInstance().sendEvent("redownload_attachment",
207                         Utils.normalizeMimeType(mAttachment.getContentType()), "attachment_bar",
208                         mAttachment.size);
209             }
210         } else if (res == R.id.cancel_attachment) {
211             mActionHandler.cancelAttachment();
212             mSaveClicked = false;
213 
214             Analytics.getInstance().sendEvent(
215                     "cancel_attachment", Utils.normalizeMimeType(mAttachment.getContentType()),
216                     "attachment_bar", mAttachment.size);
217         } else if (res == R.id.attachment_extra_option1) {
218             mActionHandler.handleOption1();
219         } else if (res == R.id.overflow) {
220             // If no overflow items are visible, just bail out.
221             // We shouldn't be able to get here anyhow since the overflow
222             // button should be hidden.
223             if (shouldShowOverflow()) {
224                 if (mPopup == null) {
225                     mPopup = new PopupMenu(getContext(), v);
226                     mPopup.getMenuInflater().inflate(R.menu.message_footer_overflow_menu,
227                             mPopup.getMenu());
228                     mPopup.setOnMenuItemClickListener(this);
229                 }
230 
231                 final Menu menu = mPopup.getMenu();
232                 menu.findItem(R.id.preview_attachment).setVisible(shouldShowPreview());
233                 menu.findItem(R.id.save_attachment).setVisible(shouldShowSave());
234                 menu.findItem(R.id.download_again).setVisible(shouldShowDownloadAgain());
235                 menu.findItem(R.id.attachment_extra_option1).setVisible(shouldShowExtraOption1());
236 
237                 mPopup.show();
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 
shouldShowPreview()299     private boolean shouldShowPreview() {
300         // state could be anything
301         return mAttachment.canPreview();
302     }
303 
shouldShowSave()304     private boolean shouldShowSave() {
305         return mAttachment.canSave() && !mSaveClicked;
306     }
307 
shouldShowDownloadAgain()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 
shouldShowExtraOption1()314     private boolean shouldShowExtraOption1() {
315         return !mHideExtraOptionOne &&
316                 mActionHandler.shouldShowExtraOption1(mAccount.getType(),
317                         mAttachment.getContentType());
318     }
319 
shouldShowOverflow()320     private boolean shouldShowOverflow() {
321         return (shouldShowPreview() || shouldShowSave() || shouldShowDownloadAgain() ||
322                 shouldShowExtraOption1()) && !shouldShowCancel();
323     }
324 
shouldShowCancel()325     private boolean shouldShowCancel() {
326         return mAttachment.isDownloading() && mSaveClicked;
327     }
328 
329     @Override
viewAttachment()330     public void viewAttachment() {
331         if (mAttachment.contentUri == null) {
332             LogUtils.e(LOG_TAG, "viewAttachment with null content uri");
333             return;
334         }
335 
336         Intent intent = new Intent(Intent.ACTION_VIEW);
337         intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
338                 | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
339 
340         final String contentType = mAttachment.getContentType();
341         Utils.setIntentDataAndTypeAndNormalize(
342                 intent, mAttachment.contentUri, contentType);
343 
344         // For EML files, we want to open our dedicated
345         // viewer rather than let any activity open it.
346         if (MimeType.isEmlMimeType(contentType)) {
347             intent.setPackage(getContext().getPackageName());
348             intent.putExtra(AccountFeedbackActivity.EXTRA_ACCOUNT_URI,
349                     mAccount != null ? mAccount.uri : null);
350         }
351 
352         try {
353             getContext().startActivity(intent);
354         } catch (ActivityNotFoundException e) {
355             // couldn't find activity for View intent
356             LogUtils.e(LOG_TAG, e, "Couldn't find Activity for intent");
357         }
358     }
359 
previewAttachment()360     private void previewAttachment() {
361         if (mAttachment.canPreview()) {
362             final Intent previewIntent =
363                     new Intent(Intent.ACTION_VIEW, mAttachment.previewIntentUri);
364             previewIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
365             getContext().startActivity(previewIntent);
366 
367             Analytics.getInstance().sendEvent(
368                     "preview_attachment", Utils.normalizeMimeType(mAttachment.getContentType()),
369                     null, mAttachment.size);
370         }
371     }
372 
setButtonVisible(View button, boolean visible)373     private static void setButtonVisible(View button, boolean visible) {
374         button.setVisibility(visible ? VISIBLE : GONE);
375     }
376 
377     /**
378      * Update all actions based on current downloading state.
379      */
updateActions()380     private void updateActions() {
381         removeCallbacks(mUpdateRunnable);
382         post(mUpdateRunnable);
383     }
384 
updateActionsInternal()385     private void updateActionsInternal() {
386         // If the progress dialog is visible, skip any of the updating
387         if (mActionHandler.isProgressDialogVisible()) {
388             return;
389         }
390 
391         // To avoid visibility state transition bugs, every button's visibility should be touched
392         // once by this routine.
393         setButtonVisible(mCancelButton, shouldShowCancel());
394         setButtonVisible(mOverflowButton, shouldShowOverflow());
395     }
396 
397     @Override
onUpdateStatus()398     public void onUpdateStatus() {
399         updateSubtitleText();
400     }
401 
402     @Override
updateProgress(boolean showProgress)403     public void updateProgress(boolean showProgress) {
404         if (mAttachment.isDownloading()) {
405             mProgress.setMax(mAttachment.size);
406             mProgress.setProgress(mAttachment.downloadedSize);
407             mProgress.setIndeterminate(!showProgress);
408             mProgress.setVisibility(VISIBLE);
409             mSubTitle.setVisibility(INVISIBLE);
410         } else {
411             mProgress.setVisibility(INVISIBLE);
412             mSubTitle.setVisibility(VISIBLE);
413         }
414     }
415 
updateSubtitleText()416     private void updateSubtitleText() {
417         // TODO: make this a formatted resource when we have a UX design.
418         // not worth translation right now.
419         final StringBuilder sb = new StringBuilder();
420         if (mAttachment.state == AttachmentState.FAILED) {
421             sb.append(getResources().getString(R.string.download_failed));
422         } else {
423             if (mAttachment.isSavedToExternal()) {
424                 sb.append(getResources().getString(R.string.saved, mAttachmentSizeText));
425             } else {
426                 sb.append(mAttachmentSizeText);
427             }
428             if (mDisplayType != null) {
429                 sb.append(' ');
430                 sb.append(mDisplayType);
431             }
432         }
433         mSubTitle.setText(sb.toString());
434     }
435 }
436