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