1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.cooliris.media;
18
19import java.util.ArrayList;
20
21public class MediaSet {
22    public static final int TYPE_SMART = 0;
23    public static final int TYPE_FOLDER = 1;
24    public static final int TYPE_USERDEFINED = 2;
25
26    public long mId;
27    public String mName;
28
29    public boolean mFlagForDelete;
30
31    public boolean mHasImages;
32    public boolean mHasVideos;
33
34    // The type of the media set. A smart media set is an automatically
35    // generated media set. For example, the most recently
36    // viewed items media set is a media set that gets populated by the contents
37    // of a folder. A user defined media set
38    // is a set that is made by the user. This would typically correspond to
39    // media items belonging to an event.
40    public int mType;
41
42    // The min and max date taken and added at timestamps.
43    public long mMinTimestamp = Long.MAX_VALUE;
44    public long mMaxTimestamp = 0;
45    public long mMinAddedTimestamp = Long.MAX_VALUE;
46    public long mMaxAddedTimestamp = 0;
47
48    // The latitude and longitude of the min latitude point.
49    public double mMinLatLatitude = LocationMediaFilter.LAT_MAX;
50    public double mMinLatLongitude;
51    // The latitude and longitude of the max latitude point.
52    public double mMaxLatLatitude = LocationMediaFilter.LAT_MIN;
53    public double mMaxLatLongitude;
54    // The latitude and longitude of the min longitude point.
55    public double mMinLonLatitude;
56    public double mMinLonLongitude = LocationMediaFilter.LON_MAX;
57    // The latitude and longitude of the max longitude point.
58    public double mMaxLonLatitude;
59    public double mMaxLonLongitude = LocationMediaFilter.LON_MIN;
60
61    // Reverse geocoding the latitude, longitude and getting an address or
62    // location.
63    public String mReverseGeocodedLocation;
64    // Set to true if at least one item in the set has a valid latitude and
65    // longitude.
66    public boolean mLatLongDetermined = false;
67    public boolean mReverseGeocodedLocationComputed = false;
68    public boolean mReverseGeocodedLocationRequestMade = false;
69
70    public String mTitleString;
71    public String mTruncTitleString;
72    public String mNoCountTitleString;
73
74    public String mEditUri = null;
75    public long mPicasaAlbumId = Shared.INVALID;
76    public boolean mIsLocal = true;
77
78    public DataSource mDataSource;
79    public boolean mSyncPending = false;
80
81    private ArrayList<MediaItem> mItems;
82    private LongSparseArray<MediaItem> mItemsLookup;
83    private LongSparseArray<MediaItem> mItemsLookupVideo;
84    public int mNumItemsLoaded = 0;
85    // mNumExpectedItems is preset to how many items are expected to be in the
86    // set as it is used to visually
87    // display the number of items in the set and we don't want this display to
88    // keep changing as items get loaded.
89    private int mNumExpectedItems = 0;
90    private boolean mNumExpectedItemsCountAccurate = false;
91    private int mCurrentLocation = 0;
92
93    public MediaSet() {
94        this(null);
95    }
96
97    public MediaSet(DataSource dataSource) {
98        mItems = new ArrayList<MediaItem>(16);
99        mItemsLookup = new LongSparseArray<MediaItem>();
100        mItemsLookup.clear();
101        mItemsLookupVideo = new LongSparseArray<MediaItem>();
102        mItemsLookupVideo.clear();
103        mDataSource = dataSource;
104        // TODO(Venkat): Can we move away from this dummy item setup?
105        MediaItem item = new MediaItem();
106        item.mId = Shared.INVALID;
107        item.mParentMediaSet = this;
108        mItems.add(item);
109        mNumExpectedItems = 16;
110    }
111
112    /**
113     * @return underlying ArrayList of MediaItems. Use only for iteration (read
114     *         operations) on the ArrayList.
115     */
116    public ArrayList<MediaItem> getItems() {
117        return mItems;
118    }
119
120    public void setNumExpectedItems(int numExpectedItems) {
121        mItems.ensureCapacity(numExpectedItems);
122        mNumExpectedItems = numExpectedItems;
123        mNumExpectedItemsCountAccurate = true;
124    }
125
126    public int getNumExpectedItems() {
127        return mNumExpectedItems;
128    }
129
130    public boolean setContainsValidItems() {
131        if (mNumExpectedItems == 0)
132            return false;
133        return true;
134    }
135
136    public void updateNumExpectedItems() {
137        mNumExpectedItems = mNumItemsLoaded;
138        mNumExpectedItemsCountAccurate = true;
139    }
140
141    public int getNumItems() {
142        return mItems.size();
143    }
144
145    public void clear() {
146        mItems.clear();
147        MediaItem item = new MediaItem();
148        item.mId = Shared.INVALID;
149        item.mParentMediaSet = this;
150        mItems.add(item);
151        mNumExpectedItems = 16;
152        refresh();
153        mItemsLookup.clear();
154        mItemsLookupVideo.clear();
155    }
156
157    /**
158     * Generates the label for the MediaSet.
159     */
160    public void generateTitle(final boolean truncateTitle) {
161        if (mName == null) {
162            mName = "";
163        }
164        String size = (mNumExpectedItemsCountAccurate) ? "  (" + mNumExpectedItems + ")" : "";
165        mTitleString = mName + size;
166        if (truncateTitle) {
167            int length = mName.length();
168            mTruncTitleString = (length > 16) ? mName.substring(0, 12) + "..." + mName.substring(length - 4, length) + size : mName
169                    + size;
170            mNoCountTitleString = mName;
171        } else {
172            mTruncTitleString = mTitleString;
173        }
174    }
175
176    /**
177     * Adds a MediaItem to this set, and increments the load count.
178     * Additionally, it also recomputes the location bounds and time range of
179     * the media set.
180     */
181    public void addItem(final MediaItem itemToAdd) {
182        // Important to not set the parentMediaSet in here as temporary
183        // MediaSet's are occasionally
184        // created and we do not want the MediaItem updated as a result of that.
185        if (itemToAdd == null) {
186            return;
187        }
188        final LongSparseArray<MediaItem> lookup = (itemToAdd.getMediaType() == MediaItem.MEDIA_TYPE_IMAGE) ? mItemsLookup
189                : mItemsLookupVideo;
190        MediaItem lookupItem = lookup.get(itemToAdd.mId);
191        if (lookupItem != null && !lookupItem.mFilePath.equals(itemToAdd.mFilePath)) {
192            lookupItem = null;
193        }
194        final MediaItem item = (lookupItem == null) ? itemToAdd : lookupItem;
195        item.mFlagForDelete = false;
196        if (mItems.size() == 0) {
197            mItems.add(item);
198        } else if (mItems.get(0).mId == -1L) {
199            mItems.set(0, item);
200        } else {
201            if (mItems.size() > mCurrentLocation) {
202                mItems.set(mCurrentLocation, item);
203            } else {
204                mItems.add(mCurrentLocation, item);
205            }
206        }
207        if (item.mId != Shared.INVALID) {
208            if (lookupItem == null) {
209                lookup.put(item.mId, item);
210            }
211            ++mNumItemsLoaded;
212            ++mCurrentLocation;
213        }
214        if (item.isDateTakenValid()) {
215            long dateTaken = item.mDateTakenInMs;
216            if (dateTaken < mMinTimestamp) {
217                mMinTimestamp = dateTaken;
218            }
219            if (dateTaken > mMaxTimestamp) {
220                mMaxTimestamp = dateTaken;
221            }
222        } else if (item.isDateAddedValid()) {
223            long dateAdded = item.mDateAddedInSec * 1000;
224            if (dateAdded < mMinAddedTimestamp) {
225                mMinAddedTimestamp = dateAdded;
226            }
227            if (dateAdded > mMaxAddedTimestamp) {
228                mMaxAddedTimestamp = dateAdded;
229            }
230        }
231
232        // Determining the latitude longitude bounds of the set and setting the
233        // location string.
234        if (!item.isLatLongValid()) {
235            return;
236        }
237        double itemLatitude = item.mLatitude;
238        double itemLongitude = item.mLongitude;
239        if (mMinLatLatitude > itemLatitude) {
240            mMinLatLatitude = itemLatitude;
241            mMinLatLongitude = itemLongitude;
242            mLatLongDetermined = true;
243        }
244        if (mMaxLatLatitude < itemLatitude) {
245            mMaxLatLatitude = itemLatitude;
246            mMaxLatLongitude = itemLongitude;
247            mLatLongDetermined = true;
248        }
249        if (mMinLonLongitude > itemLongitude) {
250            mMinLonLatitude = itemLatitude;
251            mMinLonLongitude = itemLongitude;
252            mLatLongDetermined = true;
253        }
254        if (mMaxLonLongitude < itemLongitude) {
255            mMaxLonLatitude = itemLatitude;
256            mMaxLonLongitude = itemLongitude;
257            mLatLongDetermined = true;
258        }
259    }
260
261    /**
262     * Removes a MediaItem if present in the MediaSet.
263     *
264     * @return true if the item was removed, false if removal failed or item was
265     *         not present in the set.
266     */
267    public boolean removeItem(final MediaItem itemToRemove) {
268        synchronized (mItems) {
269            if (mItems.remove(itemToRemove)) {
270                --mNumExpectedItems;
271                --mNumItemsLoaded;
272                --mCurrentLocation;
273                final LongSparseArray<MediaItem> lookup = (itemToRemove.getMediaType() == MediaItem.MEDIA_TYPE_IMAGE) ? mItemsLookup
274                        : mItemsLookupVideo;
275                lookup.remove(itemToRemove.mId);
276                return true;
277            }
278            return false;
279        }
280    }
281
282    public void removeDuplicate(final MediaItem itemToRemove) {
283        synchronized (mItems) {
284            int numItems = mItems.size();
285            boolean foundItem = false;
286            for (int i = 0; i < numItems; ++i) {
287                final MediaItem item = mItems.get(i);
288                if (item == itemToRemove) {
289                    if (foundItem == false) {
290                        foundItem = true;
291                    } else {
292                        mItems.remove(i);
293                        --mNumExpectedItems;
294                        --mNumItemsLoaded;
295                        --mCurrentLocation;
296                        break;
297                    }
298                }
299            }
300        }
301    }
302
303    /**
304     * @return true if this MediaSet contains the argument MediaItem.
305     */
306    public boolean lookupContainsItem(final MediaItem item) {
307        final LongSparseArray<MediaItem> lookupTable = (item.getMediaType() == MediaItem.MEDIA_TYPE_IMAGE) ? mItemsLookup
308                : mItemsLookupVideo;
309        MediaItem lookUp = lookupTable.get(item.mId);
310        if (lookUp != null && lookUp.mFilePath.equals(item.mFilePath)) {
311            return true;
312        } else {
313            return false;
314        }
315    }
316
317    /**
318     * @return true if the title string is truncated.
319     */
320    public boolean isTruncated() {
321        return (mTitleString != null && !mTitleString.equals(mTruncTitleString));
322    }
323
324    /**
325     * @return true if timestamps are available for this set.
326     */
327    public boolean areTimestampsAvailable() {
328        return (mMinTimestamp < Long.MAX_VALUE && mMaxTimestamp > 0);
329    }
330
331    /**
332     * @return true if the added timestamps are available for this set.
333     */
334    public boolean areAddedTimestampsAvailable() {
335        return (mMinAddedTimestamp < Long.MAX_VALUE && mMaxAddedTimestamp > 0);
336    }
337
338    /**
339     * @return true if this set of items corresponds to Picassa items.
340     */
341    public boolean isPicassaSet() {
342        // 2 cases:-
343        // 1. This set is just a Picassa Album, and all its items are therefore
344        // from Picassa.
345        // 2. This set is a random collection of items and each item is a
346        // Picassa item.
347        if (isPicassaAlbum()) {
348            return true;
349        }
350        int numItems = mItems.size();
351        for (int i = 0; i < numItems; i++) {
352            if (!mItems.get(i).isPicassaItem()) {
353                return false;
354            }
355        }
356        return true;
357    }
358
359    /**
360     * @return true if this set is a Picassa album.
361     */
362    public boolean isPicassaAlbum() {
363        return (mPicasaAlbumId != Shared.INVALID);
364    }
365
366    public void refresh() {
367        mNumItemsLoaded = 0;
368        mCurrentLocation = 0;
369        final ArrayList<MediaItem> items = mItems;
370        final int numItems = items.size();
371        for (int i = 0; i < numItems; ++i) {
372            MediaItem item = items.get(i);
373            item.mFlagForDelete = true;
374        }
375    }
376
377    public void checkForDeletedItems() {
378        final ArrayList<MediaItem> items = mItems;
379        final ArrayList<MediaItem> itemsToDelete = new ArrayList<MediaItem>();
380        synchronized (items) {
381            final int numItems = items.size();
382            for (int i = 0; i < numItems; ++i) {
383                MediaItem item = items.get(i);
384                if (item.mFlagForDelete) {
385                    itemsToDelete.add(item);
386                }
387            }
388        }
389        final int numItemsToDelete = itemsToDelete.size();
390        for (int i = 0; i < numItemsToDelete; ++i) {
391            removeItem(itemsToDelete.get(i));
392        }
393    }
394}
395