PageRenderTime 49ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 1ms

/src/br/com/carlosrafaelgn/fplay/list/FileFetcher.java

https://gitlab.com/madamovic-bg/FPlayAndroid
Java | 887 lines | 710 code | 63 blank | 114 comment | 200 complexity | 4f6fcf8117e2134cc66ab6bd3448fe8d MD5 | raw file
  1. //
  2. // FPlayAndroid is distributed under the FreeBSD License
  3. //
  4. // Copyright (c) 2013-2014, Carlos Rafael Gimenes das Neves
  5. // All rights reserved.
  6. //
  7. // Redistribution and use in source and binary forms, with or without
  8. // modification, are permitted provided that the following conditions are met:
  9. //
  10. // 1. Redistributions of source code must retain the above copyright notice, this
  11. // list of conditions and the following disclaimer.
  12. // 2. Redistributions in binary form must reproduce the above copyright notice,
  13. // this list of conditions and the following disclaimer in the documentation
  14. // and/or other materials provided with the distribution.
  15. //
  16. // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  17. // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  18. // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  19. // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
  20. // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  21. // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  22. // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  23. // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  24. // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  25. // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  26. //
  27. // The views and conclusions contained in the software and documentation are those
  28. // of the authors and should not be interpreted as representing official policies,
  29. // either expressed or implied, of the FreeBSD Project.
  30. //
  31. // https://github.com/carlosrafaelgn/FPlayAndroid
  32. //
  33. package br.com.carlosrafaelgn.fplay.list;
  34. import android.annotation.TargetApi;
  35. import android.app.Service;
  36. import android.database.Cursor;
  37. import android.net.Uri;
  38. import android.os.Build;
  39. import android.os.Environment;
  40. import java.io.BufferedReader;
  41. import java.io.File;
  42. import java.io.FileFilter;
  43. import java.io.InputStream;
  44. import java.io.InputStreamReader;
  45. import java.util.ArrayList;
  46. import java.util.Arrays;
  47. import java.util.HashMap;
  48. import java.util.HashSet;
  49. import java.util.Locale;
  50. import br.com.carlosrafaelgn.fplay.R;
  51. import br.com.carlosrafaelgn.fplay.activity.MainHandler;
  52. import br.com.carlosrafaelgn.fplay.playback.Player;
  53. import br.com.carlosrafaelgn.fplay.ui.UI;
  54. import br.com.carlosrafaelgn.fplay.util.ArraySorter;
  55. //
  56. //Supported Media Formats
  57. //http://developer.android.com/guide/appendix/media-formats.html
  58. //
  59. public final class FileFetcher implements Runnable, ArraySorter.Comparer<FileSt>, FileFilter {
  60. public interface Listener {
  61. void onFilesFetched(FileFetcher fetcher, Throwable e);
  62. }
  63. private static final class RootItem {
  64. public final String fs_specLC, pathLC, path;
  65. public final boolean isFileSystemTypeValid;
  66. public RootItem(String fs_spec, String path, boolean isFileSystemTypeValid) {
  67. this.fs_specLC = fs_spec.toLowerCase(Locale.US);
  68. this.pathLC = path.toLowerCase(Locale.US);
  69. this.path = path;
  70. this.isFileSystemTypeValid = isFileSystemTypeValid;
  71. }
  72. @Override
  73. public int hashCode() {
  74. return fs_specLC.hashCode();
  75. }
  76. @Override
  77. public boolean equals(Object o) {
  78. if (o instanceof String)
  79. return fs_specLC.equals(o);
  80. return fs_specLC.equals(((RootItem)o).fs_specLC);
  81. }
  82. }
  83. private static final int LIST_DELTA = 32;
  84. private static final HashSet<String> supportedTypes;
  85. public final String path;
  86. public FileSt[] files;
  87. public String[] sections;
  88. public int[] sectionPositions;
  89. public int count;
  90. public final boolean playAfterFetching, isInTouchMode, createSections;
  91. private Throwable notifyE;
  92. private Listener listener;
  93. private boolean recursive;
  94. private final boolean notifyFromMain, recursiveIfFirstEmpty;
  95. private volatile boolean cancelled;
  96. static {
  97. //http://developer.android.com/guide/appendix/media-formats.html
  98. supportedTypes = new HashSet<>(21);
  99. supportedTypes.add(".3gp");
  100. supportedTypes.add(".3ga");
  101. supportedTypes.add(".3gpa");
  102. supportedTypes.add(".mp4");
  103. supportedTypes.add(".m4a");
  104. supportedTypes.add(".aac");
  105. supportedTypes.add(".mp3");
  106. supportedTypes.add(".mid");
  107. supportedTypes.add(".rmi");
  108. supportedTypes.add(".xmf");
  109. supportedTypes.add(".mxmf");
  110. supportedTypes.add(".rtttl");
  111. supportedTypes.add(".rtx");
  112. supportedTypes.add(".ota");
  113. supportedTypes.add(".imy");
  114. supportedTypes.add(".ogg");
  115. supportedTypes.add(".oga");
  116. supportedTypes.add(".wav");
  117. supportedTypes.add(".mka");
  118. supportedTypes.add(".mkv");
  119. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1)
  120. supportedTypes.add(".flac");
  121. }
  122. @Override
  123. public boolean accept(File file) {
  124. if (file.isDirectory()) return true;
  125. final String name = file.getName();
  126. final int i = name.lastIndexOf('.');
  127. return ((i >= 0) && supportedTypes.contains(name.substring(i).toLowerCase(Locale.US)));
  128. }
  129. public static boolean isFileAcceptable(String name) {
  130. int i;
  131. return (name != null && (i = name.lastIndexOf('.')) >= 0 && supportedTypes.contains(name.substring(i).toLowerCase(Locale.US)));
  132. }
  133. public static FileFetcher fetchFiles(String path, Listener listener, boolean notifyFromMain, boolean recursive, boolean isInTouchMode, boolean createSections) {
  134. FileFetcher f = new FileFetcher(path, listener, notifyFromMain, recursive, false, false, isInTouchMode, createSections);
  135. f.fetch();
  136. return f;
  137. }
  138. public static FileFetcher fetchFilesInThisThread(String path, Listener listener, boolean notifyFromMain, boolean recursive, boolean recursiveIfFirstEmpty, boolean playAfterFetching, boolean createSections) {
  139. FileFetcher f = new FileFetcher(path, listener, notifyFromMain, recursive, recursiveIfFirstEmpty, playAfterFetching, false, createSections);
  140. f.run();
  141. return f;
  142. }
  143. private FileFetcher(String path, Listener listener, boolean notifyFromMain, boolean recursive, boolean recursiveIfFirstEmpty, boolean playAfterFetching, boolean isInTouchMode, boolean createSections) {
  144. this.files = new FileSt[LIST_DELTA];
  145. this.path = path;
  146. this.listener = listener;
  147. this.notifyFromMain = notifyFromMain;
  148. this.recursive = recursive;
  149. this.recursiveIfFirstEmpty = recursiveIfFirstEmpty;
  150. this.playAfterFetching = playAfterFetching;
  151. this.isInTouchMode = isInTouchMode;
  152. this.createSections = createSections;
  153. this.count = 0;
  154. }
  155. private void fetch() {
  156. (new Thread(this, "File Fetcher Thread")).start();
  157. }
  158. public Throwable getThrowedException() {
  159. return notifyE;
  160. }
  161. private void ensureCapacity(int capacity) {
  162. if (capacity < count)
  163. return;
  164. if (capacity > files.length ||
  165. capacity <= (files.length - (2 * LIST_DELTA))) {
  166. capacity += LIST_DELTA;
  167. } else {
  168. return;
  169. }
  170. files = Arrays.copyOf(files, capacity);
  171. }
  172. private void addStorage(Service s, File path, boolean isExternal, int[] internalCount, int[] externalCount, int[] usbCount, int[] addedCount, String[] addedPaths) throws Throwable {
  173. if (!path.exists() || !path.isDirectory())
  174. return;
  175. int c = addedCount[0];
  176. if (c >= addedPaths.length)
  177. return;
  178. //readlink command could be used instead? which is better???
  179. //http://www.computerhope.com/unix/readlink.htm
  180. String canonicalPath = path.getCanonicalFile().getAbsolutePath();
  181. String canonicalPathLC = canonicalPath.toLowerCase(Locale.US);
  182. try {
  183. //limit the amount of iterations while resolving symlinks
  184. for (int i = 4; i > 0; i--) {
  185. final File file = new File(canonicalPath);
  186. final String p = file.getCanonicalFile().getAbsolutePath();
  187. final String pLC = p.toLowerCase(Locale.US);
  188. if (pLC.equals(canonicalPathLC))
  189. break;
  190. canonicalPath = p;
  191. canonicalPathLC = pLC;
  192. }
  193. } catch (Throwable ex) {
  194. ex.printStackTrace();
  195. }
  196. for (int i = c - 1; i >= 0; i--) {
  197. if (canonicalPathLC.equals(addedPaths[i]))
  198. return;
  199. }
  200. addedPaths[c] = canonicalPathLC;
  201. addedCount[0] = c + 1;
  202. if (isExternal) {
  203. if (canonicalPathLC.contains("usb")) {
  204. c = usbCount[0] + 1;
  205. files[count] = new FileSt(canonicalPath, s.getText(R.string.usb_storage).toString() + ((c <= 1) ? "" : (" " + Integer.toString(c))), null, FileSt.TYPE_EXTERNAL_STORAGE_USB);
  206. usbCount[0] = c;
  207. } else {
  208. //try to avoid duplication of internal sdcard on a few phones...
  209. if (internalCount[0] > 0 && canonicalPathLC.contains("/legacy")) {
  210. //ignore this path in addedPaths
  211. addedCount[0]--;
  212. addedPaths[addedCount[0]] = null;
  213. return;
  214. }
  215. c = externalCount[0] + 1;
  216. files[count] = new FileSt(canonicalPath, s.getText(R.string.external_storage).toString() + ((c <= 1) ? "" : (" " + Integer.toString(c))), null, FileSt.TYPE_EXTERNAL_STORAGE);
  217. externalCount[0] = c;
  218. }
  219. } else {
  220. files[count] = new FileSt(canonicalPath, s.getText(R.string.internal_storage).toString(), null, FileSt.TYPE_INTERNAL_STORAGE);
  221. internalCount[0]++;
  222. }
  223. count++;
  224. }
  225. private void fetchRoot() {
  226. final Service s = Player.getService();
  227. if (s == null)
  228. return;
  229. files = Player.getFavoriteFolders(16);
  230. count = files.length - 16;
  231. String desc = s.getText(R.string.artists).toString();
  232. files[count] = new FileSt(FileSt.ARTIST_ROOT + FileSt.FAKE_PATH_ROOT + desc, desc, null, FileSt.TYPE_ARTIST_ROOT);
  233. count++;
  234. desc = s.getText(R.string.albums).toString();
  235. files[count] = new FileSt(FileSt.ALBUM_ROOT + FileSt.FAKE_PATH_ROOT + desc, desc, null, FileSt.TYPE_ALBUM_ROOT);
  236. count++;
  237. File f;
  238. try {
  239. f = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC);
  240. if (f.exists() && f.isDirectory()) {
  241. files[count] = new FileSt(f.getAbsolutePath(), s.getText(R.string.music).toString(), null, FileSt.TYPE_MUSIC);
  242. count++;
  243. }
  244. } catch (Throwable ex) {
  245. ex.printStackTrace();
  246. }
  247. try {
  248. f = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
  249. if (f.exists() && f.isDirectory()) {
  250. files[count] = new FileSt(f.getAbsolutePath(), s.getText(R.string.downloads).toString(), null, FileSt.TYPE_DOWNLOADS);
  251. count++;
  252. }
  253. } catch (Throwable ex) {
  254. ex.printStackTrace();
  255. }
  256. if (cancelled)
  257. return;
  258. int i;
  259. int[] internalCount = new int[1], externalCount = new int[1], usbCount = new int[1], addedCount = new int[1];
  260. String[] addedPaths = new String[16];
  261. String path;
  262. try {
  263. addStorage(s, Environment.getExternalStorageDirectory(), Environment.isExternalStorageRemovable(), internalCount, externalCount, usbCount, addedCount, addedPaths);
  264. } catch (Throwable ex) {
  265. ex.printStackTrace();
  266. }
  267. //the following is an improved version based on these ideas:
  268. //http://sapienmobile.com/?p=204
  269. //http://stackoverflow.com/questions/11281010/how-can-i-get-external-sd-card-path-for-android-4-0
  270. /*try {
  271. path = System.getenv("SECONDARY_STORAGE");
  272. if (path != null && path.length() > 0) {
  273. //this file helps clarifying this ':' a little bit...
  274. //http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.4.2_r1/android/os/Environment.java
  275. int start = path.indexOf(':');
  276. if (start < 0) {
  277. addStorage(s, new File(path), true, internalCount, externalCount, usbCount, addedCount, addedPaths);
  278. } else {
  279. //avoid using split ;)
  280. int end;
  281. do {
  282. end = path.indexOf(':', start + 1);
  283. if (end <= start) end = path.length();
  284. addStorage(s, new File(path.substring(start, end)), true, internalCount, externalCount, usbCount, addedCount, addedPaths);
  285. start = end + 1;
  286. } while (end < path.length());
  287. }
  288. }
  289. } catch (Throwable ex) {
  290. }
  291. if (cancelled)
  292. return;*/
  293. InputStream is = null;
  294. InputStreamReader isr = null;
  295. BufferedReader br = null;
  296. try {
  297. final HashMap<String, RootItem> map = new HashMap<>(32);
  298. String line;
  299. is = Runtime.getRuntime().exec("mount").getInputStream();
  300. isr = new InputStreamReader(is);
  301. br = new BufferedReader(isr);
  302. while ((line = br.readLine()) != null) {
  303. if (cancelled)
  304. break;
  305. //skip secure storages (accessing them usually result in
  306. //a permission denied error)
  307. if (line.length() == 0 ||
  308. line.contains("secure") ||
  309. line.contains("asec")) continue;
  310. //http://unix.stackexchange.com/questions/91960/can-anyone-explain-the-output-of-mount
  311. //every line returned by mount must be seen as
  312. //fs_spec fs_file fs_vfstype [fs_mntopts - optional]
  313. final int first = line.indexOf(' ');
  314. if (first <= 0) continue;
  315. final int second = line.indexOf(' ', first + 1);
  316. if (second <= first) continue;
  317. final String fs_spec = line.substring(0, first);
  318. path = line.substring(first + 1, second);
  319. //fuse is used for internal storage
  320. final RootItem item = new RootItem(fs_spec, path,
  321. line.contains("fat") ||
  322. line.contains("fuse") ||
  323. //line.contains("ntfs") || //would this be safe?????
  324. //there are a few "interesting" mount points starting with /mnt/ ;)
  325. (path.startsWith("/mnt/") && !fs_spec.equals("tmpfs")));
  326. map.put(item.fs_specLC, item);
  327. }
  328. for (RootItem item : map.values()) {
  329. if (cancelled)
  330. break;
  331. if (item.isFileSystemTypeValid) {
  332. RootItem tmp, it = item;
  333. //try to get the actual path pointed by this item, using a
  334. //poor man's cycle prevention ;)
  335. i = 0;
  336. while (i < 4 && (tmp = map.get(it.pathLC)) != null) {
  337. it = tmp;
  338. i++;
  339. }
  340. try {
  341. //a few old phones erroneously return these 3 as mounted devices
  342. if (!it.pathLC.equals("/system") && !it.pathLC.equals("/data") && !it.pathLC.equals("/cache"))
  343. addStorage(s, new File(it.path), true, internalCount, externalCount, usbCount, addedCount, addedPaths);
  344. } catch (Throwable ex) {
  345. ex.printStackTrace();
  346. }
  347. }
  348. }
  349. } catch (Throwable ex) {
  350. ex.printStackTrace();
  351. } finally {
  352. if (br != null) {
  353. try {
  354. br.close();
  355. } catch (Throwable ex) {
  356. ex.printStackTrace();
  357. }
  358. }
  359. if (isr != null) {
  360. try {
  361. isr.close();
  362. } catch (Throwable ex) {
  363. ex.printStackTrace();
  364. }
  365. }
  366. if (is != null) {
  367. try {
  368. is.close();
  369. } catch (Throwable ex) {
  370. ex.printStackTrace();
  371. }
  372. }
  373. }
  374. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
  375. fetchRoot19(s, internalCount, externalCount, usbCount, addedCount, addedPaths);
  376. if (count < files.length) {
  377. files[count] = new FileSt(File.separator, s.getText(R.string.all_files).toString(), null, FileSt.TYPE_ALL_FILES);
  378. count++;
  379. }
  380. }
  381. @TargetApi(Build.VERSION_CODES.KITKAT)
  382. private void fetchRoot19(Service s, int[] internalCount, int[] externalCount, int[] usbCount, int[] addedCount, String[] addedPaths) {
  383. //Massive workaround! This is a desperate attempt to fetch all possible directories
  384. //in newer CM and others...
  385. try {
  386. final File[] fs = s.getExternalFilesDirs(null);
  387. if (fs != null) {
  388. for (int i = 0; i < fs.length; i++) {
  389. final String p = fs[i].getAbsolutePath();
  390. final int a = p.indexOf("Android");
  391. if (a <= 0)
  392. continue;
  393. addStorage(s, new File(p.substring(0, a - 1)), true, internalCount, externalCount, usbCount, addedCount, addedPaths);
  394. }
  395. }
  396. } catch (Throwable ex) {
  397. ex.printStackTrace();
  398. }
  399. }
  400. private void fetchArtists() {
  401. final Service s = Player.getService();
  402. if (s == null)
  403. return;
  404. final String fakeRoot = FileSt.FAKE_PATH_ROOT + s.getText(R.string.artists).toString() + FileSt.FAKE_PATH_SEPARATOR;
  405. final String root = FileSt.ARTIST_ROOT + File.separator;
  406. //apparently a few devices don't like these members, so I converted them to the hardcoded version!
  407. final String[] proj = { "_id", "artist", "number_of_albums", "number_of_tracks" };
  408. final Cursor c = s.getContentResolver().query(Uri.parse("content://media/external/audio/artists"), proj, null, null, null);
  409. //
  410. //Despite its name, EXTERNAL_CONTENT_URI also comprises the internal storage
  411. //(at least it does so in all devices I have tested!)
  412. //
  413. //final String[] proj = { MediaStore.Audio.Artists._ID, MediaStore.Audio.Artists.ARTIST, MediaStore.Audio.Artists.NUMBER_OF_ALBUMS, MediaStore.Audio.Artists.NUMBER_OF_TRACKS };
  414. //final Cursor c = s.getContentResolver().query(MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI, proj, null, null, null);
  415. if (c == null) {
  416. count = 0;
  417. files = new FileSt[0];
  418. return;
  419. }
  420. final ArrayList<FileSt> tmp = new ArrayList<>(64);
  421. while (c.moveToNext()) {
  422. if (cancelled || Player.state >= Player.STATE_TERMINATING) {
  423. count = 0;
  424. c.close();
  425. return;
  426. }
  427. String name = c.getString(1);
  428. if (name == null || name.equals("<unknown>"))
  429. name = UI.unknownArtist;
  430. final long id = c.getLong(0);
  431. final FileSt f = new FileSt(root + id + fakeRoot + name, name, null, FileSt.TYPE_ARTIST);
  432. f.artistIdForAlbumArt = id;
  433. f.albums = c.getInt(2);
  434. f.tracks = c.getInt(3);
  435. tmp.add(f);
  436. }
  437. c.close();
  438. count = tmp.size();
  439. files = new FileSt[count];
  440. tmp.toArray(files);
  441. ArraySorter.sort(files, 0, files.length, new ArraySorter.Comparer<FileSt>() {
  442. @SuppressWarnings("StringEquality")
  443. @Override
  444. public int compare(FileSt a, FileSt b) {
  445. if (a.name == UI.unknownArtist)
  446. return -1;
  447. else if (b.name == UI.unknownArtist)
  448. return 1;
  449. return a.name.compareToIgnoreCase(b.name);
  450. }
  451. });
  452. tmp.clear();
  453. }
  454. private void fetchAlbums(String path) {
  455. final Service s = Player.getService();
  456. if (s == null)
  457. return;
  458. final String artist;
  459. final String fakeRoot;
  460. final String root;
  461. if (path == null) {
  462. artist = null;
  463. fakeRoot = FileSt.FAKE_PATH_ROOT + s.getText(R.string.albums).toString() + FileSt.FAKE_PATH_SEPARATOR;
  464. root = FileSt.ALBUM_ROOT + File.separator;
  465. } else {
  466. final int fakePathIdx = path.indexOf(FileSt.FAKE_PATH_ROOT_CHAR);
  467. final String realPath = path.substring(0, fakePathIdx);
  468. final String fakePath = path.substring(fakePathIdx);
  469. artist = realPath.substring(realPath.lastIndexOf(File.separatorChar) + 1);
  470. fakeRoot = fakePath + FileSt.FAKE_PATH_SEPARATOR;
  471. root = realPath + File.separator;
  472. }
  473. //apparently a few devices don't like these members, so I converted them to the hardcoded version!
  474. final String[] proj = { "_id", "album", "album_art", "numsongs" };
  475. final Cursor c = s.getContentResolver().query(Uri.parse((artist == null) ?
  476. "content://media/external/audio/albums" :
  477. "content://media/external/audio/artists/" + artist + "/albums"), proj, null, null, null);
  478. //final String[] proj = { MediaStore.Audio.Albums._ID, MediaStore.Audio.Albums.ALBUM, MediaStore.Audio.Albums.ALBUM_ART, MediaStore.Audio.Albums.NUMBER_OF_SONGS };
  479. //final Cursor c = s.getContentResolver().query((artist == null) ?
  480. // MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI :
  481. // MediaStore.Audio.Artists.Albums.getContentUri("external", Long.parseLong(artist)), proj, null, null, null);
  482. if (c == null) {
  483. count = 0;
  484. files = new FileSt[0];
  485. return;
  486. }
  487. final ArrayList<FileSt> tmp = new ArrayList<>(64);
  488. while (c.moveToNext()) {
  489. if (cancelled || Player.state >= Player.STATE_TERMINATING) {
  490. count = 0;
  491. c.close();
  492. return;
  493. }
  494. final String name = c.getString(1);
  495. final FileSt f = new FileSt(root + c.getLong(0) + fakeRoot + name, name, c.getString(2), FileSt.TYPE_ALBUM);
  496. f.tracks = c.getInt(3);
  497. tmp.add(f);
  498. }
  499. c.close();
  500. count = tmp.size();
  501. files = new FileSt[count];
  502. tmp.toArray(files);
  503. ArraySorter.sort(files, 0, files.length, this);
  504. tmp.clear();
  505. }
  506. private void fetchTracks(String path) {
  507. final Service s = Player.getService();
  508. if (s == null)
  509. return;
  510. final int fakePathIdx = path.indexOf(FileSt.FAKE_PATH_ROOT_CHAR);
  511. final String realPath = path.substring(0, fakePathIdx);
  512. final int albumIdIdx = realPath.lastIndexOf(File.separatorChar) + 1;
  513. final String artist = ((realPath.charAt(0) == FileSt.ARTIST_ROOT_CHAR) ? realPath.substring(2, albumIdIdx - 1) : null);
  514. final String album = realPath.substring(albumIdIdx);
  515. //apparently a few devices don't like these members, so I converted them to the hardcoded version!
  516. final String[] proj = { "_data", "title", "track" };
  517. final Cursor c = s.getContentResolver().query(
  518. Uri.parse("content://media/external/audio/media"), proj,
  519. (artist == null) ?
  520. "album_id=?" :
  521. "album_id=? AND artist_id=?",
  522. (artist == null) ?
  523. new String[] { album } :
  524. new String[] { album, artist },
  525. null);
  526. //final String[] proj = { MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.TRACK };
  527. //final Cursor c = s.getContentResolver().query(
  528. // MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, proj,
  529. // (artist == null) ?
  530. // (MediaStore.Audio.Media.ALBUM_ID + "=?") :
  531. // (MediaStore.Audio.Media.ALBUM_ID + "=? AND " + MediaStore.Audio.Media.ARTIST_ID + "=?"),
  532. // (artist == null) ?
  533. // new String[] { album } :
  534. // new String[] { album, artist },
  535. // null);
  536. if (c == null) {
  537. count = 0;
  538. files = new FileSt[0];
  539. return;
  540. }
  541. final ArrayList<FileSt> tmp = new ArrayList<>(64);
  542. while (c.moveToNext()) {
  543. if (cancelled || Player.state >= Player.STATE_TERMINATING) {
  544. count = 0;
  545. c.close();
  546. return;
  547. }
  548. //temporarily use specialType as the song's track number ;)
  549. tmp.add(new FileSt(c.getString(0), c.getString(1), c.getInt(2)));
  550. }
  551. c.close();
  552. count = tmp.size();
  553. files = new FileSt[count];
  554. tmp.toArray(files);
  555. ArraySorter.sort(files, 0, files.length, new ArraySorter.Comparer<FileSt>() {
  556. @Override
  557. public int compare(FileSt a, FileSt b) {
  558. if (a.specialType != b.specialType)
  559. return a.specialType - b.specialType;
  560. return a.name.compareToIgnoreCase(b.name);
  561. }
  562. });
  563. for (int i = files.length - 1; i >= 0; i--)
  564. files[i].specialType = 0;
  565. tmp.clear();
  566. }
  567. @SuppressWarnings("UnusedAssignment")
  568. private void fetchFiles(String path, boolean first) {
  569. if (cancelled || Player.state >= Player.STATE_TERMINATING) {
  570. count = 0;
  571. return;
  572. }
  573. int i;
  574. File root = new File((path.charAt(path.length() - 1) == File.separatorChar) ? path : (path + File.separator));
  575. File[] files = root.listFiles(this);
  576. boolean filesAdded = false;
  577. if (files == null || files.length == 0) {
  578. if (this.files == null)
  579. this.files = new FileSt[0];
  580. return;
  581. }
  582. final int l = count;
  583. ensureCapacity(count + files.length);
  584. for (i = 0; i < files.length; i++) {
  585. if (cancelled || Player.state >= Player.STATE_TERMINATING) {
  586. count = 0;
  587. return;
  588. }
  589. this.files[count] = new FileSt(files[i]);
  590. if (!this.files[count].isDirectory)
  591. filesAdded = true;
  592. count++;
  593. files[i] = null;
  594. }
  595. files = null; //help the garbage collector
  596. final int e = count;
  597. ArraySorter.sort(this.files, l, e - l, this);
  598. if (first && !filesAdded && recursiveIfFirstEmpty)
  599. recursive = true;
  600. if (!recursive)
  601. return;
  602. for (i = l; i < e; i++) {
  603. if (cancelled || Player.state >= Player.STATE_TERMINATING) {
  604. count = 0;
  605. return;
  606. }
  607. if (this.files[i].isDirectory)
  608. fetchFiles(this.files[i].path, false);
  609. }
  610. }
  611. private void fetchPrivateFiles(String fileType) {
  612. if (cancelled || Player.state >= Player.STATE_TERMINATING) {
  613. count = 0;
  614. return;
  615. }
  616. final String[] files = Player.getService().fileList();
  617. if (files == null || files.length == 0) {
  618. if (this.files == null)
  619. this.files = new FileSt[0];
  620. return;
  621. }
  622. ensureCapacity(files.length);
  623. int i, c = 0;
  624. final int l = fileType.length();
  625. for (i = files.length - 1; i >= 0; i--) {
  626. if (cancelled || Player.state >= Player.STATE_TERMINATING) {
  627. count = 0;
  628. return;
  629. }
  630. final String f = files[i];
  631. if (files[i].endsWith(fileType)) {
  632. this.files[c] = new FileSt(f, f.substring(0, f.length() - l), null, 0);
  633. c++;
  634. }
  635. }
  636. count = c;
  637. ArraySorter.sort(this.files, 0, c, this);
  638. }
  639. @SuppressWarnings("StringEquality")
  640. public void computeSections() {
  641. if (!createSections || count < 1) {
  642. sections = null;
  643. sectionPositions = null;
  644. return;
  645. }
  646. int sectionIdx = 0, i = 1, currentCount = 1;
  647. int[] charCount = new int[100]; //no more than 100 groups allowed
  648. int[] pos = new int[100];
  649. char[] chars = new char[100];
  650. char current, last = (char)Character.toUpperCase((int)files[0].name.charAt(0));
  651. if (last < '@' || files[0].name == UI.unknownArtist)
  652. last = '#';
  653. chars[0] = last;
  654. pos[0] = 0;
  655. while (i < count && sectionIdx < 100) {
  656. current = (char)Character.toUpperCase((int)files[i].name.charAt(0));
  657. if (current < '@')
  658. current = '#';
  659. if (current != last) {
  660. charCount[sectionIdx] = currentCount;
  661. sectionIdx++;
  662. currentCount = 1;
  663. last = current;
  664. if (sectionIdx < 100) {
  665. chars[sectionIdx] = last;
  666. pos[sectionIdx] = i;
  667. }
  668. } else {
  669. currentCount++;
  670. }
  671. i++;
  672. }
  673. if (currentCount != 0 && sectionIdx < 100) {
  674. charCount[sectionIdx] = currentCount;
  675. sectionIdx++;
  676. }
  677. //we must not create more than 28 sections
  678. if (sectionIdx > 28) {
  679. //sort by charCount (ignoring the first section, which is always included)
  680. //a insertion-sort-like sort should do it :)
  681. for (i = 2; i < sectionIdx; i++) {
  682. final int c = charCount[i];
  683. final int p = pos[i];
  684. final char ch = chars[i];
  685. int j = i - 1;
  686. //ignore section 0
  687. while (j > 0 && c > charCount[j]) {
  688. charCount[j + 1] = charCount[j];
  689. pos[j + 1] = pos[j];
  690. chars[j + 1] = chars[j];
  691. charCount[j] = c;
  692. pos[j] = p;
  693. chars[j] = ch;
  694. j--;
  695. }
  696. }
  697. sectionIdx = 28;
  698. //now we take the first 28 sections, and sort them by pos
  699. for (i = 2; i < 28; i++) {
  700. final int p = pos[i];
  701. final char ch = chars[i];
  702. int j = i - 1;
  703. //ignore section 0
  704. while (j > 0 && p < pos[j]) {
  705. pos[j + 1] = pos[j];
  706. chars[j + 1] = chars[j];
  707. pos[j] = p;
  708. chars[j] = ch;
  709. j--;
  710. }
  711. }
  712. }
  713. sections = new String[sectionIdx];
  714. sectionPositions = new int[sectionIdx];
  715. for (i = sectionIdx - 1; i >= 0; i--) {
  716. sections[i] = Character.toString(chars[i]);
  717. sectionPositions[i] = pos[i];
  718. }
  719. }
  720. @Override
  721. public int compare(FileSt a, FileSt b) {
  722. if (a.isDirectory == b.isDirectory)
  723. return a.name.compareToIgnoreCase(b.name);
  724. return (a.isDirectory ? -1 : 1);
  725. }
  726. public void cancel() {
  727. cancelled = true;
  728. }
  729. @Override
  730. public void run() {
  731. if (MainHandler.isOnMainThread()) {
  732. if (listener != null && !cancelled && Player.state < Player.STATE_TERMINATING)
  733. listener.onFilesFetched(this, notifyE);
  734. listener = null;
  735. notifyE = null;
  736. return;
  737. }
  738. Throwable e = null;
  739. try {
  740. if (path == null || path.length() == 0) {
  741. fetchRoot();
  742. } else if (path.charAt(0) == FileSt.PRIVATE_FILETYPE_ID) {
  743. fetchPrivateFiles(path);
  744. } else if (path.charAt(0) == FileSt.ARTIST_ROOT_CHAR) {
  745. if (path.startsWith(FileSt.ARTIST_PREFIX)) {
  746. fetchArtists();
  747. computeSections();
  748. } else {
  749. final int p1 = path.indexOf(File.separatorChar);
  750. final int p2 = path.lastIndexOf(File.separatorChar, path.indexOf(FileSt.FAKE_PATH_ROOT_CHAR));
  751. if (p2 != p1) {
  752. fetchTracks(path);
  753. } else {
  754. fetchAlbums(path);
  755. //we actually need to fetch all tracks from all this artist's albums...
  756. final FileSt[] albums = files;
  757. final ArrayList<FileSt> tracks = new ArrayList<>(albums.length * 11);
  758. for (int i = 0; i < albums.length; i++) {
  759. if (cancelled || Player.state >= Player.STATE_TERMINATING) {
  760. count = 0;
  761. tracks.clear();
  762. break;
  763. }
  764. try {
  765. fetchTracks(albums[i].path);
  766. if (files != null && count > 0) {
  767. tracks.ensureCapacity(tracks.size() + count);
  768. albums[i].specialType = FileSt.TYPE_ALBUM_ITEM;
  769. tracks.add(albums[i]);
  770. for (int j = 0; j < count; j++) {
  771. tracks.add(files[j]);
  772. files[j] = null;
  773. }
  774. files = null;
  775. }
  776. } catch (Throwable ex) {
  777. e = ex;
  778. }
  779. albums[i] = null;
  780. }
  781. if (tracks.size() > 0) {
  782. //ignore any errors if at least one track was fetched
  783. e = null;
  784. count = tracks.size();
  785. files = new FileSt[count];
  786. tracks.toArray(files);
  787. tracks.clear();
  788. } else {
  789. count = 0;
  790. if (files == null)
  791. files = new FileSt[0];
  792. }
  793. }
  794. }
  795. } else if (path.charAt(0) == FileSt.ALBUM_ROOT_CHAR) {
  796. if (path.startsWith(FileSt.ALBUM_PREFIX)) {
  797. fetchAlbums(null);
  798. computeSections();
  799. } else {
  800. fetchTracks(path);
  801. }
  802. } else {
  803. fetchFiles(path, true);
  804. computeSections();
  805. }
  806. } catch (Throwable ex) {
  807. e = ex;
  808. }
  809. if (!cancelled && Player.state < Player.STATE_TERMINATING) {
  810. if (listener != null) {
  811. if (notifyFromMain) {
  812. notifyE = e;
  813. MainHandler.postToMainThread(this);
  814. } else {
  815. listener.onFilesFetched(this, e);
  816. listener = null;
  817. }
  818. } else {
  819. notifyE = e;
  820. }
  821. }
  822. }
  823. }