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.ui; 19 20 import android.content.ContentResolver; 21 import android.content.Context; 22 import android.graphics.Bitmap; 23 import android.os.Parcel; 24 import android.os.Parcelable; 25 import android.text.TextUtils; 26 import android.util.AttributeSet; 27 import android.util.DisplayMetrics; 28 import android.view.View; 29 import android.widget.ImageView; 30 import android.widget.ImageView.ScaleType; 31 import android.widget.RelativeLayout; 32 import android.widget.TextView; 33 34 import com.android.ex.photo.util.ImageUtils; 35 import com.android.mail.R; 36 import com.android.mail.providers.Attachment; 37 import com.android.mail.utils.AttachmentUtils; 38 import com.android.mail.utils.LogTag; 39 import com.android.mail.utils.LogUtils; 40 41 /** 42 * Base class for attachment tiles that handles the work of fetching and displaying the bitmaps for 43 * the tiles. 44 */ 45 public abstract class AttachmentTile extends RelativeLayout implements AttachmentBitmapHolder { 46 protected Attachment mAttachment; 47 private ImageView mIcon; 48 private ImageView mDefaultIcon; 49 private TextView mTitle; 50 private TextView mSubtitle; 51 private String mAttachmentSizeText; 52 private String mDisplayType; 53 private boolean mDefaultThumbnailSet; 54 private AttachmentPreviewCache mAttachmentPreviewCache; 55 56 private static final String LOG_TAG = LogTag.getLogTag(); 57 // previews with width/height or height/width less than this value will be 58 // considered skinny 59 private static final float skinnyThresholdRatio = 0.5f; 60 61 private boolean mAlwaysShowInfoText; 62 63 64 /** 65 * Returns true if the attachment should be rendered as a tile. with a large image preview. 66 * @param attachment the attachment to render 67 * @return true if the attachment should be rendered as a tile 68 */ 69 public static boolean isTiledAttachment(final Attachment attachment) { 70 return ImageUtils.isImageMimeType(attachment.getContentType()); 71 } 72 73 public AttachmentTile(Context context) { 74 this(context, null); 75 } 76 77 public AttachmentTile(Context context, AttributeSet attrs) { 78 super(context, attrs); 79 mDefaultThumbnailSet = true; 80 mAlwaysShowInfoText = false; 81 } 82 83 @Override 84 protected void onFinishInflate() { 85 super.onFinishInflate(); 86 87 mTitle = (TextView) findViewById(R.id.attachment_tile_title); 88 mSubtitle = (TextView) findViewById(R.id.attachment_tile_subtitle); 89 mIcon = (ImageView) findViewById(R.id.attachment_tile_image); 90 mDefaultIcon = (ImageView) findViewById(R.id.attachment_default_image); 91 } 92 93 @Override 94 protected void onLayout(boolean changed, int l, int t, int r, int b) { 95 super.onLayout(changed, l, t, r, b); 96 97 ThumbnailLoadTask.setupThumbnailPreview(mAttachmentPreviewCache, this, mAttachment, null); 98 } 99 100 public Attachment getAttachment() { 101 return mAttachment; 102 } 103 104 /** 105 * Render or update an attachment's view. This happens immediately upon instantiation, and 106 * repeatedly as status updates stream in, so only properties with new or changed values will 107 * cause sub-views to update. 108 */ 109 protected void render(Attachment attachment, AttachmentPreviewCache attachmentPreviewCache) { 110 if (attachment == null) { 111 setVisibility(View.INVISIBLE); 112 return; 113 } 114 115 final Attachment prevAttachment = mAttachment; 116 mAttachment = attachment; 117 mAttachmentPreviewCache = attachmentPreviewCache; 118 119 LogUtils.d(LOG_TAG, "got attachment list row: name=%s state/dest=%d/%d dled=%d" + 120 " contentUri=%s MIME=%s flags=%d", attachment.getName(), attachment.state, 121 attachment.destination, attachment.downloadedSize, attachment.contentUri, 122 attachment.getContentType(), attachment.flags); 123 124 if ((attachment.flags & Attachment.FLAG_DUMMY_ATTACHMENT) != 0) { 125 // TODO: This is not an ideal string, but it's too late in KLP to add new strings. 126 mTitle.setText(R.string.load_more); 127 } else if (prevAttachment == null 128 || !TextUtils.equals(attachment.getName(), prevAttachment.getName())) { 129 mTitle.setText(attachment.getName()); 130 } 131 132 if (prevAttachment == null || attachment.size != prevAttachment.size) { 133 mAttachmentSizeText = AttachmentUtils.convertToHumanReadableSize(getContext(), 134 attachment.size); 135 mDisplayType = AttachmentUtils.getDisplayType(getContext(), attachment); 136 updateSubtitleText(); 137 } 138 139 ThumbnailLoadTask.setupThumbnailPreview(mAttachmentPreviewCache, this, attachment, 140 prevAttachment); 141 } 142 143 private void updateSubtitleText() { 144 // TODO: make this a formatted resource when we have a UX design. 145 // not worth translation right now. 146 StringBuilder sb = new StringBuilder(); 147 sb.append(mAttachmentSizeText); 148 if (mDisplayType != null) { 149 sb.append(' '); 150 sb.append(mDisplayType); 151 } 152 mSubtitle.setText(sb.toString()); 153 } 154 155 @Override 156 public void setThumbnailToDefault() { 157 final Bitmap cachedPreview = mAttachmentPreviewCache.get(mAttachment); 158 if (cachedPreview != null) { 159 setThumbnail(cachedPreview); 160 return; 161 } 162 mDefaultIcon.setVisibility(View.VISIBLE); 163 mTitle.setVisibility(View.VISIBLE); 164 mSubtitle.setVisibility(View.VISIBLE); 165 mDefaultThumbnailSet = true; 166 } 167 168 @Override 169 public void setThumbnail(Bitmap result) { 170 if (result == null) { 171 return; 172 } 173 174 // We got a real thumbnail; hide the default thumbnail. 175 mDefaultIcon.setVisibility(View.GONE); 176 if (!mAlwaysShowInfoText) { 177 mTitle.setVisibility(View.GONE); 178 mSubtitle.setVisibility(View.GONE); 179 } 180 181 final int maxSize = getResources().getInteger(R.integer.attachment_preview_max_size); 182 final int width = result.getWidth(); 183 final int height = result.getHeight(); 184 final int scaledWidth = width * getResources().getDisplayMetrics().densityDpi 185 / DisplayMetrics.DENSITY_DEFAULT; 186 final int scaledHeight = height * getResources().getDisplayMetrics().densityDpi 187 / DisplayMetrics.DENSITY_DEFAULT; 188 // ratio of the image 189 final float ratio = Math.min((float) width / height, (float) height / width); 190 191 final boolean large = width >= maxSize || scaledWidth >= mIcon.getWidth() 192 || height >= maxSize || scaledHeight >= mIcon.getHeight(); 193 final boolean skinny = 194 // the image is loooong 195 ratio < skinnyThresholdRatio && 196 // AND if the image was centered and cropped, the resulting 197 // image would still be loooong 198 !(scaledWidth >= mIcon.getHeight() * skinnyThresholdRatio 199 && scaledHeight >= mIcon.getWidth() * skinnyThresholdRatio); 200 LogUtils.d(LOG_TAG, "scaledWidth: %d, scaledHeight: %d, large: %b, skinny: %b", scaledWidth, 201 scaledHeight, large, skinny); 202 203 if (large) { 204 // preview fills up at least 1 dimension 205 if (skinny) { 206 // just center. The shorter dimension stays the same while the 207 // longer dimension is cropped 208 mIcon.setScaleType(ScaleType.CENTER); 209 } else { 210 // fill. Both dimensions are scaled to fill the box, the longer 211 // dimension is cropped 212 mIcon.setScaleType(ScaleType.CENTER_CROP); 213 } 214 } else { 215 // preview is small. just center 216 mIcon.setScaleType(ScaleType.CENTER); 217 } 218 219 mIcon.setImageBitmap(result); 220 mAttachmentPreviewCache.set(mAttachment, result); 221 mDefaultThumbnailSet = false; 222 } 223 224 @Override 225 public int getThumbnailWidth() { 226 return mIcon.getWidth(); 227 } 228 229 @Override 230 public int getThumbnailHeight() { 231 return mIcon.getHeight(); 232 } 233 234 @Override 235 public ContentResolver getResolver() { 236 return getContext().getContentResolver(); 237 } 238 239 @Override 240 public boolean bitmapSetToDefault() { 241 return mDefaultThumbnailSet; 242 } 243 244 public static final class AttachmentPreview implements Parcelable { 245 public String attachmentIdentifier; 246 public Bitmap preview; 247 248 @Override 249 public int describeContents() { 250 return 0; 251 } 252 253 @Override 254 public void writeToParcel(Parcel dest, int flags) { 255 dest.writeString(attachmentIdentifier); 256 dest.writeParcelable(preview, 0); 257 } 258 259 public static final Parcelable.Creator<AttachmentPreview> CREATOR 260 = new Parcelable.Creator<AttachmentPreview>() { 261 @Override 262 public AttachmentPreview createFromParcel(Parcel in) { 263 return new AttachmentPreview(in); 264 } 265 266 @Override 267 public AttachmentPreview[] newArray(int size) { 268 return new AttachmentPreview[size]; 269 } 270 }; 271 272 private AttachmentPreview(Parcel in) { 273 attachmentIdentifier = in.readString(); 274 preview = in.readParcelable(null); 275 } 276 277 public AttachmentPreview(Attachment attachment, Bitmap preview) { 278 this.attachmentIdentifier = attachment.getIdentifierUri().toString(); 279 this.preview = preview; 280 } 281 } 282 283 public interface AttachmentPreviewCache { 284 void set(Attachment attachment, Bitmap preview); 285 Bitmap get(Attachment attachment); 286 } 287 288 @Override 289 public void thumbnailLoadFailed() { 290 setThumbnailToDefault(); 291 } 292 293 protected void setAlwaysShowInfoText(boolean alwaysShowInfoText) { 294 mAlwaysShowInfoText = alwaysShowInfoText; 295 } 296 } 297