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.activity.main;
19  
20  import java.io.File;
21  import java.io.IOException;
22  import java.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.Collections;
25  import java.util.Comparator;
26  import java.util.EnumMap;
27  import java.util.List;
28  import java.util.Map;
29  
30  import sk.baka.ambient.ActionsEnum;
31  import sk.baka.ambient.AmbientApplication;
32  import sk.baka.ambient.R;
33  import sk.baka.ambient.collection.CategoryEnum;
34  import sk.baka.ambient.collection.ICollection;
35  import sk.baka.ambient.collection.TrackMetadataBean;
36  import sk.baka.ambient.collection.local.MediaStoreCollection;
37  import sk.baka.ambient.commons.Interval;
38  import sk.baka.ambient.commons.MiscUtils;
39  import sk.baka.ambient.library.LibraryUtils;
40  import sk.baka.ambient.playlist.Parsers;
41  import sk.baka.ambient.views.gesturelist.GesturesListView;
42  import android.util.Log;
43  import android.view.View;
44  import android.widget.TextView;
45  
46  /***
47   * The file browser.
48   * 
49   * @author Martin Vysny
50   */
51  public final class FileBrowserController extends AbstractListController {
52  
53  	/***
54  	 * The actions to display on the Task switcher.
55  	 */
56  	private final List<ActionsEnum> actions = Collections
57  			.unmodifiableList(Arrays.asList(ActionsEnum.Back,
58  					ActionsEnum.GoToRoot, ActionsEnum.DeleteSelected));
59  
60  	/***
61  	 * @param mainActivity
62  	 * @param playlistView
63  	 *            the playlist view.
64  	 */
65  	public FileBrowserController(final MainActivity mainActivity,
66  			final GesturesListView playlistView) {
67  		super(R.id.filebrowser, R.id.filebrowserView, mainActivity);
68  		// register the drag'n'drop target.
69  		listView.dragDropViews.clear();
70  		listView.dragDropViews.add(playlistView);
71  		update(Interval.EMPTY);
72  		initButtonBar(R.id.filebrowserButtons, actions);
73  	}
74  
75  	@Override
76  	public void destroy() {
77  		listView.dragDropViews.clear();
78  		super.destroy();
79  	}
80  
81  	@Override
82  	protected void onAction(ActionsEnum action) {
83  		if (action == ActionsEnum.Back) {
84  			goBack();
85  			return;
86  		}
87  		if (action == ActionsEnum.DeleteSelected) {
88  			deleteSelected();
89  			return;
90  		}
91  		if (action == ActionsEnum.GoToRoot) {
92  			currentDirectory = new File("/");
93  			update(Interval.EMPTY);
94  			return;
95  		}
96  		super.onAction(action);
97  	}
98  
99  	private void deleteSelected() {
100 		if (!currentDirectory.getAbsolutePath().startsWith("/sdcard")) {
101 			return;
102 		}
103 		final Interval highlight = listView.getHighlight();
104 		if (highlight.isEmpty()) {
105 			return;
106 		}
107 		final File[] filesToDelete = new File[highlight.length];
108 		System.arraycopy(currentDirectoryContents, highlight.start,
109 				filesToDelete, 0, highlight.length);
110 		final String name = app.getString(R.string.deleteSelectedFiles);
111 		final Runnable del = new Runnable() {
112 			public void run() {
113 				try {
114 					delete(filesToDelete, true);
115 				} catch (IOException e) {
116 					throw new RuntimeException(e);
117 				} finally {
118 					AmbientApplication.getHandler().post(new Runnable() {
119 						public void run() {
120 							update(Interval.EMPTY);
121 						}
122 					});
123 				}
124 			}
125 
126 			private void delete(File[] toDelete, final boolean topLevel)
127 					throws IOException {
128 				if (toDelete == null) {
129 					return;
130 				}
131 				int progress = 0;
132 				for (File f : toDelete) {
133 					if (topLevel) {
134 						app.getBackgroundTasks().backgroundTask(0, name,
135 								progress++, toDelete.length);
136 					}
137 					if (f.isDirectory()) {
138 						delete(f.listFiles(), false);
139 					}
140 					if (!f.delete()) {
141 						throw new IOException("Cannot delete: "
142 								+ f.getAbsolutePath());
143 					}
144 				}
145 			}
146 		};
147 		app.getBackgroundTasks().schedule(del, del.getClass(), false, name);
148 	}
149 
150 	public void itemActivated(final int index, final Object model) {
151 		activateFile(index);
152 	}
153 
154 	@Override
155 	public void removeItems(Interval remove) {
156 		goBack();
157 	}
158 
159 	private void goBack() {
160 		final String currentDir = currentDirectory.getName();
161 		currentDirectory = currentDirectory.getParentFile();
162 		if (currentDirectory == null) {
163 			currentDirectory = new File("/");
164 		}
165 		update(Interval.EMPTY);
166 		// try to scroll to the directory we just left.
167 		final int currentDirIndex = listView.getModel().getModel().indexOf(
168 				"[" + currentDir + "]");
169 		if (currentDirIndex >= 0) {
170 			this.listView.setSelection(currentDirIndex);
171 		}
172 	}
173 
174 	/***
175 	 * Activates given file - either enters the directory or appends music file
176 	 * to the playlist.
177 	 * 
178 	 * @param index
179 	 *            the index of the file to activate, indexes
180 	 *            {@link #currentDirectoryContents}.
181 	 */
182 	private void activateFile(int index) {
183 		final File file = currentDirectoryContents[index];
184 		if (file.isDirectory()) {
185 			currentDirectory = file;
186 			update(Interval.EMPTY);
187 		} else {
188 			final TrackMetadataBean track = LibraryUtils.getTag(file
189 					.getAbsolutePath());
190 			app.getPlaylist().add(track);
191 		}
192 	}
193 
194 	/***
195 	 * The directory being displayed.
196 	 */
197 	private File currentDirectory = new File("/");
198 
199 	/***
200 	 * Current directory contents.
201 	 */
202 	private File[] currentDirectoryContents;
203 
204 	@Override
205 	protected void recomputeListItems() {
206 		// update the directory panel
207 		final TextView dir = (TextView) mainView
208 				.findViewById(R.id.filebrowserPath);
209 		dir.setText(currentDirectory.getAbsolutePath());
210 		// update the file list
211 		currentDirectoryContents = currentDirectory
212 				.listFiles(LibraryUtils.MUSIC_FILTER);
213 		if (currentDirectoryContents == null) {
214 			currentDirectoryContents = new File[0];
215 		}
216 		Arrays.sort(currentDirectoryContents, fileSort);
217 		listView.getModel().getModel().clear();
218 		for (int i = 0; i < currentDirectoryContents.length; i++) {
219 			String name = currentDirectoryContents[i].getName();
220 			if (currentDirectoryContents[i].isDirectory()) {
221 				name = "[" + name + "]";
222 			}
223 			listView.getModel().getModel().add(name);
224 		}
225 	}
226 
227 	public void update(GesturesListView listView, View itemView, int index,
228 			Object model) {
229 		final TextView view = (TextView) itemView;
230 		if (listView.getHighlight().contains(index)) {
231 			view.setBackgroundColor(highlightColor);
232 		} else {
233 			view.setBackgroundColor(0);
234 		}
235 		view.setText((String) listView.getModel().getModel().get(index));
236 	}
237 
238 	/***
239 	 * Sorts files: directories first, then sorts files by the file name.
240 	 */
241 	private final static Comparator<File> fileSort = new Comparator<File>() {
242 		public int compare(File object1, File object2) {
243 			final boolean bothFilesOrDirs = (object1.isDirectory() == object2
244 					.isDirectory());
245 			if (!bothFilesOrDirs) {
246 				return object1.isDirectory() ? -1 : 1;
247 			}
248 			return object1.getName().compareTo(object2.getName());
249 		}
250 	};
251 
252 	@Override
253 	public synchronized List<TrackMetadataBean> computeTracks(Interval highlight) {
254 		final File[] filesToAdd = new File[highlight.length];
255 		System.arraycopy(currentDirectoryContents, highlight.start, filesToAdd,
256 				0, highlight.length);
257 		final List<TrackMetadataBean> result = new ArrayList<TrackMetadataBean>();
258 		scan(filesToAdd, result);
259 		if (Thread.currentThread().isInterrupted()) {
260 			return null;
261 		}
262 		return result;
263 	}
264 
265 	/***
266 	 * Scans given directories/files recursively for music files.
267 	 * 
268 	 * @param dirs
269 	 *            the directories/files to scan.
270 	 * @param result
271 	 *            put all track meta here.
272 	 */
273 	private void scan(final File[] dirs, List<TrackMetadataBean> result) {
274 		if (dirs == null)
275 			return;
276 		for (final File file : dirs) {
277 			Thread.yield();
278 			if (Thread.currentThread().isInterrupted())
279 				return;
280 			if (file.isDirectory()) {
281 				final File[] children = file
282 						.listFiles(LibraryUtils.MUSIC_FILTER);
283 				Arrays.sort(children, fileSort);
284 				scan(children, result);
285 			} else {
286 				addFile(result, file);
287 			}
288 		}
289 	}
290 
291 	private void addFile(List<TrackMetadataBean> result, final File file) {
292 		if (LibraryUtils.PLAYLIST_FILTER.accept(file)) {
293 			try {
294 				final List<TrackMetadataBean> playlist = Parsers.parse(file);
295 				if (Parsers.hasMetadata(file.getName())) {
296 					findInCollection(playlist);
297 				}
298 				result.addAll(playlist);
299 			} catch (final Exception ex) {
300 				app.error(getClass(), true, mainActivity
301 						.getString(R.string.failedToParsePlaylist), ex);
302 			}
303 		} else {
304 			final TrackMetadataBean tag = LibraryUtils.getTag(file
305 					.getAbsolutePath());
306 			result.add(tag);
307 		}
308 	}
309 
310 	/***
311 	 * Finds tracks in given playlist in the collection and fixes locations.
312 	 * 
313 	 * @param playlist
314 	 *            the playlist to post-process.
315 	 */
316 	private void findInCollection(List<TrackMetadataBean> playlist) {
317 		final ICollection coll = new MediaStoreCollection(mainActivity
318 				.getContentResolver());
319 		for (int i = 0; i < playlist.size(); i++) {
320 			final TrackMetadataBean track = playlist.get(i);
321 			if (track.isLocal() && new File(track.getLocation()).exists()) {
322 				continue;
323 			}
324 			if (MiscUtils.isEmptyOrWhitespace(track.getAlbum())
325 					|| MiscUtils.isEmptyOrWhitespace(track.getTitle())) {
326 				continue;
327 			}
328 			// find the track in the collection
329 			final Map<CategoryEnum, String> criteria = new EnumMap<CategoryEnum, String>(
330 					CategoryEnum.class);
331 			criteria.put(CategoryEnum.Album, track.getAlbum().trim());
332 			criteria.put(CategoryEnum.Title, track.getTitle().trim());
333 			try {
334 				final List<TrackMetadataBean> tracks = coll
335 						.findTracks(criteria);
336 				if (tracks.size() != 1) {
337 					continue;
338 				}
339 				playlist.set(i, tracks.get(0));
340 			} catch (final Exception e) {
341 				Log.e(FileBrowserController.class.getSimpleName(),
342 						"Failed to find a track", e);
343 			}
344 		}
345 	}
346 
347 	@Override
348 	public boolean isComputeTracksLong(Interval interval) {
349 		return true;
350 	}
351 
352 	@Override
353 	public boolean isComputeTracksOnlineOp(Interval interval) {
354 		return false;
355 	}
356 
357 	@Override
358 	public String getHint(Interval highlight) {
359 		int files = 0;
360 		int dirs = 0;
361 		for (int i = highlight.start; i <= highlight.end; i++) {
362 			final File f = currentDirectoryContents[i];
363 			if (f.isDirectory())
364 				dirs++;
365 			else
366 				files++;
367 		}
368 		return listView.getResources()
369 				.getString(R.string.numFiles, files, dirs);
370 	}
371 
372 	@Override
373 	public boolean isReadOnly() {
374 		return true;
375 	}
376 
377 	@Override
378 	public boolean canComputeItems() {
379 		return true;
380 	}
381 
382 	@Override
383 	protected void performZoom(boolean zoom) {
384 		super.performZoom(zoom);
385 		initButtonBar(R.id.filebrowserButtons, actions);
386 	}
387 }