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.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 
     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