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(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(this, attachment, prevAttachment); 140 } 141 142 private void updateSubtitleText() { 143 // TODO: make this a formatted resource when we have a UX design. 144 // not worth translation right now. 145 StringBuilder sb = new StringBuilder(); 146 sb.append(mAttachmentSizeText); 147 if (mDisplayType != null) { 148 sb.append(' '); 149 sb.append(mDisplayType); 150 } 151 mSubtitle.setText(sb.toString()); 152 } 153 154 @Override 155 public void setThumbnailToDefault() { 156 Bitmap cachedPreview = mAttachmentPreviewCache.get(mAttachment); 157 if (cachedPreview != null) { 158 setThumbnail(cachedPreview); 159 return; 160 } 161 mDefaultIcon.setVisibility(View.VISIBLE); 162 mTitle.setVisibility(View.VISIBLE); 163 mSubtitle.setVisibility(View.VISIBLE); 164 mDefaultThumbnailSet = true; 165 } 166 167 @Override 168 public void setThumbnail(Bitmap result) { 169 if (result == null) { 170 return; 171 } 172 173 // We got a real thumbnail; hide the default thumbnail. 174 mDefaultIcon.setVisibility(View.GONE); 175 if (!mAlwaysShowInfoText) { 176 mTitle.setVisibility(View.GONE); 177 mSubtitle.setVisibility(View.GONE); 178 } 179 180 final int maxSize = getResources().getInteger(R.integer.attachment_preview_max_size); 181 final int width = result.getWidth(); 182 final int height = result.getHeight(); 183 final int scaledWidth = width * getResources().getDisplayMetrics().densityDpi 184 / DisplayMetrics.DENSITY_DEFAULT; 185 final int scaledHeight = height * getResources().getDisplayMetrics().densityDpi 186 / DisplayMetrics.DENSITY_DEFAULT; 187 // ratio of the image 188 final float ratio = Math.min((float) width / height, (float) height / width); 189 190 final boolean large = width >= maxSize || scaledWidth >= mIcon.getWidth() 191 || height >= maxSize || scaledHeight >= mIcon.getHeight(); 192 final boolean skinny = 193 // the image is loooong 194 ratio < skinnyThresholdRatio && 195 // AND if the image was centered and cropped, the resulting 196 // image would still be loooong 197 !(scaledWidth >= mIcon.getHeight() * skinnyThresholdRatio 198 && scaledHeight >= mIcon.getWidth() * skinnyThresholdRatio); 199 LogUtils.d(LOG_TAG, "scaledWidth: %d, scaledHeight: %d, large: %b, skinny: %b", scaledWidth, 200 scaledHeight, large, skinny); 201 202 if (large) { 203 // preview fills up at least 1 dimension 204 if (skinny) { 205 // just center. The shorter dimension stays the same while the 206 // longer dimension is cropped 207 mIcon.setScaleType(ScaleType.CENTER); 208 } else { 209 // fill. Both dimensions are scaled to fill the box, the longer 210 // dimension is cropped 211 mIcon.setScaleType(ScaleType.CENTER_CROP); 212 } 213 } else { 214 // preview is small. just center 215 mIcon.setScaleType(ScaleType.CENTER); 216 } 217 218 mIcon.setImageBitmap(result); 219 mAttachmentPreviewCache.set(mAttachment, result); 220 mDefaultThumbnailSet = false; 221 } 222 223 @Override 224 public int getThumbnailWidth() { 225 return mIcon.getWidth(); 226 } 227 228 @Override 229 public int getThumbnailHeight() { 230 return mIcon.getHeight(); 231 } 232 233 @Override 234 public ContentResolver getResolver() { 235 return getContext().getContentResolver(); 236 } 237 238 @Override 239 public boolean bitmapSetToDefault() { 240 return mDefaultThumbnailSet; 241 } 242 243 public static final class AttachmentPreview implements Parcelable { 244 public String attachmentIdentifier; 245 public Bitmap preview; 246 247 @Override 248 public int describeContents() { 249 return 0; 250 } 251 252 @Override 253 public void writeToParcel(Parcel dest, int flags) { 254 dest.writeString(attachmentIdentifier); 255 dest.writeParcelable(preview, 0); 256 } 257 258 public static final Parcelable.Creator<AttachmentPreview> CREATOR 259 = new Parcelable.Creator<AttachmentPreview>() { 260 @Override 261 public AttachmentPreview createFromParcel(Parcel in) { 262 return new AttachmentPreview(in); 263 } 264 265 @Override 266 public AttachmentPreview[] newArray(int size) { 267 return new AttachmentPreview[size]; 268 } 269 }; 270 271 private AttachmentPreview(Parcel in) { 272 attachmentIdentifier = in.readString(); 273 preview = in.readParcelable(null); 274 } 275 276 public AttachmentPreview(Attachment attachment, Bitmap preview) { 277 this.attachmentIdentifier = attachment.getIdentifierUri().toString(); 278 this.preview = preview; 279 } 280 } 281 282 public interface AttachmentPreviewCache { 283 void set(Attachment attachment, Bitmap preview); 284 Bitmap get(Attachment attachment); 285 } 286 287 @Override 288 public void thumbnailLoadFailed() { 289 setThumbnailToDefault(); 290 } 291 292 protected void setAlwaysShowInfoText(boolean alwaysShowInfoText) { 293 mAlwaysShowInfoText = alwaysShowInfoText; 294 } 295 } 296