Home | History | Annotate | Download | only in browse
      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 
     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         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
    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
    179     public void onClick(View v) {
    180         onClick(v.getId(), v);
    181     }
    182 
    183     @Override
    184     public boolean onMenuItemClick(MenuItem item) {
    185         mPopup.dismiss();
    186         return onClick(item.getItemId(), null);
    187     }
    188 
    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 
    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 &&
    316                 mActionHandler.shouldShowExtraOption1(mAccount.getType(),
    317                         mAttachment.getContentType());
    318     }
    319 
    320     private boolean shouldShowOverflow() {
    321         return (shouldShowPreview() || shouldShowSave() || shouldShowDownloadAgain() ||
    322                 shouldShowExtraOption1()) && !shouldShowCancel();
    323     }
    324 
    325     private boolean shouldShowCancel() {
    326         return mAttachment.isDownloading() && mSaveClicked;
    327     }
    328 
    329     @Override
    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 
    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 
    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      */
    380     private void updateActions() {
    381         removeCallbacks(mUpdateRunnable);
    382         post(mUpdateRunnable);
    383     }
    384 
    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
    398     public void onUpdateStatus() {
    399         updateSubtitleText();
    400     }
    401 
    402     @Override
    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 
    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