/* * * TURTLE PLAYER * * Licensed under MIT & GPL * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE * OR OTHER DEALINGS IN THE SOFTWARE. * * Created by Edd Turtle (www.eddturtle.co.uk) * More Information @ www.turtle-player.co.uk * */ package com.turtleplayer.playlist; import android.content.Context; import android.util.Log; import com.turtleplayer.Stats; import com.turtleplayer.common.filefilter.FileFilters; import com.turtleplayer.controller.Observer; import com.turtleplayer.model.*; import com.turtleplayer.persistance.framework.executor.OperationExecutor; import com.turtleplayer.persistance.framework.filter.FieldFilter; import com.turtleplayer.persistance.framework.filter.Filter; import com.turtleplayer.persistance.framework.filter.FilterSet; import com.turtleplayer.persistance.framework.filter.Operator; import com.turtleplayer.persistance.framework.sort.RandomOrder; import com.turtleplayer.persistance.source.sql.First; import com.turtleplayer.persistance.source.sqlite.QuerySqlite; import com.turtleplayer.persistance.turtle.FsReader; import com.turtleplayer.persistance.turtle.db.TurtleDatabase; import com.turtleplayer.persistance.turtle.db.structure.Tables; import com.turtleplayer.persistance.turtle.mapping.TrackCreator; import com.turtleplayer.playlist.playorder.PlayOrderStrategy; import com.turtleplayer.preferences.Keys; import com.turtleplayer.preferences.Preferences; import com.turtleplayer.util.Shorty; import java.io.IOException; import java.util.*; import java.util.concurrent.*; public class Playlist { //Log Constants /* { NEXT, PREV, PULL } */ // Not in ClassDiagram public final Preferences preferences; public final Stats stats = new Stats(); private final TurtleDatabase db; private final Set<Filter<? super Tables.Tracks>> filters = new HashSet<Filter<? super Tables.Tracks>>(); private final ExecutorService fsScannerExecutorService = Executors.newSingleThreadExecutor(); private Future<?> currentFuture = null; public Playlist(Context mainContext, TurtleDatabase db) { // Location, Repeat, Shuffle (Remember Trailing / on Location) preferences = new Preferences(mainContext); this.db = db; } /** * @return true if the filter was not already there */ public boolean addFilter(Filter<? super Tables.Tracks> filter){ boolean modified = filters.add(filter); if(modified) { for (PlaylistObserver observer : observers.values()) { observer.filterAdded(filter); } } return modified; } public void clearFilters(){ final Set<Filter<? super Tables.Tracks>> filtersToDelete = new HashSet<Filter<? super Tables.Tracks>>(filters); for(Filter<? super Tables.Tracks> filter : filtersToDelete) { removeFilter(filter); } } public boolean removeFilter(Filter<? super Tables.Tracks> filter){ boolean modified = filters.remove(filter); if(modified) { for (PlaylistObserver observer : observers.values()) { observer.filterRemoved(filter); } } return modified; } public Filter<? super Tables.Tracks> getCompressedFilter() { return filters.isEmpty() ? new FilterSet<Tables.Tracks>() : new FilterSet<Tables.Tracks>(filters); } public Set<Filter<? super Tables.Tracks>> getFilter() { return Collections.unmodifiableSet(filters); } /**@ * param track * @return adds additional information to track */ public TrackBundle enrich(PlayOrderStrategy strategy, Track track){ return new TrackBundle( track, strategy.getNext(track), strategy.getPrevious(track) ); } public Track getTrack(String src) { FSobject fsObject = new FSobject(src); return OperationExecutor.execute( db, new QuerySqlite<Tables.Tracks, Tables.Tracks, Track>( new FilterSet<Tables.Tracks>( getCompressedFilter(), new FieldFilter<Tables.Tracks, Track, String>(Tables.FsObjects.NAME, Operator.EQ, fsObject.getPath()), new FieldFilter<Tables.Tracks, Track, String>(Tables.FsObjects.PATH, Operator.EQ, fsObject.getName())), new First<Track>(Tables.TRACKS, new TrackCreator()) ) ); } public Track getNext(PlayOrderStrategy strategy, Track ofTrack) { return strategy.getNext(ofTrack); } public Track getPrevious(PlayOrderStrategy strategy, Track ofTrack) { return strategy.getPrevious(ofTrack); } public Track getRandom() { return OperationExecutor.execute(db, new QuerySqlite<Tables.Tracks, Tables.Tracks, Track>( getCompressedFilter(), new RandomOrder<Tables.Tracks>(), new First<Track>(Tables.TRACKS, new TrackCreator()))); } /** * reads the stored state and calls the obsrver methods to adjust the ui, */ public void notifyInitialState() { if(!Shorty.isVoid(preferences.get(Keys.FS_SCAN_INTERRUPT_PATH))){ for (PlaylistObserver observer : observers.values()) { observer.startUpdatePlaylist(); observer.startRescan(preferences.get(Keys.FS_SCAN_INTERRUPT_COUNT_ALL)); observer.trackAdded( preferences.get(Keys.FS_SCAN_INTERRUPT_PATH), preferences.get(Keys.FS_SCAN_INTERRUPT_COUNT_PROCESSED) ); observer.pauseRescan(); } } } public void toggleFsScanPause() { if(fsScanActive()) { interruptFsScan(); } else { runFsScan(); } } public boolean isFsScanNotStarted(){ return !fsScanActive() && Shorty.isVoid(preferences.get(Keys.FS_SCAN_INTERRUPT_PATH)); } private boolean fsScanActive() { return currentFuture != null && !currentFuture.isCancelled() && !currentFuture.isDone(); } public void pauseFsScan(){ interruptFsScan(); } public void stopFsScan() { boolean interrupted = interruptFsScan(); if(interrupted) { try { synchronized (currentFuture) { currentFuture.wait(3000); } } catch (InterruptedException e) { //expected } preferences.set(Keys.FS_SCAN_INTERRUPT_PATH, null); for (PlaylistObserver observer : observers.values()) { observer.endRescan(); } } preferences.set(Keys.FS_SCAN_INTERRUPT_PATH, null); } public void startFsScan() { stopFsScan(); runFsScan(); } private boolean interruptFsScan() { return currentFuture != null && currentFuture.cancel(true); } private void runFsScan() { interruptFsScan(); currentFuture = fsScannerExecutorService.submit(new Runnable() { public void run() { Thread.currentThread().setPriority(Thread.MIN_PRIORITY); for (PlaylistObserver observer : observers.values()) { observer.startUpdatePlaylist(); } boolean wasPaused = false; try { final String mediaPath = preferences.getExitstingMediaPath().toString(); final String lastFsScanInterruptPath = preferences.get(Keys.FS_SCAN_INTERRUPT_PATH); if(lastFsScanInterruptPath != null) { for (PlaylistObserver observer : observers.values()) { observer.unpauseRescanInitializing(); } } List<String> mediaFilePaths = FsReader.getMediaFilesPaths(mediaPath, FileFilters.PLAYABLE_FILES_FILTER, true, false); final List<String> mediaFilePathsToScan; final int lastFsScanInterruptPathIndex = mediaFilePaths.indexOf(lastFsScanInterruptPath); if(lastFsScanInterruptPath == null || lastFsScanInterruptPathIndex < 0) { mediaFilePathsToScan = mediaFilePaths; for (PlaylistObserver observer : observers.values()) { observer.startRescan(mediaFilePathsToScan.size()); } } else { mediaFilePathsToScan = new ArrayList<String>(); for(int i = lastFsScanInterruptPathIndex+1; i < mediaFilePaths.size(); i++){ mediaFilePathsToScan.add(mediaFilePaths.get(i)); } for (PlaylistObserver observer : observers.values()) { observer.unpauseRescan(lastFsScanInterruptPathIndex+1, mediaFilePathsToScan.size()); } } scanFiles(mediaFilePathsToScan, db, lastFsScanInterruptPathIndex); } catch (InterruptedException e) { wasPaused = true; for (PlaylistObserver observer : observers.values()) { observer.pauseRescan(); } } finally { if(!wasPaused) { for (PlaylistObserver observer : observers.values()) { observer.endRescan(); } } } } }); } public void scanFiles(Collection<String> mediaFilePaths, TurtleDatabase db, int allreadyProcessed) throws InterruptedException { Set<String> encounteredRootSrcs = new HashSet<String>(); int countProcessed = allreadyProcessed; for(String mediaFilePath : mediaFilePaths) { boolean trackAdded = false; try { trackAdded = FsReader.scanFile(mediaFilePath, db, encounteredRootSrcs); } catch (IOException e) { //log and go on with next File Log.v(Preferences.TAG, "failed to process " + mediaFilePath); } finally { countProcessed++; if (Thread.currentThread().isInterrupted()) { preferences.set(Keys.FS_SCAN_INTERRUPT_PATH, mediaFilePath); preferences.set(Keys.FS_SCAN_INTERRUPT_COUNT_PROCESSED, countProcessed); preferences.set(Keys.FS_SCAN_INTERRUPT_COUNT_ALL, mediaFilePaths.size()); throw new InterruptedException(); } Thread.sleep(10); } if(trackAdded) { for (PlaylistObserver observer : observers.values()) { observer.trackAdded(mediaFilePath, countProcessed); } } } preferences.set(Keys.FS_SCAN_INTERRUPT_PATH, null); for (PlaylistObserver observer : observers.values()) { observer.endUpdatePlaylist(); } } public Collection<? extends Track> getCurrTracks() { return db.getTracks(getCompressedFilter()); } public int Length() { return getCurrTracks().size(); } public boolean IsEmpty() { return db.isEmpty(null); } public void DatabaseClear() { db.clear(); } //------------------------------------------------------ Observable final Map<String, PlaylistObserver> observers = new HashMap<String, PlaylistObserver>(); public interface PlaylistObserver extends Observer { void trackAdded(final String filePath, final int allreadyProcessed); void startRescan(int toProcess); public void endRescan(); void pauseRescan(); void unpauseRescanInitializing(); void unpauseRescan(int alreadyProcessed, int toProcess); void startUpdatePlaylist(); void endUpdatePlaylist(); void filterAdded(Filter<? super Tables.Tracks> filter); void filterRemoved(Filter<? super Tables.Tracks> filter); } public static abstract class PlaylistFilterChangeObserver implements PlaylistObserver { public void trackAdded(final String filePath, final int allreadyProcessed){/*doNothing*/} public void startRescan(int toProcess){/*doNothing*/} public void endRescan(){/*doNothing*/} public void startUpdatePlaylist(){/*doNothing*/} public void endUpdatePlaylist(){/*doNothing*/} public void unpauseRescanInitializing(){/*doNothing*/} public void unpauseRescan(int alreadyProcessed, int toProcess){/*doNothing*/} public void pauseRescan(){/*doNothing*/} } public static abstract class PlaylistTrackChangeObserver implements PlaylistObserver{ public void filterAdded(Filter<? super Tables.Tracks> filter){/*doNothing*/} public void filterRemoved(Filter<? super Tables.Tracks> filter){/*doNothing*/} } public void addObserver(PlaylistObserver observer) { observers.put(observer.getId(), observer); } public void removeObserver(Observer observer) { observers.remove(observer.getId()); } }