package com.poupa.vinylmusicplayer.loader; import android.content.Context; import android.database.Cursor; import android.database.MergeCursor; import android.provider.BaseColumns; import android.provider.MediaStore; import android.provider.MediaStore.Audio.AudioColumns; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.poupa.vinylmusicplayer.model.Song; import com.poupa.vinylmusicplayer.provider.BlacklistStore; import com.poupa.vinylmusicplayer.util.PreferenceUtil; import java.util.ArrayList; import java.util.List; /** * @author Karim Abou Zeid (kabouzeid) */ public class SongLoader { protected static final String BASE_SELECTION = AudioColumns.IS_MUSIC + "=1" + " AND " + AudioColumns.TITLE + " != ''"; protected static final String[] BASE_PROJECTION = new String[]{ BaseColumns._ID,// 0 AudioColumns.TITLE,// 1 AudioColumns.TRACK,// 2 AudioColumns.YEAR,// 3 AudioColumns.DURATION,// 4 AudioColumns.DATA,// 5 AudioColumns.DATE_MODIFIED,// 6 AudioColumns.DATE_ADDED,// 7 AudioColumns.ALBUM_ID,// 8 AudioColumns.ALBUM,// 9 AudioColumns.ARTIST_ID,// 10 AudioColumns.ARTIST,// 11 }; private static final int BATCH_SIZE = 900; // used in makeSongCursor* functions. SQLite limit on the number of ?argument is 999, we leave some to the other call sites. @NonNull public static ArrayList<Song> getAllSongs(@NonNull Context context) { Cursor cursor = makeSongCursor(context, null, null); return getSongs(cursor); } @NonNull public static ArrayList<Song> getSongs(@NonNull final Context context, final String query) { Cursor cursor = makeSongCursor(context, AudioColumns.TITLE + " LIKE ?", new String[]{"%" + query + "%"}); return getSongs(cursor); } @NonNull public static Song getSong(@NonNull final Context context, final int queryId) { Cursor cursor = makeSongCursor(context, AudioColumns._ID + "=?", new String[]{String.valueOf(queryId)}); return getSong(cursor); } @NonNull public static ArrayList<Song> getSongs(@Nullable final Cursor cursor) { ArrayList<Song> songs = new ArrayList<>(); if (cursor != null && cursor.moveToFirst()) { do { songs.add(getSongFromCursorImpl(cursor)); } while (cursor.moveToNext()); } if (cursor != null) cursor.close(); return songs; } @NonNull public static Song getSong(@Nullable Cursor cursor) { Song song; if (cursor != null && cursor.moveToFirst()) { song = getSongFromCursorImpl(cursor); } else { song = Song.EMPTY_SONG; } if (cursor != null) { cursor.close(); } return song; } @NonNull private static Song getSongFromCursorImpl(@NonNull Cursor cursor) { final int id = cursor.getInt(0); final String title = cursor.getString(1); final int trackNumber = cursor.getInt(2); final int year = cursor.getInt(3); final long duration = cursor.getLong(4); final String data = cursor.getString(5); final long dateAdded = cursor.getLong(6); final long dateModified = cursor.getLong(7); final int albumId = cursor.getInt(8); final String albumName = cursor.getString(9); final int artistId = cursor.getInt(10); final String artistName = cursor.getString(11); return new Song(id, title, trackNumber, year, duration, data, dateAdded, dateModified, albumId, albumName, artistId, artistName); } @Nullable public static Cursor makeSongCursorFromPaths(@NonNull final Context context, @NonNull ArrayList<String> paths) { // Exclude blacklist paths.removeAll(BlacklistStore.getInstance(context).getPaths()); int remaining = paths.size(); int processed = 0; ArrayList<Cursor> cursors = new ArrayList<>(); final String sortOrder = PreferenceUtil.getInstance().getSongSortOrder(); while (remaining > 0) { final int currentBatch = Math.min(BATCH_SIZE, remaining); StringBuilder selection = new StringBuilder(); selection.append(BASE_SELECTION + " AND " + MediaStore.Audio.AudioColumns.DATA + " IN (?"); for (int i = 1; i < currentBatch; i++) { selection.append(",?"); } selection.append(")"); try { Cursor cursor = context.getContentResolver().query( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, BASE_PROJECTION, selection.toString(), paths.subList(processed, processed + currentBatch).toArray(new String[currentBatch]), sortOrder ); if (cursor != null) {cursors.add(cursor);}; } catch (SecurityException ignored) { } remaining -= currentBatch; processed += currentBatch; } if (cursors.isEmpty()) {return null;} return new MergeCursor(cursors.toArray(new Cursor[cursors.size()])); } @Nullable public static Cursor makeSongCursor(@NonNull final Context context, @Nullable final String selection, final String[] selectionValues) { return makeSongCursor(context, selection, selectionValues, PreferenceUtil.getInstance().getSongSortOrder()); } @Nullable public static Cursor makeSongCursor(@NonNull final Context context, @Nullable String selection, String[] selectionValues, final String sortOrder) { if (selection != null && !selection.trim().equals("")) { selection = BASE_SELECTION + " AND " + selection; } else { selection = BASE_SELECTION; } // Blacklist final ArrayList<String> paths = BlacklistStore.getInstance(context).getPaths(); int remaining = paths.size(); int processed = 0; ArrayList<Cursor> cursors = new ArrayList<>(); while (remaining > 0) { final int currentBatch = Math.min(BATCH_SIZE, remaining); // Enrich the base selection with the current batch parameters String batchSelection = generateBlacklistSelection(selection, currentBatch); ArrayList<String> batchSelectionValues = addBlacklistSelectionValues(selectionValues, paths.subList(processed, processed + currentBatch)); try { Cursor cursor = context.getContentResolver().query( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, BASE_PROJECTION, batchSelection, batchSelectionValues.toArray(new String[batchSelectionValues.size()]), sortOrder ); if (cursor != null) { cursors.add(cursor); } } catch (SecurityException ignored) { } remaining -= currentBatch; processed += currentBatch; } if (cursors.isEmpty()) {return null;} return new MergeCursor(cursors.toArray(new Cursor[cursors.size()])); } private static String generateBlacklistSelection(String selection, int pathCount) { StringBuilder newSelection = new StringBuilder(selection != null && !selection.trim().equals("") ? selection + " AND " : ""); newSelection.append(AudioColumns.DATA + " NOT LIKE ?"); for (int i = 1; i < pathCount; i++) { newSelection.append(" AND " + AudioColumns.DATA + " NOT LIKE ?"); } return newSelection.toString(); } private static ArrayList<String> addBlacklistSelectionValues(String[] selectionValues, @NonNull final List<String> paths) { ArrayList<String> newSelectionValues; if (selectionValues == null) { newSelectionValues = new ArrayList<>(paths.size()); } else { newSelectionValues = new ArrayList<>(selectionValues.length + paths.size()); for (int i=0; i < selectionValues.length; ++i) { newSelectionValues.add(selectionValues[i]); } } for (int i = 0; i < paths.size(); i++) { newSelectionValues.add(paths.get(i) + "%"); } return newSelectionValues; } }