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.util.ArrayList;
21import java.util.Collection;
22import java.util.Iterator;
23import java.util.List;
24import java.util.ListIterator;
25
26import org.w3c.dom.events.Event;
27import org.w3c.dom.events.EventListener;
28import org.w3c.dom.smil.ElementTime;
29
30import android.text.TextUtils;
31import android.util.Config;
32import android.util.Log;
33
34import com.android.mms.ContentRestrictionException;
35import com.android.mms.dom.smil.SmilParElementImpl;
36import com.google.android.mms.ContentType;
37
38public class SlideModel extends Model implements List<MediaModel>, EventListener {
39    public static final String TAG = "Mms/slideshow";
40    private static final boolean DEBUG = false;
41    private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV;
42    private static final int DEFAULT_SLIDE_DURATION = 5000;
43
44    private final ArrayList<MediaModel> mMedia = new ArrayList<MediaModel>();
45
46    private MediaModel mText;
47    private MediaModel mImage;
48    private MediaModel mAudio;
49    private MediaModel mVideo;
50
51    private boolean mCanAddImage = true;
52    private boolean mCanAddAudio = true;
53    private boolean mCanAddVideo = true;
54
55    private int mDuration;
56    private boolean mVisible = true;
57    private short mFill;
58    private int mSlideSize;
59    private SlideshowModel mParent;
60
61    public SlideModel(SlideshowModel slideshow) {
62        this(DEFAULT_SLIDE_DURATION, slideshow);
63    }
64
65    public SlideModel(int duration, SlideshowModel slideshow) {
66        mDuration = duration;
67        mParent = slideshow;
68    }
69
70    /**
71     * Create a SlideModel with exist media collection.
72     *
73     * @param duration The duration of the slide.
74     * @param mediaList The exist media collection.
75     *
76     * @throws IllegalStateException One or more media in the mediaList cannot
77     *         be added into the slide due to a slide cannot contain image
78     *         and video or audio and video at the same time.
79     */
80    public SlideModel(int duration, ArrayList<MediaModel> mediaList) {
81        mDuration = duration;
82
83        int maxDur = 0;
84        for (MediaModel media : mediaList) {
85            internalAdd(media);
86
87            int mediaDur = media.getDuration();
88            if (mediaDur > maxDur) {
89                maxDur = mediaDur;
90            }
91        }
92
93        updateDuration(maxDur);
94    }
95
96    private void internalAdd(MediaModel media) throws IllegalStateException {
97        if (media == null) {
98            // Don't add null value into the list.
99            return;
100        }
101
102        if (media.isText()) {
103            String contentType = media.getContentType();
104            if (TextUtils.isEmpty(contentType) || ContentType.TEXT_PLAIN.equals(contentType)
105                    || ContentType.TEXT_HTML.equals(contentType)) {
106                internalAddOrReplace(mText, media);
107                mText = media;
108            } else {
109                Log.w(TAG, "[SlideModel] content type " + media.getContentType() +
110                        " isn't supported (as text)");
111            }
112        } else if (media.isImage()) {
113            if (mCanAddImage) {
114                internalAddOrReplace(mImage, media);
115                mImage = media;
116                mCanAddVideo = false;
117            } else {
118                Log.w(TAG, "[SlideModel] content type " + media.getContentType() +
119                    " - can't add image in this state");
120            }
121        } else if (media.isAudio()) {
122            if (mCanAddAudio) {
123                internalAddOrReplace(mAudio, media);
124                mAudio = media;
125                mCanAddVideo = false;
126            } else {
127                Log.w(TAG, "[SlideModel] content type " + media.getContentType() +
128                    " - can't add audio in this state");
129            }
130        } else if (media.isVideo()) {
131            if (mCanAddVideo) {
132                internalAddOrReplace(mVideo, media);
133                mVideo = media;
134                mCanAddImage = false;
135                mCanAddAudio = false;
136            } else {
137                Log.w(TAG, "[SlideModel] content type " + media.getContentType() +
138                    " - can't add video in this state");
139            }
140        }
141    }
142
143    private void internalAddOrReplace(MediaModel old, MediaModel media) {
144        // If the media is resizable, at this point consider it to be zero length.
145        // Just before we send the slideshow, we take the remaining space in the
146        // slideshow and equally allocate it to all the resizeable media items and resize them.
147        int addSize = media.getMediaResizable() ? 0 : media.getMediaSize();
148        int removeSize;
149        if (old == null) {
150            if (null != mParent) {
151                mParent.checkMessageSize(addSize);
152            }
153            mMedia.add(media);
154            increaseSlideSize(addSize);
155            increaseMessageSize(addSize);
156        } else {
157            removeSize = old.getMediaResizable() ? 0 : old.getMediaSize();
158            if (addSize > removeSize) {
159                if (null != mParent) {
160                    mParent.checkMessageSize(addSize - removeSize);
161                }
162                increaseSlideSize(addSize - removeSize);
163                increaseMessageSize(addSize - removeSize);
164            } else {
165                decreaseSlideSize(removeSize - addSize);
166                decreaseMessageSize(removeSize - addSize);
167            }
168            mMedia.set(mMedia.indexOf(old), media);
169            old.unregisterAllModelChangedObservers();
170        }
171
172        for (IModelChangedObserver observer : mModelChangedObservers) {
173            media.registerModelChangedObserver(observer);
174        }
175    }
176
177    private boolean internalRemove(Object object) {
178        if (mMedia.remove(object)) {
179            if (object instanceof TextModel) {
180                mText = null;
181            } else if (object instanceof ImageModel) {
182                mImage = null;
183                mCanAddVideo = true;
184            } else if (object instanceof AudioModel) {
185                mAudio = null;
186                mCanAddVideo = true;
187            } else if (object instanceof VideoModel) {
188                mVideo = null;
189                mCanAddImage = true;
190                mCanAddAudio = true;
191            }
192            // If the media is resizable, at this point consider it to be zero length.
193            // Just before we send the slideshow, we take the remaining space in the
194            // slideshow and equally allocate it to all the resizeable media items and resize them.
195            int decreaseSize = ((MediaModel) object).getMediaResizable() ? 0
196                                        : ((MediaModel) object).getMediaSize();
197            decreaseSlideSize(decreaseSize);
198            decreaseMessageSize(decreaseSize);
199
200            ((Model) object).unregisterAllModelChangedObservers();
201
202            return true;
203        }
204
205        return false;
206    }
207
208    /**
209     * @return the mDuration
210     */
211    public int getDuration() {
212        return mDuration;
213    }
214
215    /**
216     * @param duration the mDuration to set
217     */
218    public void setDuration(int duration) {
219        mDuration = duration;
220        notifyModelChanged(true);
221    }
222
223    public int getSlideSize() {
224        return mSlideSize;
225    }
226
227    public void increaseSlideSize(int increaseSize) {
228        if (increaseSize > 0) {
229            mSlideSize += increaseSize;
230        }
231    }
232
233    public void decreaseSlideSize(int decreaseSize) {
234        if (decreaseSize > 0) {
235            mSlideSize -= decreaseSize;
236            if (mSlideSize < 0) {
237                mSlideSize = 0;
238            }
239        }
240    }
241
242    public void setParent(SlideshowModel parent) {
243        mParent = parent;
244    }
245
246    public void increaseMessageSize(int increaseSize) {
247        if ((increaseSize > 0) && (null != mParent)) {
248            int size = mParent.getCurrentMessageSize();
249            size += increaseSize;
250            mParent.setCurrentMessageSize(size);
251        }
252    }
253
254    public void decreaseMessageSize(int decreaseSize) {
255        if ((decreaseSize > 0) && (null != mParent)) {
256            int size = mParent.getCurrentMessageSize();
257            size -= decreaseSize;
258            if (size < 0) {
259                size = 0;
260            }
261            mParent.setCurrentMessageSize(size);
262        }
263    }
264
265    //
266    // Implement List<E> interface.
267    //
268
269    /**
270     * Add a MediaModel to the slide. If the slide has already contained
271     * a media object in the same type, the media object will be replaced by
272     * the new one.
273     *
274     * @param object A media object to be added into the slide.
275     * @return true
276     * @throws IllegalStateException One or more media in the mediaList cannot
277     *         be added into the slide due to a slide cannot contain image
278     *         and video or audio and video at the same time.
279     * @throws ContentRestrictionException when can not add this object.
280     *
281     */
282    public boolean add(MediaModel object) {
283        internalAdd(object);
284        notifyModelChanged(true);
285        return true;
286    }
287
288    public boolean addAll(Collection<? extends MediaModel> collection) {
289        throw new UnsupportedOperationException("Operation not supported.");
290    }
291
292    public void clear() {
293        if (mMedia.size() > 0) {
294            for (MediaModel media : mMedia) {
295                media.unregisterAllModelChangedObservers();
296                int decreaseSize = media.getMediaSize();
297                decreaseSlideSize(decreaseSize);
298                decreaseMessageSize(decreaseSize);
299            }
300            mMedia.clear();
301
302            mText = null;
303            mImage = null;
304            mAudio = null;
305            mVideo = null;
306
307            mCanAddImage = true;
308            mCanAddAudio = true;
309            mCanAddVideo = true;
310
311            notifyModelChanged(true);
312        }
313    }
314
315    public boolean contains(Object object) {
316        return mMedia.contains(object);
317    }
318
319    public boolean containsAll(Collection<?> collection) {
320        return mMedia.containsAll(collection);
321    }
322
323    public boolean isEmpty() {
324        return mMedia.isEmpty();
325    }
326
327    public Iterator<MediaModel> iterator() {
328        return mMedia.iterator();
329    }
330
331    public boolean remove(Object object) {
332        if ((object != null) && (object instanceof MediaModel)
333                && internalRemove(object)) {
334            notifyModelChanged(true);
335            return true;
336        }
337        return false;
338    }
339
340    public boolean removeAll(Collection<?> collection) {
341        throw new UnsupportedOperationException("Operation not supported.");
342    }
343
344    public boolean retainAll(Collection<?> collection) {
345        throw new UnsupportedOperationException("Operation not supported.");
346    }
347
348    public int size() {
349        return mMedia.size();
350    }
351
352    public Object[] toArray() {
353        return mMedia.toArray();
354    }
355
356    public <T> T[] toArray(T[] array) {
357        return mMedia.toArray(array);
358    }
359
360    public void add(int location, MediaModel object) {
361        throw new UnsupportedOperationException("Operation not supported.");
362    }
363
364    public boolean addAll(int location,
365            Collection<? extends MediaModel> collection) {
366        throw new UnsupportedOperationException("Operation not supported.");
367    }
368
369    public MediaModel get(int location) {
370        if (mMedia.size() == 0) {
371            return null;
372        }
373
374        return mMedia.get(location);
375    }
376
377    public int indexOf(Object object) {
378        return mMedia.indexOf(object);
379    }
380
381    public int lastIndexOf(Object object) {
382        return mMedia.lastIndexOf(object);
383    }
384
385    public ListIterator<MediaModel> listIterator() {
386        return mMedia.listIterator();
387    }
388
389    public ListIterator<MediaModel> listIterator(int location) {
390        return mMedia.listIterator(location);
391    }
392
393    public MediaModel remove(int location) {
394        MediaModel media = mMedia.get(location);
395        if ((media != null) && internalRemove(media)) {
396            notifyModelChanged(true);
397        }
398        return media;
399    }
400
401    public MediaModel set(int location, MediaModel object) {
402        throw new UnsupportedOperationException("Operation not supported.");
403    }
404
405    public List<MediaModel> subList(int start, int end) {
406        return mMedia.subList(start, end);
407    }
408
409    /**
410     * @return the mVisible
411     */
412    public boolean isVisible() {
413        return mVisible;
414    }
415
416    /**
417     * @param visible the mVisible to set
418     */
419    public void setVisible(boolean visible) {
420        mVisible = visible;
421        notifyModelChanged(true);
422    }
423
424    /**
425     * @return the mFill
426     */
427    public short getFill() {
428        return mFill;
429    }
430
431    /**
432     * @param fill the mFill to set
433     */
434    public void setFill(short fill) {
435        mFill = fill;
436        notifyModelChanged(true);
437    }
438
439    @Override
440    protected void registerModelChangedObserverInDescendants(
441            IModelChangedObserver observer) {
442        for (MediaModel media : mMedia) {
443            media.registerModelChangedObserver(observer);
444        }
445    }
446
447    @Override
448    protected void unregisterModelChangedObserverInDescendants(
449            IModelChangedObserver observer) {
450        for (MediaModel media : mMedia) {
451            media.unregisterModelChangedObserver(observer);
452        }
453    }
454
455    @Override
456    protected void unregisterAllModelChangedObserversInDescendants() {
457        for (MediaModel media : mMedia) {
458            media.unregisterAllModelChangedObservers();
459        }
460    }
461
462    // EventListener Interface
463    public void handleEvent(Event evt) {
464        if (evt.getType().equals(SmilParElementImpl.SMIL_SLIDE_START_EVENT)) {
465            if (LOCAL_LOGV) {
466                Log.v(TAG, "Start to play slide: " + this);
467            }
468            mVisible = true;
469        } else if (mFill != ElementTime.FILL_FREEZE) {
470            if (LOCAL_LOGV) {
471                Log.v(TAG, "Stop playing slide: " + this);
472            }
473            mVisible = false;
474        }
475
476        notifyModelChanged(false);
477    }
478
479    public boolean hasText() {
480        return mText != null;
481    }
482
483    public boolean hasImage() {
484        return mImage != null;
485    }
486
487    public boolean hasAudio() {
488        return mAudio != null;
489    }
490
491    public boolean hasVideo() {
492        return mVideo != null;
493    }
494
495    public boolean removeText() {
496        return remove(mText);
497    }
498
499    public boolean removeImage() {
500        return remove(mImage);
501    }
502
503    public boolean removeAudio() {
504        boolean result = remove(mAudio);
505        resetDuration();
506        return result;
507    }
508
509    public boolean removeVideo() {
510        boolean result = remove(mVideo);
511        resetDuration();
512        return result;
513    }
514
515    public TextModel getText() {
516        return (TextModel) mText;
517    }
518
519    public ImageModel getImage() {
520        return (ImageModel) mImage;
521    }
522
523    public AudioModel getAudio() {
524        return (AudioModel) mAudio;
525    }
526
527    public VideoModel getVideo() {
528        return (VideoModel) mVideo;
529    }
530
531    public void resetDuration() {
532        // If we remove all the objects that have duration, reset the slide back to its
533        // default duration. If we don't do this, if the user replaces a 10 sec video with
534        // a 3 sec audio, the duration will remain at 10 sec (see the way updateDuration() below
535        // works).
536        if (!hasAudio() && !hasVideo()) {
537            mDuration = DEFAULT_SLIDE_DURATION;
538        }
539    }
540
541    public void updateDuration(int duration) {
542        if (duration <= 0) {
543            return;
544        }
545
546        if ((duration > mDuration)
547                || (mDuration == DEFAULT_SLIDE_DURATION)) {
548            mDuration = duration;
549        }
550    }
551}
552