From a4943b2b68f69d3db1da946b59f0fa5eab412401 Mon Sep 17 00:00:00 2001 From: Karim Abou Zeid Date: Sat, 26 Mar 2016 01:18:51 +0100 Subject: [PATCH] Async song matching in the FoldersFragment. --- .../mainactivity/folders/FoldersFragment.java | 439 +++++++++--------- .../kabouzeid/gramophone/util/FileUtil.java | 163 +++++++ app/src/main/res/values/strings.xml | 1 + 3 files changed, 396 insertions(+), 207 deletions(-) create mode 100644 app/src/main/java/com/kabouzeid/gramophone/util/FileUtil.java diff --git a/app/src/main/java/com/kabouzeid/gramophone/ui/fragments/mainactivity/folders/FoldersFragment.java b/app/src/main/java/com/kabouzeid/gramophone/ui/fragments/mainactivity/folders/FoldersFragment.java index 94d1f7df..5748a866 100644 --- a/app/src/main/java/com/kabouzeid/gramophone/ui/fragments/mainactivity/folders/FoldersFragment.java +++ b/app/src/main/java/com/kabouzeid/gramophone/ui/fragments/mainactivity/folders/FoldersFragment.java @@ -2,15 +2,18 @@ package com.kabouzeid.gramophone.ui.fragments.mainactivity.folders; import android.annotation.SuppressLint; +import android.app.Dialog; import android.content.Context; -import android.database.Cursor; +import android.content.DialogInterface; import android.media.MediaScannerConnection; import android.net.Uri; +import android.os.AsyncTask; import android.os.Bundle; import android.os.Environment; -import android.provider.MediaStore.Audio.AudioColumns; +import android.os.Handler; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.StringRes; import android.support.design.widget.AppBarLayout; import android.support.design.widget.CoordinatorLayout; import android.support.design.widget.Snackbar; @@ -31,6 +34,8 @@ import android.widget.PopupMenu; import android.widget.Toast; import com.afollestad.materialcab.MaterialCab; +import com.afollestad.materialdialogs.DialogAction; +import com.afollestad.materialdialogs.MaterialDialog; import com.kabouzeid.appthemehelper.ThemeStore; import com.kabouzeid.appthemehelper.common.ATHToolbarActivity; 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.helper.MusicPlayerRemote; 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.model.Song; import com.kabouzeid.gramophone.ui.activities.MainActivity; 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.PreferenceUtil; import com.kabouzeid.gramophone.util.ViewUtil; @@ -57,7 +61,6 @@ import java.io.FileFilter; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.LinkedList; @@ -65,12 +68,18 @@ import java.util.List; import butterknife.Bind; import butterknife.ButterKnife; +import hugo.weaving.DebugLog; public class FoldersFragment extends AbsMainActivityFragment implements MainActivity.MainActivityFragmentCallbacks, CabHolder, BreadCrumbLayout.SelectionCallback, SongFileAdapter.Callbacks, AppBarLayout.OnOffsetChangedListener, LoaderManager.LoaderCallbacks> { public static final String TAG = FoldersFragment.class.getSimpleName(); 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 CRUMBS = "crumbs"; @@ -292,83 +301,20 @@ public class FoldersFragment extends AbsMainActivityFragment implements MainActi if (file.isDirectory()) { setCrumb(new BreadCrumbLayout.Crumb(file), true); } else { - List files = listFilesDeep(file.getParentFile(), new FileFilter() { + List files = new LinkedList<>(); + files.add(file.getParentFile()); + + FileFilter fileFilter = new FileFilter() { @Override public boolean accept(File pathname) { return !pathname.isDirectory() && getFileFilter().accept(pathname); } - }); - sort(files); - ArrayList songs = matchFilesWithMediaStore(files); + }; - 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(); - } + new ListSongsAsyncTask(this, PLAY, file).execute(new ListSongsAsyncTask.LoadingInfo(files, fileFilter, getFileComparator())); } } - @Nullable - private static SortedCursor makeSongCursor(@NonNull final Context context, @Nullable final List 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 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 public void onFileMenuClicked(final File file, View view) { PopupMenu popupMenu = new PopupMenu(getActivity(), view); @@ -409,7 +355,8 @@ public class FoldersFragment extends AbsMainActivityFragment implements MainActi final String[] toBeScanned; if (file.isDirectory()) { - List files = listFilesDeep(file, getFileFilter()); + // TODO load async + List files = FileUtil.listFilesDeep(file, getFileFilter()); toBeScanned = new String[files.size()]; for (int i = 0; i < files.size(); i++) { File f = files.get(i); @@ -457,72 +404,46 @@ public class FoldersFragment extends AbsMainActivityFragment implements MainActi @Override public void onAddToPlaylist(ArrayList files) { - ArrayList songs = matchFilesWithMediaStore(sort(listFilesDeep(files, getFileFilter()))); - if (!songs.isEmpty()) - AddToPlaylistDialog.create(songs).show(getFragmentManager(), "ADD_PLAYLIST"); + new ListSongsAsyncTask(this, ADD_TO_PLAYLIST, null).execute(new ListSongsAsyncTask.LoadingInfo(files, getFileFilter(), getFileComparator())); } @Override public void onAddToCurrentPlaying(ArrayList files) { - ArrayList songs = matchFilesWithMediaStore(sort(listFilesDeep(files, getFileFilter()))); - if (!songs.isEmpty()) - MusicPlayerRemote.enqueue(songs); + new ListSongsAsyncTask(this, ADD_TO_CURRENT_PLAYING, null).execute(new ListSongsAsyncTask.LoadingInfo(files, getFileFilter(), getFileComparator())); } @Override public void onDeleteFromDevice(ArrayList files) { - ArrayList songs = matchFilesWithMediaStore(sort(listFilesDeep(files, getFileFilter()))); - if (!songs.isEmpty()) - DeleteSongsDialog.create(songs).show(getFragmentManager(), "DELETE_SONGS"); + new ListSongsAsyncTask(this, DELETE, null).execute(new ListSongsAsyncTask.LoadingInfo(files, getFileFilter(), getFileComparator())); } - @NonNull - private ArrayList matchFilesWithMediaStore(@Nullable List files) { - return SongLoader.getSongs(makeSongCursor(getActivity(), files)); - } - - @NonNull - private static List listFiles(@NonNull File directory, @Nullable FileFilter fileFilter) { - List fileList = new LinkedList<>(); - File[] found = directory.listFiles(fileFilter); - if (found != null) { - Collections.addAll(fileList, found); - } - return fileList; - } - - @NonNull - private static List listFilesDeep(@NonNull File directory, @Nullable FileFilter fileFilter) { - List files = new LinkedList<>(); - internalListFilesDeep(files, directory, fileFilter); - return files; - } - - @NonNull - private static List listFilesDeep(@NonNull List files, @Nullable FileFilter fileFilter) { - List resFiles = new LinkedList<>(); - for (File file : files) { - if (file.isDirectory()) { - internalListFilesDeep(resFiles, file, fileFilter); - } else if (fileFilter == null || fileFilter.accept(file)) { - resFiles.add(file); + Comparator fileComparator = new Comparator() { + @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()); } } - return resFiles; + }; + + private Comparator getFileComparator() { + return fileComparator; } - private static void internalListFilesDeep(@NonNull Collection 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); - } - } + FileFilter audioFileFilter = new FileFilter() { + @Override + public boolean accept(File file) { + return !file.isHidden() && (file.isDirectory() || FileUtil.fileIsMimeType(file, "audio/*", MimeTypeMap.getSingleton())); } + }; + + private FileFilter getFileFilter() { + return audioFileFilter; } @Override @@ -536,86 +457,6 @@ public class FoldersFragment extends AbsMainActivityFragment implements MainActi } } - Comparator fileComparator = new Comparator() { - @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 sort(List 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) { try { return file.getCanonicalFile(); @@ -666,7 +507,191 @@ public class FoldersFragment extends AbsMainActivityFragment implements MainActi directory = crumb.getFile(); } } - return directory != null ? foldersFragment.sort(listFiles(directory, foldersFragment.getFileFilter())) : new LinkedList(); + if (directory != null) { + List files = FileUtil.listFiles(directory, foldersFragment.getFileFilter()); + Collections.sort(files, foldersFragment.getFileComparator()); + return files; + } else { + return new LinkedList<>(); + } + } + } + + private void onSongsListed(int requestCode, ArrayList 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> { + private WeakReference 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 doInBackground(LoadingInfo... params) { + try { + LoadingInfo info = params[0]; + List 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 songs) { + super.onPostExecute(songs); + if (!songs.isEmpty() && !checkFragmentReference()) + fragmentWeakReference.get().onSongsListed(requestCode, songs, extra); + } + + public static class LoadingInfo { + public final Comparator fileComparator; + public final FileFilter fileFilter; + public final List files; + + public LoadingInfo(@NonNull List files, @NonNull FileFilter fileFilter, @NonNull Comparator 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 extends AsyncTask { + private final int title; + private WeakReference contextWeakReference; + private WeakReference 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(); + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/kabouzeid/gramophone/util/FileUtil.java b/app/src/main/java/com/kabouzeid/gramophone/util/FileUtil.java new file mode 100644 index 00000000..65b6d716 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/gramophone/util/FileUtil.java @@ -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 matchFilesWithMediaStore(@NonNull Context context, @Nullable List files) { + return SongLoader.getSongs(makeSongCursor(context, files)); + } + + @Nullable + public static SortedCursor makeSongCursor(@NonNull final Context context, @Nullable final List 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 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 listFiles(@NonNull File directory, @Nullable FileFilter fileFilter) { + List fileList = new LinkedList<>(); + File[] found = directory.listFiles(fileFilter); + if (found != null) { + Collections.addAll(fileList, found); + } + return fileList; + } + + @NonNull + public static List listFilesDeep(@NonNull File directory, @Nullable FileFilter fileFilter) { + List files = new LinkedList<>(); + internalListFilesDeep(files, directory, fileFilter); + return files; + } + + @NonNull + public static List listFilesDeep(@NonNull Collection files, @Nullable FileFilter fileFilter) { + List 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 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; + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 54de9bdd..ec7d7f03 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -244,4 +244,5 @@ Scanning %s Scanned %1$d of %2$d files. Could not scan %d files. + Listing files