MessageAttachmentTile.java revision 15b43f21f67e425bdfc2c401b3ac4367075c2dd1
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 18package com.android.mail.browse; 19 20import android.app.AlertDialog; 21import android.app.ProgressDialog; 22import android.content.ActivityNotFoundException; 23import android.content.AsyncQueryHandler; 24import android.content.ContentValues; 25import android.content.Context; 26import android.content.DialogInterface; 27import android.content.Intent; 28import android.content.res.AssetFileDescriptor; 29import android.graphics.Bitmap; 30import android.graphics.BitmapFactory; 31import android.net.Uri; 32import android.os.AsyncTask; 33import android.text.TextUtils; 34import android.util.AttributeSet; 35import android.view.LayoutInflater; 36import android.view.MenuItem; 37import android.view.View; 38import android.view.View.OnClickListener; 39import android.view.ViewGroup; 40import android.widget.Button; 41import android.widget.ImageView; 42import android.widget.LinearLayout; 43import android.widget.PopupMenu.OnMenuItemClickListener; 44import android.widget.ProgressBar; 45import android.widget.TextView; 46 47import com.android.mail.R; 48import com.android.mail.photo.Intents; 49import com.android.mail.photo.Intents.PhotoViewIntentBuilder; 50import com.android.mail.photo.util.MediaStoreUtils; 51import com.android.mail.providers.Attachment; 52import com.android.mail.providers.UIProvider.AttachmentColumns; 53import com.android.mail.providers.UIProvider.AttachmentDestination; 54import com.android.mail.providers.UIProvider.AttachmentState; 55import com.android.mail.utils.AttachmentUtils; 56import com.android.mail.utils.LogUtils; 57import com.android.mail.utils.MimeType; 58import com.android.mail.utils.Utils; 59 60import java.io.IOException; 61 62/** 63 * View for a single attachment in conversation view. Shows download status and allows launching 64 * intents to act on an attachment. 65 * 66 */ 67public class MessageAttachmentTile extends LinearLayout implements OnClickListener, 68 OnMenuItemClickListener, DialogInterface.OnCancelListener, 69 DialogInterface.OnDismissListener { 70 71 private Attachment mAttachment; 72 private ImageView mIcon; 73 private ImageView.ScaleType mIconScaleType; 74 private int mPhotoIndex; 75 private TextView mTitle; 76 private TextView mSubTitle; 77 private String mAttachmentSizeText; 78 private String mDisplayType; 79 private Uri mAttachmentsListUri; 80 private ProgressDialog mViewProgressDialog; 81 private AttachmentCommandHandler mCommandHandler; 82 private ProgressBar mProgress; 83 private Button mPreviewButton; 84 private Button mViewButton; 85 private Button mSaveButton; 86 private Button mInfoButton; 87 private Button mPlayButton; 88 private Button mInstallButton; 89 private Button mCancelButton; 90 91 private ThumbnailLoadTask mThumbnailTask; 92 93 private static final String LOG_TAG = new LogUtils().getLogTag(); 94 95 private class AttachmentCommandHandler extends AsyncQueryHandler { 96 97 public AttachmentCommandHandler() { 98 super(getContext().getContentResolver()); 99 } 100 101 /** 102 * Asynchronously begin an update() on a ContentProvider. 103 * 104 */ 105 public void sendCommand(ContentValues params) { 106 startUpdate(0, null, mAttachment.uri, params, null, null); 107 } 108 109 } 110 111 private class ThumbnailLoadTask extends AsyncTask<Uri, Void, Bitmap> { 112 113 private final int mWidth; 114 private final int mHeight; 115 116 public ThumbnailLoadTask(int width, int height) { 117 mWidth = width; 118 mHeight = height; 119 } 120 121 @Override 122 protected Bitmap doInBackground(Uri... params) { 123 final Uri thumbnailUri = params[0]; 124 125 AssetFileDescriptor fd = null; 126 Bitmap result = null; 127 128 try { 129 fd = getContext().getContentResolver().openAssetFileDescriptor(thumbnailUri, "r"); 130 if (isCancelled() || fd == null) { 131 return null; 132 } 133 134 final BitmapFactory.Options opts = new BitmapFactory.Options(); 135 opts.inJustDecodeBounds = true; 136 137 BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, opts); 138 if (isCancelled() || opts.outWidth == -1 || opts.outHeight == -1) { 139 return null; 140 } 141 142 opts.inJustDecodeBounds = false; 143 144 LogUtils.d(LOG_TAG, "in background, src w/h=%d/%d dst w/h=%d/%d, divider=%d", 145 opts.outWidth, opts.outHeight, mWidth, mHeight, opts.inSampleSize); 146 147 result = BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, opts); 148 149 } catch (Throwable t) { 150 LogUtils.e(LOG_TAG, t, "Unable to decode thumbnail %s", thumbnailUri); 151 } finally { 152 if (fd != null) { 153 try { 154 fd.close(); 155 } catch (IOException e) { 156 LogUtils.e(LOG_TAG, e, ""); 157 } 158 } 159 } 160 161 return result; 162 } 163 164 @Override 165 protected void onPostExecute(Bitmap result) { 166 if (result == null) { 167 LogUtils.d(LOG_TAG, "back in UI thread, decode failed"); 168 setThumbnailToDefault(); 169 return; 170 } 171 172 LogUtils.d(LOG_TAG, "back in UI thread, decode success, w/h=%d/%d", result.getWidth(), 173 result.getHeight()); 174 mIcon.setImageBitmap(result); 175 mIcon.setScaleType(mIconScaleType); 176 } 177 178 } 179 180 public MessageAttachmentTile(Context context) { 181 super(context); 182 } 183 184 public MessageAttachmentTile(Context context, AttributeSet attrs) { 185 super(context, attrs); 186 187 mCommandHandler = new AttachmentCommandHandler(); 188 } 189 190 public static MessageAttachmentTile inflate(LayoutInflater inflater, ViewGroup parent) { 191 MessageAttachmentTile view = (MessageAttachmentTile) inflater.inflate( 192 R.layout.conversation_message_attachment, parent, false); 193 return view; 194 } 195 196 /** 197 * Render or update an attachment's view. This happens immediately upon instantiation, and 198 * repeatedly as status updates stream in, so only properties with new or changed values will 199 * cause sub-views to update. 200 * 201 */ 202 public void render(Attachment attachment, Uri attachmentsListUri, int index) { 203 if (attachment == null) { 204 setVisibility(View.INVISIBLE); 205 return; 206 } 207 208 final Attachment prevAttachment = mAttachment; 209 mAttachment = attachment; 210 mAttachmentsListUri = attachmentsListUri; 211 mPhotoIndex = index; 212 213 LogUtils.d(LOG_TAG, "got attachment list row: name=%s state/dest=%d/%d dled=%d" + 214 " contentUri=%s MIME=%s", attachment.name, attachment.state, 215 attachment.destination, attachment.downloadedSize, attachment.contentUri, 216 attachment.contentType); 217 218 if (prevAttachment == null || TextUtils.equals(attachment.name, prevAttachment.name)) { 219 mTitle.setText(attachment.name); 220 } 221 222 if (prevAttachment == null || attachment.size != prevAttachment.size) { 223 mAttachmentSizeText = AttachmentUtils.convertToHumanReadableSize(getContext(), 224 attachment.size); 225 mDisplayType = AttachmentUtils.getDisplayType(getContext(), attachment); 226 updateSubtitleText(null); 227 } 228 229 final Uri imageUri = attachment.getImageUri(); 230 final Uri prevImageUri = (prevAttachment == null) ? null : prevAttachment.getImageUri(); 231 // begin loading a thumbnail if this is an image and either the thumbnail or the original 232 // content is ready (and different from any existing image) 233 if (imageUri != null && (prevImageUri == null || !imageUri.equals(prevImageUri))) { 234 // cancel/dispose any existing task and start a new one 235 if (mThumbnailTask != null) { 236 mThumbnailTask.cancel(true); 237 } 238 mThumbnailTask = new ThumbnailLoadTask(mIcon.getWidth(), mIcon.getHeight()); 239 mThumbnailTask.execute(imageUri); 240 } else if (imageUri == null) { 241 // not an image, or no thumbnail exists. fall back to default. 242 // async image load must separately ensure the default appears upon load failure. 243 setThumbnailToDefault(); 244 } 245 246 if (mProgress != null) { 247 mProgress.setMax(attachment.size); 248 } 249 250 updateActions(); 251 updateStatus(); 252 } 253 254 private void setThumbnailToDefault() { 255 mIcon.setImageResource(R.drawable.ic_menu_attachment_holo_light); 256 mIcon.setScaleType(ImageView.ScaleType.CENTER); 257 } 258 259 /** 260 * Update progress-related views. Will also trigger a view intent if a progress dialog was 261 * previously brought up (by tapping 'View') and the download has now finished. 262 */ 263 private void updateStatus() { 264 final boolean showProgress = mAttachment.size > 0 && mAttachment.downloadedSize > 0 265 && mAttachment.downloadedSize < mAttachment.size; 266 267 if (mViewProgressDialog != null && mViewProgressDialog.isShowing()) { 268 mViewProgressDialog.setProgress(mAttachment.downloadedSize); 269 mViewProgressDialog.setIndeterminate(!showProgress); 270 271 if (!mAttachment.isDownloading()) { 272 mViewProgressDialog.dismiss(); 273 } 274 275 if (mAttachment.state == AttachmentState.SAVED) { 276 sendViewIntent(); 277 } 278 } else { 279 280 if (mAttachment.isDownloading()) { 281 mProgress.setProgress(mAttachment.downloadedSize); 282 setProgressVisible(true); 283 mProgress.setIndeterminate(!showProgress); 284 } else { 285 setProgressVisible(false); 286 } 287 288 } 289 290 if (mAttachment.state == AttachmentState.FAILED) { 291 mSubTitle.setText(getResources().getString(R.string.download_failed)); 292 } else { 293 updateSubtitleText(mAttachment.isSavedToExternal() ? 294 getResources().getString(R.string.saved) : null); 295 } 296 } 297 298 private void setProgressVisible(boolean visible) { 299 if (visible) { 300 mProgress.setVisibility(VISIBLE); 301 mSubTitle.setVisibility(INVISIBLE); 302 } else { 303 mProgress.setVisibility(GONE); 304 mSubTitle.setVisibility(VISIBLE); 305 } 306 } 307 308 private void updateSubtitleText(String prefix) { 309 // TODO: make this a formatted resource when we have a UX design. 310 // not worth translation right now. 311 StringBuilder sb = new StringBuilder(); 312 if (prefix != null) { 313 sb.append(prefix); 314 } 315 sb.append(mAttachmentSizeText); 316 sb.append(' '); 317 sb.append(mDisplayType); 318 mSubTitle.setText(sb.toString()); 319 } 320 321 @Override 322 protected void onFinishInflate() { 323 super.onFinishInflate(); 324 325 mIcon = (ImageView) findViewById(R.id.attachment_tile_image); 326 mTitle = (TextView) findViewById(R.id.attachment_tile_title); 327 mSubTitle = (TextView) findViewById(R.id.attachment_tile_subtitle); 328 mProgress = (ProgressBar) findViewById(R.id.attachment_progress); 329 330// mPreviewButton = (Button) findViewById(R.id.preview_attachment); 331// mViewButton = (Button) findViewById(R.id.view_attachment); 332 mSaveButton = (Button) findViewById(R.id.attachment_tile_secondary_button); 333// mInfoButton = (Button) findViewById(R.id.info_attachment); 334// mPlayButton = (Button) findViewById(R.id.play_attachment); 335// mInstallButton = (Button) findViewById(R.id.install_attachment); 336// mCancelButton = (Button) findViewById(R.id.cancel_attachment); 337 338 setOnClickListener(this); 339// mPreviewButton.setOnClickListener(this); 340// mViewButton.setOnClickListener(this); 341 mSaveButton.setOnClickListener(this); 342// mInfoButton.setOnClickListener(this); 343// mPlayButton.setOnClickListener(this); 344// mInstallButton.setOnClickListener(this); 345// mCancelButton.setOnClickListener(this); 346 347 mIconScaleType = mIcon.getScaleType(); 348 } 349 350 @Override 351 public void onClick(View v) { 352 onClick(v.getId(), v); 353 } 354 355 @Override 356 public boolean onMenuItemClick(MenuItem item) { 357 return onClick(item.getItemId(), null); 358 } 359 360 private boolean onClick(int res, View v) { 361 switch (res) { 362 case R.id.preview_attachment: 363 getContext().startActivity(mAttachment.previewIntent); 364 break; 365 case R.id.view_attachment: 366 case R.id.play_attachment: 367 case R.id.attachment_tile: 368 showAttachment(AttachmentDestination.CACHE); 369 break; 370 case R.id.save_attachment: 371 case R.id.attachment_tile_secondary_button: 372 if (mAttachment.canSave()) { 373 startDownloadingAttachment(AttachmentDestination.EXTERNAL); 374 } 375 break; 376 case R.id.info_attachment: 377 AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); 378 int dialogMessage = MimeType.isBlocked(mAttachment.contentType) 379 ? R.string.attachment_type_blocked : R.string.no_application_found; 380 builder.setTitle(R.string.more_info_attachment).setMessage(dialogMessage).show(); 381 break; 382 case R.id.install_attachment: 383 showAttachment(AttachmentDestination.EXTERNAL); 384 break; 385 case R.id.cancel_attachment: 386 cancelAttachment(); 387 break; 388 default: 389 break; 390 } 391 return true; 392 } 393 394 private void showAttachment(int destination) { 395 if (mAttachment.isPresentLocally()) { 396 sendViewIntent(); 397 } else { 398 showDownloadingDialog(); 399 startDownloadingAttachment(destination); 400 } 401 } 402 403 private void startDownloadingAttachment(int destination) { 404 final ContentValues params = new ContentValues(2); 405 params.put(AttachmentColumns.STATE, AttachmentState.DOWNLOADING); 406 params.put(AttachmentColumns.DESTINATION, destination); 407 408 mCommandHandler.sendCommand(params); 409 } 410 411 private void cancelAttachment() { 412 final ContentValues params = new ContentValues(1); 413 params.put(AttachmentColumns.STATE, AttachmentState.NOT_SAVED); 414 415 mCommandHandler.sendCommand(params); 416 } 417 418 private void setButtonVisible(View button, boolean visible) { 419 if (button != null) { 420 button.setVisibility(visible ? VISIBLE : GONE); 421 } 422 } 423 424 /** 425 * Update all action buttons based on current downloading state. 426 */ 427 private void updateActions() { 428 // To avoid visibility state transition bugs, every button's visibility should be touched 429 // once by this routine. 430 431 final boolean isDownloading = mAttachment.isDownloading(); 432 433 setButtonVisible(mCancelButton, isDownloading); 434 435 final boolean canInstall = MimeType.isInstallable(mAttachment.contentType); 436 setButtonVisible(mInstallButton, canInstall && !isDownloading); 437 438 if (!canInstall) { 439 440 final boolean canPreview = (mAttachment.previewIntent != null); 441 final boolean canView = MimeType.isViewable(getContext(), mAttachment.contentType); 442 final boolean canPlay = MimeType.isPlayable(mAttachment.contentType); 443 444 setButtonVisible(mPreviewButton, canPreview); 445 setButtonVisible(mPlayButton, canView && canPlay && !isDownloading); 446 setButtonVisible(mViewButton, canView && !canPlay && !isDownloading); 447 setButtonVisible(mSaveButton, canView && mAttachment.canSave() && !isDownloading); 448 setButtonVisible(mInfoButton, !(canPreview || canView)); 449 450 } else { 451 452 setButtonVisible(mPreviewButton, false); 453 setButtonVisible(mPlayButton, false); 454 setButtonVisible(mViewButton, false); 455 setButtonVisible(mSaveButton, false); 456 setButtonVisible(mInfoButton, false); 457 458 } 459 } 460 461 /** 462 * View an attachment by an application on device. 463 */ 464 private void sendViewIntent() { 465 if (MediaStoreUtils.isImageMimeType(Utils.normalizeMimeType(mAttachment.contentType))) { 466 final PhotoViewIntentBuilder builder = 467 Intents.newPhotoViewActivityIntentBuilder(getContext()); 468 builder.setAlbumName(mAttachment.name) 469 .setPhotosUri(mAttachmentsListUri.toString()) 470 .setPhotoIndex(mPhotoIndex); 471 472 getContext().startActivity(builder.build()); 473 return; 474 } 475 476 Intent intent = new Intent(Intent.ACTION_VIEW); 477 intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION 478 | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 479 Utils.setIntentDataAndTypeAndNormalize(intent, mAttachment.contentUri, 480 mAttachment.contentType); 481 try { 482 getContext().startActivity(intent); 483 } catch (ActivityNotFoundException e) { 484 // couldn't find activity for View intent 485 LogUtils.e(LOG_TAG, "Coun't find Activity for intent", e); 486 } 487 } 488 489 /** 490 * Displays a loading dialog to be used for downloading attachments. 491 * Must be called on the UI thread. 492 */ 493 private void showDownloadingDialog() { 494 mViewProgressDialog = new ProgressDialog(getContext()); 495 mViewProgressDialog.setTitle(R.string.fetching_attachment); 496 mViewProgressDialog.setMessage(getResources().getString(R.string.please_wait)); 497 mViewProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); 498 mViewProgressDialog.setMax(mAttachment.size); 499 mViewProgressDialog.setOnDismissListener(this); 500 mViewProgressDialog.setOnCancelListener(this); 501 mViewProgressDialog.show(); 502 503 // The progress number format needs to be set after the dialog is shown. See bug: 5149918 504 mViewProgressDialog.setProgressNumberFormat(null); 505 } 506 507 @Override 508 public void onDismiss(DialogInterface dialog) { 509 mViewProgressDialog = null; 510 } 511 512 @Override 513 public void onCancel(DialogInterface dialog) { 514 cancelAttachment(); 515 } 516 517} 518