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
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
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
207 final TextView dir = (TextView) mainView
208 .findViewById(R.id.filebrowserPath);
209 dir.setText(currentDirectory.getAbsolutePath());
210
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
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 }