1/*
2 * Copyright (C) 2008 Esmertec AG.
3 * Copyright (C) 2008 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.mms.model;
19
20import java.lang.ref.SoftReference;
21import java.util.Arrays;
22import java.util.HashSet;
23import java.util.Set;
24
25import org.w3c.dom.events.Event;
26import org.w3c.dom.smil.ElementTime;
27
28import android.content.Context;
29import android.graphics.Bitmap;
30import android.graphics.BitmapFactory;
31import android.net.Uri;
32import android.text.TextUtils;
33import android.util.Log;
34
35import com.android.mms.ContentRestrictionException;
36import com.android.mms.ExceedMessageSizeException;
37import com.android.mms.LogTag;
38import com.android.mms.MmsApp;
39import com.android.mms.MmsConfig;
40import com.android.mms.dom.smil.SmilMediaElementImpl;
41import com.android.mms.ui.UriImage;
42import com.android.mms.util.ItemLoadedCallback;
43import com.android.mms.util.ItemLoadedFuture;
44import com.android.mms.util.ThumbnailManager;
45import com.google.android.mms.MmsException;
46import com.google.android.mms.pdu.PduPart;
47import com.google.android.mms.pdu.PduPersister;
48
49
50public class ImageModel extends RegionMediaModel {
51    private static final String TAG = "Mms/image";
52    private static final boolean DEBUG = false;
53    private static final boolean LOCAL_LOGV = false;
54
55    private static final int PICTURE_SIZE_LIMIT = 100 * 1024;
56
57    /**
58     * These are the image content types that MMS supports. Anything else needs to be transcoded
59     * into one of these content types before being sent over MMS.
60     */
61    private static final Set<String> SUPPORTED_MMS_IMAGE_CONTENT_TYPES =
62        new HashSet<String>(Arrays.asList(new String[] {
63                "image/jpeg",
64            }));
65
66    private int mWidth;
67    private int mHeight;
68    private SoftReference<Bitmap> mFullSizeBitmapCache = new SoftReference<Bitmap>(null);
69    private ItemLoadedFuture mItemLoadedFuture;
70
71    public ImageModel(Context context, Uri uri, RegionModel region)
72            throws MmsException {
73        super(context, SmilHelper.ELEMENT_TAG_IMAGE, uri, region);
74        initModelFromUri(uri);
75        checkContentRestriction();
76    }
77
78    public ImageModel(Context context, String contentType, String src,
79            Uri uri, RegionModel region) throws MmsException {
80        super(context, SmilHelper.ELEMENT_TAG_IMAGE,
81                contentType, src, uri, region);
82        decodeImageBounds(uri);
83    }
84
85    private void initModelFromUri(Uri uri) throws MmsException {
86        UriImage uriImage = new UriImage(mContext, uri);
87
88        mContentType = uriImage.getContentType();
89        if (TextUtils.isEmpty(mContentType)) {
90            throw new MmsException("Type of media is unknown.");
91        }
92        mSrc = uriImage.getSrc();
93        mWidth = uriImage.getWidth();
94        mHeight = uriImage.getHeight();
95
96        if (LOCAL_LOGV) {
97            Log.v(TAG, "New ImageModel created:"
98                    + " mSrc=" + mSrc
99                    + " mContentType=" + mContentType
100                    + " mUri=" + uri);
101        }
102    }
103
104    private void decodeImageBounds(Uri uri) {
105        UriImage uriImage = new UriImage(mContext, uri);
106        mWidth = uriImage.getWidth();
107        mHeight = uriImage.getHeight();
108
109        if (LOCAL_LOGV) {
110            Log.v(TAG, "Image bounds: " + mWidth + "x" + mHeight);
111        }
112    }
113
114    // EventListener Interface
115    @Override
116    public void handleEvent(Event evt) {
117        if (evt.getType().equals(SmilMediaElementImpl.SMIL_MEDIA_START_EVENT)) {
118            mVisible = true;
119        } else if (mFill != ElementTime.FILL_FREEZE) {
120            mVisible = false;
121        }
122
123        notifyModelChanged(false);
124    }
125
126    public int getWidth() {
127        return mWidth;
128    }
129
130    public int getHeight() {
131        return mHeight;
132    }
133
134    protected void checkContentRestriction() throws ContentRestrictionException {
135        ContentRestriction cr = ContentRestrictionFactory.getContentRestriction();
136        cr.checkImageContentType(mContentType);
137    }
138
139    public ItemLoadedFuture loadThumbnailBitmap(ItemLoadedCallback callback) {
140        ThumbnailManager thumbnailManager = MmsApp.getApplication().getThumbnailManager();
141        mItemLoadedFuture = thumbnailManager.getThumbnail(getUri(), callback);
142        return mItemLoadedFuture;
143    }
144
145    public void cancelThumbnailLoading() {
146        if (mItemLoadedFuture != null && !mItemLoadedFuture.isDone()) {
147            if (Log.isLoggable(LogTag.APP, Log.DEBUG)) {
148                Log.v(TAG, "cancelThumbnailLoading for: " + this);
149            }
150            mItemLoadedFuture.cancel(getUri());
151            mItemLoadedFuture = null;
152        }
153    }
154
155    private Bitmap createBitmap(int thumbnailBoundsLimit, Uri uri) {
156        byte[] data = UriImage.getResizedImageData(mWidth, mHeight,
157                thumbnailBoundsLimit, thumbnailBoundsLimit, PICTURE_SIZE_LIMIT, uri, mContext);
158        if (LOCAL_LOGV) {
159            Log.v(TAG, "createBitmap size: " + (data == null ? data : data.length));
160        }
161        return data == null ? null : BitmapFactory.decodeByteArray(data, 0, data.length);
162    }
163
164    public Bitmap getBitmap(int width, int height)  {
165        Bitmap bm = mFullSizeBitmapCache.get();
166        if (bm == null) {
167            try {
168                bm = createBitmap(Math.max(width, height), getUri());
169                if (bm != null) {
170                    mFullSizeBitmapCache = new SoftReference<Bitmap>(bm);
171                }
172            } catch (OutOfMemoryError ex) {
173                // fall through and return a null bitmap. The callers can handle a null
174                // result and show R.drawable.ic_missing_thumbnail_picture
175            }
176        }
177        return bm;
178    }
179
180    @Override
181    public boolean getMediaResizable() {
182        return true;
183    }
184
185    @Override
186    protected void resizeMedia(int byteLimit, long messageId) throws MmsException {
187        UriImage image = new UriImage(mContext, getUri());
188
189        int widthLimit = MmsConfig.getMaxImageWidth();
190        int heightLimit = MmsConfig.getMaxImageHeight();
191        int size = getMediaSize();
192        // In mms_config.xml, the max width has always been declared larger than the max height.
193        // Swap the width and height limits if necessary so we scale the picture as little as
194        // possible.
195        if (image.getHeight() > image.getWidth()) {
196            int temp = widthLimit;
197            widthLimit = heightLimit;
198            heightLimit = temp;
199        }
200
201        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
202            Log.v(TAG, "resizeMedia size: " + size + " image.getWidth(): "
203                    + image.getWidth() + " widthLimit: " + widthLimit
204                    + " image.getHeight(): " + image.getHeight()
205                    + " heightLimit: " + heightLimit
206                    + " image.getContentType(): " + image.getContentType());
207        }
208
209        // Check if we're already within the limits - in which case we don't need to resize.
210        // The size can be zero here, even when the media has content. See the comment in
211        // MediaModel.initMediaSize. Sometimes it'll compute zero and it's costly to read the
212        // whole stream to compute the size. When we call getResizedImageAsPart(), we'll correctly
213        // set the size.
214        if (size != 0 && size <= byteLimit &&
215                image.getWidth() <= widthLimit &&
216                image.getHeight() <= heightLimit &&
217                SUPPORTED_MMS_IMAGE_CONTENT_TYPES.contains(image.getContentType())) {
218            if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
219                Log.v(TAG, "resizeMedia - already sized");
220            }
221            return;
222        }
223
224        PduPart part = image.getResizedImageAsPart(
225                widthLimit,
226                heightLimit,
227                byteLimit);
228
229        if (part == null) {
230            throw new ExceedMessageSizeException("Not enough memory to turn image into part: " +
231                    getUri());
232        }
233
234        // Update the content type because it may have changed due to resizing/recompressing
235        mContentType = new String(part.getContentType());
236
237        String src = getSrc();
238        byte[] srcBytes = src.getBytes();
239        part.setContentLocation(srcBytes);
240        int period = src.lastIndexOf(".");
241        byte[] contentId = period != -1 ? src.substring(0, period).getBytes() : srcBytes;
242        part.setContentId(contentId);
243
244        PduPersister persister = PduPersister.getPduPersister(mContext);
245        this.mSize = part.getData().length;
246
247        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
248            Log.v(TAG, "resizeMedia mSize: " + mSize);
249        }
250
251        Uri newUri = persister.persistPart(part, messageId, null);
252        setUri(newUri);
253    }
254}
255