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.ui;
19
20import android.content.ContentResolver;
21import android.content.Context;
22import android.graphics.Bitmap;
23import android.os.Parcel;
24import android.os.Parcelable;
25import android.text.TextUtils;
26import android.util.AttributeSet;
27import android.util.DisplayMetrics;
28import android.view.View;
29import android.widget.ImageView;
30import android.widget.ImageView.ScaleType;
31import android.widget.RelativeLayout;
32import android.widget.TextView;
33
34import com.android.ex.photo.util.ImageUtils;
35import com.android.mail.R;
36import com.android.mail.providers.Attachment;
37import com.android.mail.utils.AttachmentUtils;
38import com.android.mail.utils.LogTag;
39import 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 */
45public 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