1 /***
2 * Ambient - A music player for the Android platform
3 Copyright (C) 2007 Martin Vysny
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18 package sk.baka.ambient.collection.local;
19
20 import java.util.Collections;
21 import java.util.LinkedList;
22 import java.util.List;
23
24 import sk.baka.ambient.collection.AbstractAudio;
25 import sk.baka.ambient.collection.CollectionUtils;
26 import sk.baka.ambient.collection.IDynamicPlaylistTrackProvider;
27 import sk.baka.ambient.collection.TrackMetadataBean;
28 import sk.baka.ambient.playlist.Random;
29 import android.database.Cursor;
30
31 /***
32 * <p>
33 * Provides random mix of tracks from the entire library.
34 * </p>
35 * <p>
36 * <i>Implementation note</i>: this object causes a database {@link Cursor} to
37 * be opened for a long time - not probably a good strategy.
38 * </p>
39 *
40 * @author Martin Vysny
41 */
42 public abstract class AbstractTrackProvider implements IDynamicPlaylistTrackProvider {
43
44 private static final long serialVersionUID = 8828698373273796738L;
45
46 /***
47 * Enumerates album names (or full tracks if the {@link Random#TRACK} is
48 * selected) to be played.
49 */
50 private transient Cursor cursor;
51
52 /***
53 * The track ordering.
54 */
55 private Random random = Random.TRACK;
56
57 /***
58 * Returns current random value.
59 * @return the value of the playback order.
60 */
61 protected final Random getRandom() {
62 return random;
63 }
64
65 /***
66 * Creates a new provider. Gets all files from the library.
67 *
68 * @param random
69 * The track ordering.
70 */
71 protected AbstractTrackProvider(final Random random) {
72 super();
73 this.random = random;
74 }
75
76 public void reset() {
77 reinit();
78 }
79
80 public void removeFromHistory(int trackCount) {
81
82 }
83
84 public void setRandom(final Random random, final TrackMetadataBean track) {
85 if (this.random == random) {
86 return;
87 }
88 this.random = random;
89 reinit();
90 if (random.groupsByAlbum()) {
91 if (track != null) {
92
93 final List<TrackMetadataBean> list = getAlbumTracksInternal(track
94 .getAlbum());
95 final int index = list.indexOf(track);
96 list.subList(0, index + 1).clear();
97 cache = new LinkedList<TrackMetadataBean>(list);
98 }
99 }
100 }
101
102 private void reinit() {
103 cache = null;
104 invalidateCursor();
105 }
106
107 public boolean hasNext() {
108 fillCache();
109 return !cache.isEmpty();
110 }
111
112 public TrackMetadataBean next() {
113 fillCache();
114 final TrackMetadataBean result = cache.removeFirst();
115 return result;
116 }
117
118 public void remove() {
119 throw new UnsupportedOperationException("unsupported");
120 }
121
122 /***
123 * Upcoming tracks cache.
124 */
125 private transient LinkedList<TrackMetadataBean> cache;
126
127 /***
128 * Manages upcoming track cache and fetches new tracks from the cursor
129 * on-demand.
130 */
131 private void fillCache() {
132 if (cache == null) {
133 cache = new LinkedList<TrackMetadataBean>();
134 }
135 if (cache.isEmpty()) {
136
137 cache.addAll(fetchMoreTracks(true, true));
138 }
139 }
140
141 public void close() {
142 invalidateCursor();
143 }
144
145 private void invalidateCursor() {
146 if (cursor != null) {
147 cursor.close();
148 cursor = null;
149 }
150 }
151
152 /***
153 * Fetches more tracks from the database.
154 *
155 * @param rewindOnEnd
156 * if <code>true</code> and there are no more tracks to play in
157 * the {@link #cursor}, the cursor is closed and the DB is
158 * re-queried. Note that the DB is reqeried at most once.
159 * @param rewindOnMissing
160 * if <code>true</code> and we have found an non-existant track
161 * (this can happen when SERIALIZABLE transaction isolation level
162 * is specified), the cursor is closed and the DB is re-queried.
163 * Note that the DB is reqeried at most once.
164 * @return a list of tracks. May be <code>null</code> if there are no
165 * tracks left.
166 */
167 private List<TrackMetadataBean> fetchMoreTracks(final boolean rewindOnEnd,
168 final boolean rewindOnMissing) {
169 boolean requeried = false;
170 getCursor();
171 if (!cursor.moveToNext()) {
172 if (!rewindOnEnd)
173 return null;
174 recreateCursor();
175 requeried = true;
176 if (!cursor.moveToNext())
177 return null;
178 }
179 final List<TrackMetadataBean> result;
180 if (random == Random.TRACK) {
181
182 final TrackMetadataBean track = getTrackFromCursor(cursor);
183 result = Collections.singletonList(track);
184 } else {
185 final String album = cursor.getString(0);
186
187 result = getAlbumTracksInternal(album);
188 }
189
190 for (final TrackMetadataBean track : result) {
191 final boolean isPresent = AbstractAudio
192 .fromUri(track.getLocation()).exists();
193 if (!isPresent) {
194 if (requeried || !rewindOnMissing) {
195
196 return result;
197 }
198
199 recreateCursor();
200 return fetchMoreTracks(false, false);
201 }
202 }
203 return result;
204 }
205
206 /***
207 * Fetches all tracks for given album. The tracks are sorted depending on
208 * the value of {@link #random}.
209 *
210 * @param album
211 * the album name
212 * @return track list, never <code>null</code>, may be empty if no such
213 * album exists.
214 */
215 private List<TrackMetadataBean> getAlbumTracksInternal(final String album) {
216 final List<TrackMetadataBean> result = getAlbumTracks(album);
217 if ((random == Random.ALBUM_TRACK) || (random == Random.TRACK)) {
218 Collections.shuffle(result);
219 } else {
220 CollectionUtils.sortByAlbumOrder(result);
221 }
222 return result;
223 }
224
225 /***
226 * Fetches all tracks for given album.
227 * @param album the album to fetch.
228 * @return track list, never <code>null</code>, may be empty if no such
229 * album exists. Not required to be sorted.
230 */
231 protected abstract List<TrackMetadataBean> getAlbumTracks(final String album);
232
233 /***
234 * Re-creates the cursor.
235 */
236 private void recreateCursor() {
237 invalidateCursor();
238 getCursor();
239 }
240
241 /***
242 * Returns the cursor, creating it if necessary.
243 *
244 * @return the cursor instance.
245 */
246 private Cursor getCursor() {
247 if (cursor == null) {
248 cursor = newCursor(random != Random.TRACK);
249 }
250 return cursor;
251 }
252
253 /***
254 * Creates new cursor, depending on the value of <code>albums</code>
255 * argument.
256 *
257 * @param albums
258 * if <code>true</code> then the cursor must provide string
259 * album names only, sorted by a random order. If
260 * <code>false</code> then the cursor must provide tracks
261 * sorted by random order.
262 * @return cursor, not <code>null</code>.
263 */
264 protected abstract Cursor newCursor(final boolean albums);
265
266 /***
267 * Converts current row to a track bean. The cursor was provided by
268 * {@link #newCursor(boolean)} invoked with <code>false</code>.
269 *
270 * @param c
271 * the cursor, never <code>null</code>, always on a valid
272 * line.
273 * @return non-<code>null</code> track instance.
274 */
275 protected abstract TrackMetadataBean getTrackFromCursor(final Cursor c);
276 }