View Javadoc

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  		// do nothing
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  				// try to fetch album containing given track
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 			// try to fetch some tracks
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 			// fetch a single track
182 			final TrackMetadataBean track = getTrackFromCursor(cursor);
183 			result = Collections.singletonList(track);
184 		} else {
185 			final String album = cursor.getString(0);
186 			// fetch all tracks from given album
187 			result = getAlbumTracksInternal(album);
188 		}
189 		// tracks fetched. check if they are present on the filesystem
190 		for (final TrackMetadataBean track : result) {
191 			final boolean isPresent = AbstractAudio
192 					.fromUri(track.getLocation()).exists();
193 			if (!isPresent) {
194 				if (requeried || !rewindOnMissing) {
195 					// probably an outdated collection. ignore it and bail out.
196 					return result;
197 				}
198 				// try again
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 }