Async song matching in the FoldersFragment.
This commit is contained in:
parent
2f8426f0c9
commit
a4943b2b68
3 changed files with 385 additions and 196 deletions
|
|
@ -2,15 +2,18 @@ package com.kabouzeid.gramophone.ui.fragments.mainactivity.folders;
|
||||||
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.Dialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.content.DialogInterface;
|
||||||
import android.media.MediaScannerConnection;
|
import android.media.MediaScannerConnection;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
import android.provider.MediaStore.Audio.AudioColumns;
|
import android.os.Handler;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.annotation.StringRes;
|
||||||
import android.support.design.widget.AppBarLayout;
|
import android.support.design.widget.AppBarLayout;
|
||||||
import android.support.design.widget.CoordinatorLayout;
|
import android.support.design.widget.CoordinatorLayout;
|
||||||
import android.support.design.widget.Snackbar;
|
import android.support.design.widget.Snackbar;
|
||||||
|
|
@ -31,6 +34,8 @@ import android.widget.PopupMenu;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.afollestad.materialcab.MaterialCab;
|
import com.afollestad.materialcab.MaterialCab;
|
||||||
|
import com.afollestad.materialdialogs.DialogAction;
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog;
|
||||||
import com.kabouzeid.appthemehelper.ThemeStore;
|
import com.kabouzeid.appthemehelper.ThemeStore;
|
||||||
import com.kabouzeid.appthemehelper.common.ATHToolbarActivity;
|
import com.kabouzeid.appthemehelper.common.ATHToolbarActivity;
|
||||||
import com.kabouzeid.appthemehelper.util.ToolbarContentTintHelper;
|
import com.kabouzeid.appthemehelper.util.ToolbarContentTintHelper;
|
||||||
|
|
@ -40,12 +45,11 @@ import com.kabouzeid.gramophone.dialogs.AddToPlaylistDialog;
|
||||||
import com.kabouzeid.gramophone.dialogs.DeleteSongsDialog;
|
import com.kabouzeid.gramophone.dialogs.DeleteSongsDialog;
|
||||||
import com.kabouzeid.gramophone.helper.MusicPlayerRemote;
|
import com.kabouzeid.gramophone.helper.MusicPlayerRemote;
|
||||||
import com.kabouzeid.gramophone.interfaces.CabHolder;
|
import com.kabouzeid.gramophone.interfaces.CabHolder;
|
||||||
import com.kabouzeid.gramophone.loader.SongLoader;
|
|
||||||
import com.kabouzeid.gramophone.loader.SortedCursor;
|
|
||||||
import com.kabouzeid.gramophone.misc.WrappedAsyncTaskLoader;
|
import com.kabouzeid.gramophone.misc.WrappedAsyncTaskLoader;
|
||||||
import com.kabouzeid.gramophone.model.Song;
|
import com.kabouzeid.gramophone.model.Song;
|
||||||
import com.kabouzeid.gramophone.ui.activities.MainActivity;
|
import com.kabouzeid.gramophone.ui.activities.MainActivity;
|
||||||
import com.kabouzeid.gramophone.ui.fragments.mainactivity.AbsMainActivityFragment;
|
import com.kabouzeid.gramophone.ui.fragments.mainactivity.AbsMainActivityFragment;
|
||||||
|
import com.kabouzeid.gramophone.util.FileUtil;
|
||||||
import com.kabouzeid.gramophone.util.PhonographColorUtil;
|
import com.kabouzeid.gramophone.util.PhonographColorUtil;
|
||||||
import com.kabouzeid.gramophone.util.PreferenceUtil;
|
import com.kabouzeid.gramophone.util.PreferenceUtil;
|
||||||
import com.kabouzeid.gramophone.util.ViewUtil;
|
import com.kabouzeid.gramophone.util.ViewUtil;
|
||||||
|
|
@ -57,7 +61,6 @@ import java.io.FileFilter;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
|
@ -65,12 +68,18 @@ import java.util.List;
|
||||||
|
|
||||||
import butterknife.Bind;
|
import butterknife.Bind;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
|
import hugo.weaving.DebugLog;
|
||||||
|
|
||||||
public class FoldersFragment extends AbsMainActivityFragment implements MainActivity.MainActivityFragmentCallbacks, CabHolder, BreadCrumbLayout.SelectionCallback, SongFileAdapter.Callbacks, AppBarLayout.OnOffsetChangedListener, LoaderManager.LoaderCallbacks<List<File>> {
|
public class FoldersFragment extends AbsMainActivityFragment implements MainActivity.MainActivityFragmentCallbacks, CabHolder, BreadCrumbLayout.SelectionCallback, SongFileAdapter.Callbacks, AppBarLayout.OnOffsetChangedListener, LoaderManager.LoaderCallbacks<List<File>> {
|
||||||
public static final String TAG = FoldersFragment.class.getSimpleName();
|
public static final String TAG = FoldersFragment.class.getSimpleName();
|
||||||
|
|
||||||
private static final int LOADER_ID = 1;
|
private static final int LOADER_ID = 1;
|
||||||
|
|
||||||
|
private static final int ADD_TO_PLAYLIST = 0;
|
||||||
|
private static final int ADD_TO_CURRENT_PLAYING = 1;
|
||||||
|
private static final int DELETE = 2;
|
||||||
|
private static final int PLAY = 3;
|
||||||
|
|
||||||
protected static final String PATH = "path";
|
protected static final String PATH = "path";
|
||||||
protected static final String CRUMBS = "crumbs";
|
protected static final String CRUMBS = "crumbs";
|
||||||
|
|
||||||
|
|
@ -292,83 +301,20 @@ public class FoldersFragment extends AbsMainActivityFragment implements MainActi
|
||||||
if (file.isDirectory()) {
|
if (file.isDirectory()) {
|
||||||
setCrumb(new BreadCrumbLayout.Crumb(file), true);
|
setCrumb(new BreadCrumbLayout.Crumb(file), true);
|
||||||
} else {
|
} else {
|
||||||
List<File> files = listFilesDeep(file.getParentFile(), new FileFilter() {
|
List<File> files = new LinkedList<>();
|
||||||
|
files.add(file.getParentFile());
|
||||||
|
|
||||||
|
FileFilter fileFilter = new FileFilter() {
|
||||||
@Override
|
@Override
|
||||||
public boolean accept(File pathname) {
|
public boolean accept(File pathname) {
|
||||||
return !pathname.isDirectory() && getFileFilter().accept(pathname);
|
return !pathname.isDirectory() && getFileFilter().accept(pathname);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
sort(files);
|
|
||||||
ArrayList<Song> songs = matchFilesWithMediaStore(files);
|
|
||||||
|
|
||||||
int startIndex = -1;
|
new ListSongsAsyncTask(this, PLAY, file).execute(new ListSongsAsyncTask.LoadingInfo(files, fileFilter, getFileComparator()));
|
||||||
for (int i = 0; i < songs.size(); i++) {
|
|
||||||
if (file.getPath().equals(songs.get(i).data)) { // path is already canonical here
|
|
||||||
startIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (startIndex > -1) {
|
|
||||||
MusicPlayerRemote.openQueue(songs, startIndex, true);
|
|
||||||
} else {
|
|
||||||
final File finalFile = file;
|
|
||||||
Snackbar.make(coordinatorLayout, Html.fromHtml(String.format(getString(R.string.not_listed_in_media_store), file.getName())), Snackbar.LENGTH_LONG)
|
|
||||||
.setAction(R.string.action_scan, new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
scan(finalFile);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.setActionTextColor(ThemeStore.accentColor(getActivity()))
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private static SortedCursor makeSongCursor(@NonNull final Context context, @Nullable final List<File> files) {
|
|
||||||
String selection = null;
|
|
||||||
String[] paths = null;
|
|
||||||
|
|
||||||
if (files != null) {
|
|
||||||
paths = toPathArray(files);
|
|
||||||
|
|
||||||
if (files.size() > 0 && files.size() < 999) { // 999 is the max amount Androids SQL implementation can handle.
|
|
||||||
selection = AudioColumns.DATA + " IN (" + makePlaceholders(files.size()) + ")";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Cursor songCursor = SongLoader.makeSongCursor(context, selection, selection == null ? null : paths);
|
|
||||||
|
|
||||||
return songCursor == null ? null : new SortedCursor(songCursor, paths, AudioColumns.DATA);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String makePlaceholders(int len) {
|
|
||||||
StringBuilder sb = new StringBuilder(len * 2 - 1);
|
|
||||||
sb.append("?");
|
|
||||||
for (int i = 1; i < len; i++) {
|
|
||||||
sb.append(",?");
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private static String[] toPathArray(@Nullable List<File> files) {
|
|
||||||
if (files != null) {
|
|
||||||
String[] paths = new String[files.size()];
|
|
||||||
for (int i = 0; i < files.size(); i++) {
|
|
||||||
try {
|
|
||||||
paths[i] = files.get(i).getCanonicalPath(); // canonical path is important here because we want to compare the path with the media store entry later
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
paths[i] = files.get(i).getPath();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return paths;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFileMenuClicked(final File file, View view) {
|
public void onFileMenuClicked(final File file, View view) {
|
||||||
PopupMenu popupMenu = new PopupMenu(getActivity(), view);
|
PopupMenu popupMenu = new PopupMenu(getActivity(), view);
|
||||||
|
|
@ -409,7 +355,8 @@ public class FoldersFragment extends AbsMainActivityFragment implements MainActi
|
||||||
final String[] toBeScanned;
|
final String[] toBeScanned;
|
||||||
|
|
||||||
if (file.isDirectory()) {
|
if (file.isDirectory()) {
|
||||||
List<File> files = listFilesDeep(file, getFileFilter());
|
// TODO load async
|
||||||
|
List<File> files = FileUtil.listFilesDeep(file, getFileFilter());
|
||||||
toBeScanned = new String[files.size()];
|
toBeScanned = new String[files.size()];
|
||||||
for (int i = 0; i < files.size(); i++) {
|
for (int i = 0; i < files.size(); i++) {
|
||||||
File f = files.get(i);
|
File f = files.get(i);
|
||||||
|
|
@ -457,72 +404,46 @@ public class FoldersFragment extends AbsMainActivityFragment implements MainActi
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAddToPlaylist(ArrayList<File> files) {
|
public void onAddToPlaylist(ArrayList<File> files) {
|
||||||
ArrayList<Song> songs = matchFilesWithMediaStore(sort(listFilesDeep(files, getFileFilter())));
|
new ListSongsAsyncTask(this, ADD_TO_PLAYLIST, null).execute(new ListSongsAsyncTask.LoadingInfo(files, getFileFilter(), getFileComparator()));
|
||||||
if (!songs.isEmpty())
|
|
||||||
AddToPlaylistDialog.create(songs).show(getFragmentManager(), "ADD_PLAYLIST");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAddToCurrentPlaying(ArrayList<File> files) {
|
public void onAddToCurrentPlaying(ArrayList<File> files) {
|
||||||
ArrayList<Song> songs = matchFilesWithMediaStore(sort(listFilesDeep(files, getFileFilter())));
|
new ListSongsAsyncTask(this, ADD_TO_CURRENT_PLAYING, null).execute(new ListSongsAsyncTask.LoadingInfo(files, getFileFilter(), getFileComparator()));
|
||||||
if (!songs.isEmpty())
|
|
||||||
MusicPlayerRemote.enqueue(songs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDeleteFromDevice(ArrayList<File> files) {
|
public void onDeleteFromDevice(ArrayList<File> files) {
|
||||||
ArrayList<Song> songs = matchFilesWithMediaStore(sort(listFilesDeep(files, getFileFilter())));
|
new ListSongsAsyncTask(this, DELETE, null).execute(new ListSongsAsyncTask.LoadingInfo(files, getFileFilter(), getFileComparator()));
|
||||||
if (!songs.isEmpty())
|
|
||||||
DeleteSongsDialog.create(songs).show(getFragmentManager(), "DELETE_SONGS");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
Comparator<File> fileComparator = new Comparator<File>() {
|
||||||
private ArrayList<Song> matchFilesWithMediaStore(@Nullable List<File> files) {
|
@Override
|
||||||
return SongLoader.getSongs(makeSongCursor(getActivity(), files));
|
public int compare(File lhs, File rhs) {
|
||||||
}
|
if (lhs.isDirectory() && !rhs.isDirectory()) {
|
||||||
|
return -1;
|
||||||
@NonNull
|
} else if (!lhs.isDirectory() && rhs.isDirectory()) {
|
||||||
private static List<File> listFiles(@NonNull File directory, @Nullable FileFilter fileFilter) {
|
return 1;
|
||||||
List<File> fileList = new LinkedList<>();
|
} else {
|
||||||
File[] found = directory.listFiles(fileFilter);
|
return lhs.getName().compareToIgnoreCase
|
||||||
if (found != null) {
|
(rhs.getName());
|
||||||
Collections.addAll(fileList, found);
|
|
||||||
}
|
|
||||||
return fileList;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private static List<File> listFilesDeep(@NonNull File directory, @Nullable FileFilter fileFilter) {
|
|
||||||
List<File> files = new LinkedList<>();
|
|
||||||
internalListFilesDeep(files, directory, fileFilter);
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private static List<File> listFilesDeep(@NonNull List<File> files, @Nullable FileFilter fileFilter) {
|
|
||||||
List<File> resFiles = new LinkedList<>();
|
|
||||||
for (File file : files) {
|
|
||||||
if (file.isDirectory()) {
|
|
||||||
internalListFilesDeep(resFiles, file, fileFilter);
|
|
||||||
} else if (fileFilter == null || fileFilter.accept(file)) {
|
|
||||||
resFiles.add(file);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return resFiles;
|
};
|
||||||
|
|
||||||
|
private Comparator<File> getFileComparator() {
|
||||||
|
return fileComparator;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void internalListFilesDeep(@NonNull Collection<File> files, @NonNull File directory, @Nullable FileFilter fileFilter) {
|
FileFilter audioFileFilter = new FileFilter() {
|
||||||
File[] found = directory.listFiles(fileFilter);
|
@Override
|
||||||
|
public boolean accept(File file) {
|
||||||
if (found != null) {
|
return !file.isHidden() && (file.isDirectory() || FileUtil.fileIsMimeType(file, "audio/*", MimeTypeMap.getSingleton()));
|
||||||
for (File file : found) {
|
|
||||||
if (file.isDirectory()) {
|
|
||||||
internalListFilesDeep(files, file, fileFilter);
|
|
||||||
} else {
|
|
||||||
files.add(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private FileFilter getFileFilter() {
|
||||||
|
return audioFileFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -536,86 +457,6 @@ public class FoldersFragment extends AbsMainActivityFragment implements MainActi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Comparator<File> fileComparator = new Comparator<File>() {
|
|
||||||
@Override
|
|
||||||
public int compare(File lhs, File rhs) {
|
|
||||||
if (lhs.isDirectory() && !rhs.isDirectory()) {
|
|
||||||
return -1;
|
|
||||||
} else if (!lhs.isDirectory() && rhs.isDirectory()) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return lhs.getName().compareToIgnoreCase(rhs.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private List<File> sort(List<File> files) {
|
|
||||||
Collections.sort(files, fileComparator);
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
FileFilter audioFileFilter = new FileFilter() {
|
|
||||||
@Override
|
|
||||||
public boolean accept(File file) {
|
|
||||||
return !file.isHidden() && (file.isDirectory() || fileIsMimeType(file, "audio/*", MimeTypeMap.getSingleton()));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private FileFilter getFileFilter() {
|
|
||||||
return audioFileFilter;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean fileIsMimeType(File file, String mimeType, MimeTypeMap mimeTypeMap) {
|
|
||||||
if (mimeType == null || mimeType.equals("*/*")) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
// get the file mime type
|
|
||||||
String filename = file.toURI().toString();
|
|
||||||
int dotPos = filename.lastIndexOf('.');
|
|
||||||
if (dotPos == -1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String fileExtension = filename.substring(dotPos + 1);
|
|
||||||
String fileType = mimeTypeMap.getMimeTypeFromExtension(fileExtension);
|
|
||||||
if (fileType == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// check the 'type/subtype' pattern
|
|
||||||
if (fileType.equals(mimeType)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// check the 'type/*' pattern
|
|
||||||
int mimeTypeDelimiter = mimeType.lastIndexOf('/');
|
|
||||||
if (mimeTypeDelimiter == -1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String mimeTypeMainType = mimeType.substring(0, mimeTypeDelimiter);
|
|
||||||
String mimeTypeSubtype = mimeType.substring(mimeTypeDelimiter + 1);
|
|
||||||
if (!mimeTypeSubtype.equals("*")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
int fileTypeDelimiter = fileType.lastIndexOf('/');
|
|
||||||
if (fileTypeDelimiter == -1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String fileTypeMainType = fileType.substring(0, fileTypeDelimiter);
|
|
||||||
if (fileTypeMainType.equals(mimeTypeMainType)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private static String getMimeType(String fileName, MimeTypeMap mimeTypeMap) {
|
|
||||||
int dotPos = fileName.lastIndexOf('.');
|
|
||||||
if (dotPos == -1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String fileExtension = fileName.substring(dotPos + 1);
|
|
||||||
return mimeTypeMap.getMimeTypeFromExtension(fileExtension);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static File tryGetCanonicalFile(File file) {
|
private static File tryGetCanonicalFile(File file) {
|
||||||
try {
|
try {
|
||||||
return file.getCanonicalFile();
|
return file.getCanonicalFile();
|
||||||
|
|
@ -666,7 +507,191 @@ public class FoldersFragment extends AbsMainActivityFragment implements MainActi
|
||||||
directory = crumb.getFile();
|
directory = crumb.getFile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return directory != null ? foldersFragment.sort(listFiles(directory, foldersFragment.getFileFilter())) : new LinkedList<File>();
|
if (directory != null) {
|
||||||
|
List<File> files = FileUtil.listFiles(directory, foldersFragment.getFileFilter());
|
||||||
|
Collections.sort(files, foldersFragment.getFileComparator());
|
||||||
|
return files;
|
||||||
|
} else {
|
||||||
|
return new LinkedList<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onSongsListed(int requestCode, ArrayList<Song> songs, Object extra) {
|
||||||
|
switch (requestCode) {
|
||||||
|
case ADD_TO_PLAYLIST:
|
||||||
|
AddToPlaylistDialog.create(songs).show(getFragmentManager(), "ADD_PLAYLIST");
|
||||||
|
break;
|
||||||
|
case ADD_TO_CURRENT_PLAYING:
|
||||||
|
MusicPlayerRemote.enqueue(songs);
|
||||||
|
break;
|
||||||
|
case DELETE:
|
||||||
|
DeleteSongsDialog.create(songs).show(getFragmentManager(), "DELETE_SONGS");
|
||||||
|
break;
|
||||||
|
case PLAY:
|
||||||
|
File file = (File) extra;
|
||||||
|
int startIndex = -1;
|
||||||
|
for (int i = 0; i < songs.size(); i++) {
|
||||||
|
if (file.getPath().equals(songs.get(i).data)) { // path is already canonical here
|
||||||
|
startIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (startIndex > -1) {
|
||||||
|
MusicPlayerRemote.openQueue(songs, startIndex, true);
|
||||||
|
} else {
|
||||||
|
final File finalFile = file;
|
||||||
|
Snackbar.make(coordinatorLayout, Html.fromHtml(String.format(getString(R.string.not_listed_in_media_store), file.getName())), Snackbar.LENGTH_LONG)
|
||||||
|
.setAction(R.string.action_scan, new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
scan(finalFile);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.setActionTextColor(ThemeStore.accentColor(getActivity()))
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ListSongsAsyncTask extends DialogAsyncTask<ListSongsAsyncTask.LoadingInfo, Void, ArrayList<Song>> {
|
||||||
|
private WeakReference<FoldersFragment> fragmentWeakReference;
|
||||||
|
private final int requestCode;
|
||||||
|
private final Object extra;
|
||||||
|
|
||||||
|
public ListSongsAsyncTask(FoldersFragment foldersFragment, int requestCode, Object extra) {
|
||||||
|
super(foldersFragment.getActivity(), R.string.listing_files);
|
||||||
|
this.requestCode = requestCode;
|
||||||
|
this.extra = extra;
|
||||||
|
fragmentWeakReference = new WeakReference<>(foldersFragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPreExecute() {
|
||||||
|
super.onPreExecute();
|
||||||
|
checkFragmentReference();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ArrayList<Song> doInBackground(LoadingInfo... params) {
|
||||||
|
try {
|
||||||
|
LoadingInfo info = params[0];
|
||||||
|
List<File> files = FileUtil.listFilesDeep(info.files, info.fileFilter);
|
||||||
|
if (isCancelled() || checkFragmentReference()) return null;
|
||||||
|
Collections.sort(files, info.fileComparator);
|
||||||
|
if (isCancelled() || checkFragmentReference()) return null;
|
||||||
|
return FileUtil.matchFilesWithMediaStore(fragmentWeakReference.get().getActivity(), files);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
cancel(false);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(ArrayList<Song> songs) {
|
||||||
|
super.onPostExecute(songs);
|
||||||
|
if (!songs.isEmpty() && !checkFragmentReference())
|
||||||
|
fragmentWeakReference.get().onSongsListed(requestCode, songs, extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class LoadingInfo {
|
||||||
|
public final Comparator<File> fileComparator;
|
||||||
|
public final FileFilter fileFilter;
|
||||||
|
public final List<File> files;
|
||||||
|
|
||||||
|
public LoadingInfo(@NonNull List<File> files, @NonNull FileFilter fileFilter, @NonNull Comparator<File> fileComparator) {
|
||||||
|
this.fileComparator = fileComparator;
|
||||||
|
this.fileFilter = fileFilter;
|
||||||
|
this.files = files;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if the task was canceled
|
||||||
|
*/
|
||||||
|
private boolean checkFragmentReference() {
|
||||||
|
if (fragmentWeakReference.get() == null) {
|
||||||
|
cancel(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static abstract class DialogAsyncTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {
|
||||||
|
private final int title;
|
||||||
|
private WeakReference<Context> contextWeakReference;
|
||||||
|
private WeakReference<Dialog> dialogWeakReference;
|
||||||
|
|
||||||
|
private boolean supposedToBeDismissed;
|
||||||
|
|
||||||
|
public DialogAsyncTask(Context context, @StringRes int title) {
|
||||||
|
contextWeakReference = new WeakReference<>(context);
|
||||||
|
dialogWeakReference = new WeakReference<>(null);
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPreExecute() {
|
||||||
|
super.onPreExecute();
|
||||||
|
new Handler().postDelayed(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (!supposedToBeDismissed && contextWeakReference.get() != null) {
|
||||||
|
Dialog dialog = new MaterialDialog.Builder(contextWeakReference.get())
|
||||||
|
.title(title)
|
||||||
|
.progress(true, 0)
|
||||||
|
.progressIndeterminateStyle(true)
|
||||||
|
.cancelListener(new DialogInterface.OnCancelListener() {
|
||||||
|
@Override
|
||||||
|
public void onCancel(DialogInterface dialog) {
|
||||||
|
cancel(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.dismissListener(new DialogInterface.OnDismissListener() {
|
||||||
|
@Override
|
||||||
|
public void onDismiss(DialogInterface dialog) {
|
||||||
|
cancel(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.negativeText(android.R.string.cancel)
|
||||||
|
.onNegative(new MaterialDialog.SingleButtonCallback() {
|
||||||
|
@Override
|
||||||
|
public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
|
||||||
|
cancel(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.show();
|
||||||
|
dialogWeakReference = new WeakReference<>(dialog);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DebugLog
|
||||||
|
@Override
|
||||||
|
protected void onCancelled(Result result) {
|
||||||
|
super.onCancelled(result);
|
||||||
|
tryToDismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
@DebugLog
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Result result) {
|
||||||
|
super.onPostExecute(result);
|
||||||
|
tryToDismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tryToDismiss() {
|
||||||
|
supposedToBeDismissed = true;
|
||||||
|
try {
|
||||||
|
if (dialogWeakReference.get() != null)
|
||||||
|
dialogWeakReference.get().dismiss();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
163
app/src/main/java/com/kabouzeid/gramophone/util/FileUtil.java
Normal file
163
app/src/main/java/com/kabouzeid/gramophone/util/FileUtil.java
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
package com.kabouzeid.gramophone.util;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.provider.MediaStore;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.webkit.MimeTypeMap;
|
||||||
|
|
||||||
|
import com.kabouzeid.gramophone.loader.SongLoader;
|
||||||
|
import com.kabouzeid.gramophone.loader.SortedCursor;
|
||||||
|
import com.kabouzeid.gramophone.model.Song;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileFilter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Karim Abou Zeid (kabouzeid)
|
||||||
|
*/
|
||||||
|
public final class FileUtil {
|
||||||
|
private FileUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static ArrayList<Song> matchFilesWithMediaStore(@NonNull Context context, @Nullable List<File> files) {
|
||||||
|
return SongLoader.getSongs(makeSongCursor(context, files));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static SortedCursor makeSongCursor(@NonNull final Context context, @Nullable final List<File> files) {
|
||||||
|
String selection = null;
|
||||||
|
String[] paths = null;
|
||||||
|
|
||||||
|
if (files != null) {
|
||||||
|
paths = toPathArray(files);
|
||||||
|
|
||||||
|
if (files.size() > 0 && files.size() < 999) { // 999 is the max amount Androids SQL implementation can handle.
|
||||||
|
selection = MediaStore.Audio.AudioColumns.DATA + " IN (" + makePlaceholders(files.size()) + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Cursor songCursor = SongLoader.makeSongCursor(context, selection, selection == null ? null : paths);
|
||||||
|
|
||||||
|
return songCursor == null ? null : new SortedCursor(songCursor, paths, MediaStore.Audio.AudioColumns.DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String makePlaceholders(int len) {
|
||||||
|
StringBuilder sb = new StringBuilder(len * 2 - 1);
|
||||||
|
sb.append("?");
|
||||||
|
for (int i = 1; i < len; i++) {
|
||||||
|
sb.append(",?");
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static String[] toPathArray(@Nullable List<File> files) {
|
||||||
|
if (files != null) {
|
||||||
|
String[] paths = new String[files.size()];
|
||||||
|
for (int i = 0; i < files.size(); i++) {
|
||||||
|
try {
|
||||||
|
paths[i] = files.get(i).getCanonicalPath(); // canonical path is important here because we want to compare the path with the media store entry later
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
paths[i] = files.get(i).getPath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static List<File> listFiles(@NonNull File directory, @Nullable FileFilter fileFilter) {
|
||||||
|
List<File> fileList = new LinkedList<>();
|
||||||
|
File[] found = directory.listFiles(fileFilter);
|
||||||
|
if (found != null) {
|
||||||
|
Collections.addAll(fileList, found);
|
||||||
|
}
|
||||||
|
return fileList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static List<File> listFilesDeep(@NonNull File directory, @Nullable FileFilter fileFilter) {
|
||||||
|
List<File> files = new LinkedList<>();
|
||||||
|
internalListFilesDeep(files, directory, fileFilter);
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static List<File> listFilesDeep(@NonNull Collection<File> files, @Nullable FileFilter fileFilter) {
|
||||||
|
List<File> resFiles = new LinkedList<>();
|
||||||
|
for (File file : files) {
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
internalListFilesDeep(resFiles, file, fileFilter);
|
||||||
|
} else if (fileFilter == null || fileFilter.accept(file)) {
|
||||||
|
resFiles.add(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void internalListFilesDeep(@NonNull Collection<File> files, @NonNull File directory, @Nullable FileFilter fileFilter) {
|
||||||
|
File[] found = directory.listFiles(fileFilter);
|
||||||
|
|
||||||
|
if (found != null) {
|
||||||
|
for (File file : found) {
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
internalListFilesDeep(files, file, fileFilter);
|
||||||
|
} else {
|
||||||
|
files.add(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean fileIsMimeType(File file, String mimeType, MimeTypeMap mimeTypeMap) {
|
||||||
|
if (mimeType == null || mimeType.equals("*/*")) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// get the file mime type
|
||||||
|
String filename = file.toURI().toString();
|
||||||
|
int dotPos = filename.lastIndexOf('.');
|
||||||
|
if (dotPos == -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String fileExtension = filename.substring(dotPos + 1);
|
||||||
|
String fileType = mimeTypeMap.getMimeTypeFromExtension(fileExtension);
|
||||||
|
if (fileType == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// check the 'type/subtype' pattern
|
||||||
|
if (fileType.equals(mimeType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// check the 'type/*' pattern
|
||||||
|
int mimeTypeDelimiter = mimeType.lastIndexOf('/');
|
||||||
|
if (mimeTypeDelimiter == -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String mimeTypeMainType = mimeType.substring(0, mimeTypeDelimiter);
|
||||||
|
String mimeTypeSubtype = mimeType.substring(mimeTypeDelimiter + 1);
|
||||||
|
if (!mimeTypeSubtype.equals("*")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int fileTypeDelimiter = fileType.lastIndexOf('/');
|
||||||
|
if (fileTypeDelimiter == -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String fileTypeMainType = fileType.substring(0, fileTypeDelimiter);
|
||||||
|
if (fileTypeMainType.equals(mimeTypeMainType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -244,4 +244,5 @@
|
||||||
<string name="scanning">Scanning %s</string>
|
<string name="scanning">Scanning %s</string>
|
||||||
<string name="scanned_files">Scanned %1$d of %2$d files.</string>
|
<string name="scanned_files">Scanned %1$d of %2$d files.</string>
|
||||||
<string name="could_not_scan_files">Could not scan %d files.</string>
|
<string name="could_not_scan_files">Could not scan %d files.</string>
|
||||||
|
<string name="listing_files">Listing files</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue