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
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
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
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
224 final TextView dir = (TextView) mainView
225 .findViewById(R.id.filebrowserPath);
226 dir.setText(currentDirectory.getAbsolutePath());
227
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
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
426 throw new Error();
427 }
428 }