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