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  
19  package sk.baka.ambient.collection.local;
20  
21  import java.util.ArrayList;
22  import java.util.Collection;
23  import java.util.EnumMap;
24  import java.util.HashSet;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Set;
28  
29  import junit.framework.Assert;
30  import sk.baka.ambient.collection.CategoryEnum;
31  import sk.baka.ambient.collection.CategoryItem;
32  import sk.baka.ambient.collection.CollectionException;
33  import sk.baka.ambient.collection.CollectionUtils;
34  import sk.baka.ambient.collection.ICollection;
35  import sk.baka.ambient.collection.IDynamicPlaylistTrackProvider;
36  import sk.baka.ambient.collection.Statistics;
37  import sk.baka.ambient.collection.TrackMetadataBean;
38  import sk.baka.ambient.collection.TrackOriginEnum;
39  import sk.baka.ambient.playlist.Random;
40  import android.content.ContentResolver;
41  import android.database.Cursor;
42  import android.net.Uri;
43  import android.provider.BaseColumns;
44  import android.provider.MediaStore;
45  import android.provider.MediaStore.MediaColumns;
46  import android.provider.MediaStore.Audio.AudioColumns;
47  import android.provider.MediaStore.Audio.Media;
48  
49  /***
50   * Wrapper for Android {@link MediaStore} class.
51   * 
52   * @author Martin Vysny
53   */
54  public final class MediaStoreCollection implements ICollection {
55  
56  	/***
57  	 * Columns used when retrieving tracks from {@link MediaStore}.
58  	 */
59  	static final String[] TRACK_COLUMNS = new String[] {
60  			MediaStore.Audio.AudioColumns.ALBUM,
61  			MediaStore.Audio.AudioColumns.ARTIST,
62  			MediaStore.Audio.AudioColumns.DURATION,
63  			MediaStore.Audio.AudioColumns.TRACK,
64  			MediaStore.Audio.AudioColumns.YEAR, MediaStore.MediaColumns.SIZE,
65  			MediaStore.MediaColumns.TITLE, BaseColumns._ID };
66  	private static final String[] ARTIST_FIELDS = new String[] {
67  			BaseColumns._ID, MediaStore.Audio.ArtistColumns.ARTIST,
68  			MediaStore.Audio.ArtistColumns.NUMBER_OF_ALBUMS,
69  			MediaStore.Audio.ArtistColumns.NUMBER_OF_TRACKS };
70  	private static final String[] ALBUM_FIELDS = new String[] {
71  			BaseColumns._ID, MediaStore.Audio.AlbumColumns.ALBUM,
72  			MediaStore.Audio.AlbumColumns.FIRST_YEAR,
73  			MediaStore.Audio.AlbumColumns.NUMBER_OF_SONGS };
74  	private final ContentResolver resolver;
75  
76  	/***
77  	 * Creates new collection wrapper.
78  	 * 
79  	 * @param resolver
80  	 *            resolves URI. Must not be <code>null</code>.
81  	 */
82  	public MediaStoreCollection(final ContentResolver resolver) {
83  		this.resolver = resolver;
84  	}
85  
86  	public CategoryItem deserializeItem(String serializedItem) {
87  		return new CategoryItem(Long.valueOf(serializedItem), null, null,
88  				CategoryEnum.Origin, -1, -1);
89  	}
90  
91  	public List<CategoryItem> getCategoryList(CategoryEnum request,
92  			CategoryItem context, String substring, boolean sortByYear)
93  			throws CollectionException, InterruptedException {
94  		List<CategoryItem> result = null;
95  		if (context == null) {
96  			if (request == CategoryEnum.Album) {
97  				result = findAlbums(substring);
98  			} else if (request == CategoryEnum.Artist) {
99  				result = findArtists(substring);
100 			} else if (request == CategoryEnum.Genre) {
101 				result = findGenres(substring);
102 			}
103 		} else if (context.category == CategoryEnum.Genre) {
104 			if (request == CategoryEnum.Album) {
105 				result = findAlbumsForGenre(substring, (Long) context.id);
106 			} else if (request == CategoryEnum.Artist) {
107 				result = findArtistsForGenre(substring, (Long) context.id);
108 			}
109 		} else if (context.category == CategoryEnum.Artist) {
110 			if (request == CategoryEnum.Album) {
111 				result = findAlbumsForArtist(substring, (Long) context.id);
112 			}
113 		}
114 		if (result == null) {
115 			throw new CollectionException("Searching for " + request + " in "
116 					+ context + " is not supported");
117 		}
118 		if (sortByYear && request == CategoryEnum.Album) {
119 			CollectionUtils.sortByYearName(result);
120 		} else {
121 			CollectionUtils.sortByName(result);
122 		}
123 		return result;
124 	}
125 
126 	private List<CategoryItem> findAlbumsForArtist(String substring,
127 			final Long id) throws InterruptedException {
128 		return findAlbumsForArtist(substring, id,
129 				MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI);
130 	}
131 
132 	private List<CategoryItem> findAlbumsForArtist(String substring,
133 			Long artistId, Uri artistsUri) throws InterruptedException {
134 		final Uri albums = Uri.withAppendedPath(Uri.withAppendedPath(
135 				artistsUri, artistId.toString()), "albums");
136 		final String selection = substring == null ? null : AudioColumns.ALBUM
137 				+ " LIKE ?";
138 		final String[] selectionArgs = substring == null ? null
139 				: new String[] { "%" + substring + "%" };
140 		final Cursor c = resolver.query(albums, ALBUM_FIELDS, selection,
141 				selectionArgs, null);
142 		return readAlbums(c);
143 	}
144 
145 	private List<CategoryItem> findArtistsForGenre(String substring,
146 			Long genreId) throws InterruptedException {
147 		final List<CategoryItem> result = findArtistsForGenre(substring,
148 				genreId, MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI,
149 				MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI);
150 		// result.addAll(findArtistsForGenre(substring, genreId,
151 		// MediaStore.Audio.Genres.INTERNAL_CONTENT_URI,
152 		// MediaStore.Audio.Artists.INTERNAL_CONTENT_URI));
153 		return result;
154 	}
155 
156 	private boolean isGenreNonEmpty(final Uri genreUri, final Long genreId) {
157 		final Uri members = Uri.withAppendedPath(Uri.withAppendedPath(genreUri,
158 				genreId.toString()),
159 				MediaStore.Audio.Genres.Members.CONTENT_DIRECTORY);
160 		final Cursor c = resolver.query(members, new String[] { "count(*)" },
161 				null, null, null);
162 		if (c == null) {
163 			return false;
164 		}
165 		Assert.assertTrue(c.moveToFirst());
166 		final long count = c.getLong(0);
167 		c.close();
168 		return count > 0;
169 	}
170 
171 	private List<CategoryItem> findArtistsForGenre(String substring,
172 			Long genreId, Uri genreUri, Uri artistUri)
173 			throws InterruptedException {
174 		final Uri members = Uri.withAppendedPath(Uri.withAppendedPath(genreUri,
175 				genreId.toString()),
176 				MediaStore.Audio.Genres.Members.CONTENT_DIRECTORY);
177 		final List<CategoryItem> result = new ArrayList<CategoryItem>();
178 		final String selection = substring == null ? null : AudioColumns.ALBUM
179 				+ " LIKE ?";
180 		final String[] selectionArgs = substring == null ? null
181 				: new String[] { "%" + substring + "%" };
182 		final Set<Long> artists = new HashSet<Long>();
183 		final Cursor c = resolver.query(members,
184 				new String[] { AudioColumns.ARTIST_ID }, selection,
185 				selectionArgs, null);
186 		if (c == null || !c.moveToFirst()) {
187 			return result;
188 		}
189 		do {
190 			final Long artistId = c.getLong(0);
191 			artists.add(artistId);
192 		} while (c.moveToNext());
193 		c.close();
194 		for (final Long artistId : artists) {
195 			final Cursor c1 = resolver.query(artistUri, ARTIST_FIELDS,
196 					BaseColumns._ID + "=?",
197 					new String[] { artistId.toString() }, null);
198 			result.addAll(readArtists(c1));
199 		}
200 		return result;
201 	}
202 
203 	private List<CategoryItem> readArtists(Cursor c)
204 			throws InterruptedException {
205 		final List<CategoryItem> result = new ArrayList<CategoryItem>();
206 		if (c == null || !c.moveToFirst()) {
207 			return result;
208 		}
209 		do {
210 			if (Thread.currentThread().isInterrupted()) {
211 				throw new InterruptedException();
212 			}
213 			final CategoryItem.Builder b = new CategoryItem.Builder();
214 			b.category = CategoryEnum.Artist;
215 			b.id = c.getLong(0);
216 			b.name = c.getString(1);
217 			b.albums = c.getInt(2);
218 			b.songs = c.getInt(3);
219 			result.add(b.build());
220 		} while (c.moveToNext());
221 		c.close();
222 		return result;
223 	}
224 
225 	private List<CategoryItem> findAlbumsForGenre(String substring, Long genreId)
226 			throws InterruptedException {
227 		final List<CategoryItem> result = findAlbumsForGenre(substring,
228 				genreId, MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI,
229 				MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI);
230 		// result.addAll(findAlbumsForGenre(substring, genreId,
231 		// MediaStore.Audio.Genres.INTERNAL_CONTENT_URI,
232 		// MediaStore.Audio.Albums.INTERNAL_CONTENT_URI));
233 		return result;
234 	}
235 
236 	private List<CategoryItem> findAlbumsForGenre(String substring, Long id,
237 			Uri genreUri, Uri albumUri) throws InterruptedException {
238 		final Uri members = Uri.withAppendedPath(Uri.withAppendedPath(genreUri,
239 				id.toString()),
240 				MediaStore.Audio.Genres.Members.CONTENT_DIRECTORY);
241 		final List<CategoryItem> result = new ArrayList<CategoryItem>();
242 		final String selection = substring == null ? null : AudioColumns.ALBUM
243 				+ " LIKE ?";
244 		final String[] selectionArgs = substring == null ? null
245 				: new String[] { "%" + substring + "%" };
246 		final Set<Long> albums = new HashSet<Long>();
247 		final Cursor c = resolver.query(members,
248 				new String[] { AudioColumns.ALBUM_ID }, selection,
249 				selectionArgs, null);
250 		if (c == null || !c.moveToFirst()) {
251 			return result;
252 		}
253 		do {
254 			final Long albumId = c.getLong(0);
255 			albums.add(albumId);
256 		} while (c.moveToNext());
257 		c.close();
258 		for (final Long albumId : albums) {
259 			final Cursor c1 = resolver.query(albumUri, ALBUM_FIELDS,
260 					BaseColumns._ID + "=?",
261 					new String[] { albumId.toString() }, null);
262 			result.addAll(readAlbums(c1));
263 		}
264 		return result;
265 	}
266 
267 	private List<CategoryItem> findGenres(String substring)
268 			throws InterruptedException {
269 		final List<CategoryItem> result = findGenres(substring,
270 				MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI);
271 		// result.addAll(findGenres(substring,
272 		// MediaStore.Audio.Genres.INTERNAL_CONTENT_URI));
273 		return result;
274 	}
275 
276 	private List<CategoryItem> findArtists(String substring)
277 			throws InterruptedException {
278 		final List<CategoryItem> result = findArtists(substring,
279 				MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI);
280 		// result.addAll(findArtists(substring,
281 		// MediaStore.Audio.Artists.INTERNAL_CONTENT_URI));
282 		return result;
283 	}
284 
285 	private List<CategoryItem> findAlbums(String substring)
286 			throws InterruptedException {
287 		final List<CategoryItem> result = findAlbums(substring,
288 				MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI);
289 		// result.addAll(findAlbums(substring,
290 		// MediaStore.Audio.Albums.INTERNAL_CONTENT_URI));
291 		return result;
292 	}
293 
294 	public String getName() {
295 		return "Android MediaStore";
296 	}
297 
298 	public Statistics getStatistics() {
299 		// final Statistics result = getStatistics(
300 		// MediaStore.Audio.Media.INTERNAL_CONTENT_URI,
301 		// MediaStore.Audio.Artists.INTERNAL_CONTENT_URI,
302 		// MediaStore.Audio.Albums.INTERNAL_CONTENT_URI);
303 		final Statistics result = getStatistics(
304 				MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
305 				MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI,
306 				MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI);
307 		return result;
308 	}
309 
310 	private Statistics getStatistics(final Uri mediaUri, final Uri artistUri,
311 			final Uri albumUri) {
312 		final Statistics result = new Statistics();
313 		Cursor c = resolver.query(mediaUri, new String[] { "count(*)",
314 				"sum(" + AudioColumns.DURATION + ")",
315 				"sum(" + MediaColumns.SIZE + ")" }, null, null, null);
316 		if (c != null) {
317 			Assert.assertTrue(c.moveToFirst());
318 			result.tracks = c.getInt(0);
319 			result.length = c.getInt(1) / 1000;
320 			result.fileSize = c.getLong(2);
321 			c.close();
322 		}
323 		c = resolver.query(artistUri, new String[] { "count(*)" }, null, null,
324 				null);
325 		if (c != null) {
326 			Assert.assertTrue(c.moveToFirst());
327 			result.artists = c.getInt(0);
328 			c.close();
329 		}
330 		c = resolver.query(albumUri, new String[] { "count(*)" }, null, null,
331 				null);
332 		if (c != null) {
333 			Assert.assertTrue(c.moveToFirst());
334 			result.albums = c.getInt(0);
335 			c.close();
336 		}
337 		return result;
338 	}
339 
340 	public List<TrackMetadataBean> getTracks(CategoryItem context)
341 			throws CollectionException, InterruptedException {
342 		final String selection;
343 		final String[] selectionArgs = new String[] { context.id.toString() };
344 		if (context.category == CategoryEnum.Album) {
345 			selection = AudioColumns.ALBUM_ID + "=?";
346 		} else if (context.category == CategoryEnum.Artist) {
347 			selection = AudioColumns.ARTIST_ID + "=?";
348 		} else {
349 			throw new CollectionException("Unsupported getTracks for "
350 					+ context.category);
351 		}
352 		final Cursor c = resolver.query(Media.EXTERNAL_CONTENT_URI,
353 				TRACK_COLUMNS, selection, selectionArgs, null);
354 		final List<TrackMetadataBean> result = readTracks(c,
355 				Media.EXTERNAL_CONTENT_URI);
356 		return result;
357 	}
358 
359 	/***
360 	 * Reads tracks from given cursor. The cursor must have columns as specified
361 	 * in {@link #TRACK_COLUMNS}.
362 	 * 
363 	 * @param c
364 	 *            the cursor to poll. Closed when the method finishes.
365 	 * @param contentUri
366 	 *            uses this URI when setting the location of retrieved tracks
367 	 * @return list of tracks.
368 	 * @throws InterruptedException
369 	 */
370 	static List<TrackMetadataBean> readTracks(Cursor c, final Uri contentUri)
371 			throws InterruptedException {
372 		final List<TrackMetadataBean> result = new ArrayList<TrackMetadataBean>();
373 		if (c == null || !c.moveToFirst()) {
374 			return result;
375 		}
376 		do {
377 			if (Thread.currentThread().isInterrupted()) {
378 				throw new InterruptedException();
379 			}
380 			result.add(readTrack(c, contentUri));
381 		} while (c.moveToNext());
382 		c.close();
383 		return result;
384 	}
385 
386 	/***
387 	 * Reads a single track from current cursor position. The cursor must have
388 	 * columns as specified in {@link #TRACK_COLUMNS}.
389 	 * 
390 	 * @param c
391 	 *            the cursor to poll. The cursor is not moved.
392 	 * @param contentUri
393 	 *            uses this URI when setting the location of retrieved tracks
394 	 * @return the track
395 	 */
396 	static TrackMetadataBean readTrack(final Cursor c, final Uri contentUri) {
397 		final TrackMetadataBean.Builder b = new TrackMetadataBean.Builder();
398 		b.setAlbum(c.getString(0));
399 		b.setArtist(c.getString(1));
400 		final Long length = c.getLong(2);
401 		b.setLength(length == null ? 0 : (int) (length / 1000));
402 		b.setTrackNumber(c.getString(3));
403 		b.setYearReleased(c.getString(4));
404 		final Long fileSize = c.getLong(5);
405 		b.setFileSize(fileSize == null ? 0 : fileSize);
406 		b.setTitle(c.getString(6));
407 		final Long id = c.getLong(7);
408 		b.setLocation(contentUri.toString() + "/" + id);
409 		b.setOrigin(TrackOriginEnum.LocalFs);
410 		return b.build(id);
411 	}
412 
413 	public boolean isLocal() {
414 		return true;
415 	}
416 
417 	public List<TrackMetadataBean> searchTracks(String substring)
418 			throws InterruptedException {
419 		// final List<TrackMetadataBean> result = searchTracksInUri(
420 		// MediaStore.Audio.Media.INTERNAL_CONTENT_URI, substring);
421 		final List<TrackMetadataBean> result = searchTracksInUri(
422 				MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, substring);
423 		return result;
424 	}
425 
426 	private List<TrackMetadataBean> searchTracksInUri(final Uri uri,
427 			final String substring) throws InterruptedException {
428 		final StringBuilder where = new StringBuilder();
429 		where.append(MediaStore.Audio.AudioColumns.IS_MUSIC);
430 		where.append(" != 0");
431 		final String[] whereArgs;
432 		if (substring != null) {
433 			where.append(" and (");
434 			where.append(MediaStore.Audio.AudioColumns.ALBUM);
435 			where.append(" LIKE ? or ");
436 			where.append(MediaStore.Audio.AudioColumns.ARTIST);
437 			where.append(" LIKE ? or ");
438 			where.append(MediaStore.MediaColumns.TITLE);
439 			where.append(" LIKE ?)");
440 			final String whereArg = "%" + substring + "%";
441 			whereArgs = new String[] { whereArg, whereArg, whereArg };
442 		} else {
443 			whereArgs = null;
444 		}
445 		final Cursor c = resolver.query(uri, TRACK_COLUMNS, where.toString(),
446 				whereArgs, null);
447 		return readTracks(c, uri);
448 	}
449 
450 	public String serializeItem(CategoryItem item) {
451 		return item.id.toString();
452 	}
453 
454 	private List<CategoryItem> findAlbums(final String substring,
455 			final Uri contentUri) throws InterruptedException {
456 		final String selection = substring == null ? null
457 				: MediaStore.Audio.AlbumColumns.ALBUM + " LIKE ?";
458 		final String[] selectionArgs = substring == null ? null
459 				: new String[] { "%" + substring + "%" };
460 		final Cursor c = resolver.query(contentUri, ALBUM_FIELDS, selection,
461 				selectionArgs, null);
462 		return readAlbums(c);
463 	}
464 
465 	private List<CategoryItem> readAlbums(final Cursor c)
466 			throws InterruptedException {
467 		final List<CategoryItem> result = new ArrayList<CategoryItem>();
468 		if (c == null || !c.moveToFirst()) {
469 			return result;
470 		}
471 		do {
472 			if (Thread.currentThread().isInterrupted()) {
473 				throw new InterruptedException();
474 			}
475 			final CategoryItem.Builder b = new CategoryItem.Builder();
476 			b.albums = 1;
477 			b.category = CategoryEnum.Album;
478 			b.id = c.getLong(0);
479 			b.name = c.getString(1);
480 			b.year = c.getString(2);
481 			b.songs = c.getInt(3);
482 			result.add(b.build());
483 		} while (c.moveToNext());
484 		c.close();
485 		return result;
486 	}
487 
488 	private List<CategoryItem> findArtists(final String substring,
489 			final Uri contentUri) throws InterruptedException {
490 		final String selection = substring == null ? null
491 				: MediaStore.Audio.ArtistColumns.ARTIST + " LIKE ?";
492 		final String[] selectionArgs = substring == null ? null
493 				: new String[] { "%" + substring + "%" };
494 		final Cursor c = resolver.query(contentUri, ARTIST_FIELDS, selection,
495 				selectionArgs, null);
496 		return readArtists(c);
497 	}
498 
499 	private List<CategoryItem> findGenres(final String substring,
500 			final Uri contentUri) throws InterruptedException {
501 		final List<CategoryItem> result = new ArrayList<CategoryItem>();
502 		final String selection = substring == null ? null
503 				: MediaStore.Audio.GenresColumns.NAME + " LIKE ?";
504 		final String[] selectionArgs = substring == null ? null
505 				: new String[] { "%" + substring + "%" };
506 		final Cursor c = resolver.query(contentUri, new String[] {
507 				BaseColumns._ID, MediaStore.Audio.GenresColumns.NAME },
508 				selection, selectionArgs, null);
509 		if (c == null || !c.moveToFirst()) {
510 			return result;
511 		}
512 		do {
513 			if (Thread.currentThread().isInterrupted()) {
514 				throw new InterruptedException();
515 			}
516 			final long genreId = c.getLong(0);
517 			if (!isGenreNonEmpty(contentUri, genreId)) {
518 				continue;
519 			}
520 			final CategoryItem.Builder b = new CategoryItem.Builder();
521 			b.category = CategoryEnum.Genre;
522 			b.id = genreId;
523 			b.name = c.getString(1);
524 			b.albums = -1;
525 			b.songs = -1;
526 			result.add(b.build());
527 		} while (c.moveToNext());
528 		return result;
529 	}
530 
531 	public IDynamicPlaylistTrackProvider newTrackProvider(final Random random) {
532 		return new MediaStoreTrackProvider(random);
533 	}
534 
535 	public List<TrackMetadataBean> findTracks(Map<CategoryEnum, String> criteria)
536 			throws CollectionException, InterruptedException {
537 		return findTracksInUri(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
538 				criteria);
539 	}
540 
541 	private final static Map<CategoryEnum, String> COLUMNS = new EnumMap<CategoryEnum, String>(
542 			CategoryEnum.class);
543 	static {
544 		COLUMNS.put(CategoryEnum.Album, AudioColumns.ALBUM);
545 		COLUMNS.put(CategoryEnum.Artist, AudioColumns.ARTIST);
546 		COLUMNS.put(CategoryEnum.Title, MediaColumns.TITLE);
547 		COLUMNS.put(CategoryEnum.YearReleased, AudioColumns.YEAR);
548 	}
549 
550 	private List<TrackMetadataBean> findTracksInUri(final Uri uri,
551 			final Map<CategoryEnum, String> criteria)
552 			throws InterruptedException {
553 		final StringBuilder where = new StringBuilder();
554 		where.append("(");
555 		where.append(MediaStore.Audio.AudioColumns.IS_MUSIC);
556 		where.append(" != 0)");
557 		final List<String> whereArgs = new ArrayList<String>();
558 		for (final Map.Entry<CategoryEnum, String> entry : criteria.entrySet()) {
559 			final String column = COLUMNS.get(entry.getKey());
560 			if (column == null) {
561 				continue;
562 			}
563 			where.append(" and (");
564 			where.append(column);
565 			where.append(" LIKE ?)");
566 			whereArgs.add("%" + entry.getValue() + "%");
567 		}
568 		final Cursor c = resolver.query(uri, TRACK_COLUMNS, where.toString(),
569 				whereArgs.toArray(new String[0]), null);
570 		return readTracks(c, uri);
571 	}
572 
573 	public Map<String, String> fixLocations(Collection<String> locations) {
574 		throw new UnsupportedOperationException();
575 	}
576 
577 	public boolean supportsLocationFix() {
578 		return false;
579 	}
580 }