WIP removal of mediastore backing for song info

This commit is contained in:
maia arson crimew 2021-12-15 19:28:55 +01:00
parent 0c7204ccfb
commit 13ea8dd04d
12 changed files with 272 additions and 385 deletions

View file

@ -19,15 +19,12 @@ import android.os.Bundle
import android.provider.MediaStore import android.provider.MediaStore
import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.repository.RealSongRepository import code.name.monkey.retromusic.repository.RealSongRepository
import code.name.monkey.retromusic.repository.SongQuery
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import java.util.* import software.lavender.music.query.StringFilter
object SearchQueryHelper : KoinComponent { object SearchQueryHelper : KoinComponent {
private const val TITLE_SELECTION = "lower(" + MediaStore.Audio.AudioColumns.TITLE + ") = ?"
private const val ALBUM_SELECTION = "lower(" + MediaStore.Audio.AudioColumns.ALBUM + ") = ?"
private const val ARTIST_SELECTION = "lower(" + MediaStore.Audio.AudioColumns.ARTIST + ") = ?"
private const val AND = " AND "
private val songRepository by inject<RealSongRepository>() private val songRepository by inject<RealSongRepository>()
var songs = ArrayList<Song>() var songs = ArrayList<Song>()
@ -41,13 +38,10 @@ object SearchQueryHelper : KoinComponent {
var songs = listOf<Song>() var songs = listOf<Song>()
if (artistName != null && albumName != null && titleName != null) { if (artistName != null && albumName != null && titleName != null) {
songs = songRepository.songs( songs = songRepository.songs(
songRepository.makeSongCursor( SongQuery(
ARTIST_SELECTION + AND + ALBUM_SELECTION + AND + TITLE_SELECTION, artistName = StringFilter(eq = artistName.lowercase(), lower = true),
arrayOf( albumName = StringFilter(eq = albumName.lowercase(), lower = true),
artistName.lowercase(), title = StringFilter(eq = titleName.lowercase(), lower = true)
albumName.lowercase(),
titleName.lowercase()
)
) )
) )
} }
@ -56,12 +50,9 @@ object SearchQueryHelper : KoinComponent {
} }
if (artistName != null && titleName != null) { if (artistName != null && titleName != null) {
songs = songRepository.songs( songs = songRepository.songs(
songRepository.makeSongCursor( SongQuery(
ARTIST_SELECTION + AND + TITLE_SELECTION, artistName = StringFilter(eq = artistName.lowercase(), lower = true),
arrayOf( title = StringFilter(eq = titleName.lowercase(), lower = true)
artistName.lowercase(),
titleName.lowercase()
)
) )
) )
} }
@ -70,12 +61,9 @@ object SearchQueryHelper : KoinComponent {
} }
if (albumName != null && titleName != null) { if (albumName != null && titleName != null) {
songs = songRepository.songs( songs = songRepository.songs(
songRepository.makeSongCursor( SongQuery(
ALBUM_SELECTION + AND + TITLE_SELECTION, albumName = StringFilter(eq = albumName.lowercase(), lower = true),
arrayOf( title = StringFilter(eq = titleName.lowercase(), lower = true)
albumName.lowercase(),
titleName.lowercase()
)
) )
) )
} }
@ -84,10 +72,7 @@ object SearchQueryHelper : KoinComponent {
} }
if (artistName != null) { if (artistName != null) {
songs = songRepository.songs( songs = songRepository.songs(
songRepository.makeSongCursor( SongQuery(artistName = StringFilter(eq = artistName.lowercase(), lower = true))
ARTIST_SELECTION,
arrayOf(artistName.lowercase())
)
) )
} }
if (songs.isNotEmpty()) { if (songs.isNotEmpty()) {
@ -95,10 +80,7 @@ object SearchQueryHelper : KoinComponent {
} }
if (albumName != null) { if (albumName != null) {
songs = songRepository.songs( songs = songRepository.songs(
songRepository.makeSongCursor( SongQuery(albumName = StringFilter(eq = albumName.lowercase(), lower = true))
ALBUM_SELECTION,
arrayOf(albumName.lowercase())
)
) )
} }
if (songs.isNotEmpty()) { if (songs.isNotEmpty()) {
@ -106,42 +88,28 @@ object SearchQueryHelper : KoinComponent {
} }
if (titleName != null) { if (titleName != null) {
songs = songRepository.songs( songs = songRepository.songs(
songRepository.makeSongCursor( SongQuery(title = StringFilter(eq = titleName.lowercase(), lower = true))
TITLE_SELECTION,
arrayOf(titleName.lowercase())
)
) )
} }
if (songs.isNotEmpty()) { if (songs.isNotEmpty()) {
return songs return songs
} }
songs = songRepository.songs( songs = songRepository.songs(
songRepository.makeSongCursor( SongQuery(artistName = StringFilter(eq = query.lowercase(), lower = true))
ARTIST_SELECTION,
arrayOf(query.lowercase())
)
) )
if (songs.isNotEmpty()) { if (songs.isNotEmpty()) {
return songs return songs
} }
songs = songRepository.songs( songs = songRepository.songs(
songRepository.makeSongCursor( SongQuery(albumName = StringFilter(eq = query.lowercase(), lower = true))
ALBUM_SELECTION,
arrayOf(query.lowercase())
)
) )
if (songs.isNotEmpty()) { if (songs.isNotEmpty()) {
return songs return songs
} }
songs = songRepository.songs( songs = songRepository.songs(
songRepository.makeSongCursor( SongQuery(title = StringFilter(eq = query.lowercase(), lower = true))
TITLE_SELECTION,
arrayOf(query.lowercase())
) )
) return songs.ifEmpty { ArrayList() }
return if (songs.isNotEmpty()) {
songs
} else ArrayList()
} }
} }

View file

@ -20,16 +20,14 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteOpenHelper;
import android.provider.BaseColumns; import android.provider.BaseColumns;
import android.provider.MediaStore.Audio.AudioColumns; import android.provider.MediaStore.Audio.AudioColumns;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import java.util.List;
import code.name.monkey.retromusic.App; import code.name.monkey.retromusic.App;
import code.name.monkey.retromusic.model.Song; import code.name.monkey.retromusic.model.Song;
import code.name.monkey.retromusic.repository.RealSongRepository; import code.name.monkey.retromusic.repository.RealSongRepository;
import java.util.List;
/** /**
* @author Andrew Neal, modified for Phonograph by Karim Abou Zeid * @author Andrew Neal, modified for Phonograph by Karim Abou Zeid
* <p>This keeps track of the music playback and history state of the playback service * <p>This keeps track of the music playback and history state of the playback service
@ -157,8 +155,9 @@ public class MusicPlaybackQueueStore extends SQLiteOpenHelper {
@NonNull @NonNull
private List<Song> getQueue(@NonNull final String tableName) { private List<Song> getQueue(@NonNull final String tableName) {
// TODO: add back cursor support after all just for this....
Cursor cursor = getReadableDatabase().query(tableName, null, null, null, null, null, null); Cursor cursor = getReadableDatabase().query(tableName, null, null, null, null, null, null);
return new RealSongRepository(App.Companion.getContext()).songs(cursor); return new RealSongRepository(App.Companion.getContext()).songs();
} }
/** /**

View file

@ -14,11 +14,12 @@
package code.name.monkey.retromusic.repository package code.name.monkey.retromusic.repository
import android.provider.MediaStore.Audio.AudioColumns
import code.name.monkey.retromusic.helper.SortOrder import code.name.monkey.retromusic.helper.SortOrder
import code.name.monkey.retromusic.model.Album import code.name.monkey.retromusic.model.Album
import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.PreferenceUtil
import software.lavender.music.query.LongFilter
import software.lavender.music.query.StringFilter
/** /**
@ -36,34 +37,24 @@ class RealAlbumRepository(private val songRepository: RealSongRepository) :
AlbumRepository { AlbumRepository {
override fun albums(): List<Album> { override fun albums(): List<Album> {
// TODO: song loader sort order
val songs = songRepository.songs( val songs = songRepository.songs(
songRepository.makeSongCursor( SongQuery()
null,
null,
getSongLoaderSortOrder()
)
) )
return splitIntoAlbums(songs) return splitIntoAlbums(songs)
} }
override fun albums(query: String): List<Album> { override fun albums(query: String): List<Album> {
// TODO: song loader sort order
val songs = songRepository.songs( val songs = songRepository.songs(
songRepository.makeSongCursor( SongQuery(albumName = StringFilter(like = "%$query%"))
AudioColumns.ALBUM + " LIKE ?",
arrayOf("%$query%"),
getSongLoaderSortOrder()
)
) )
return splitIntoAlbums(songs) return splitIntoAlbums(songs)
} }
override fun album(albumId: Long): Album { override fun album(albumId: Long): Album {
val cursor = songRepository.makeSongCursor( // TODO: song loader sort order
AudioColumns.ALBUM_ID + "=?", val songs = songRepository.songs(SongQuery(albumId = LongFilter(eq = albumId)))
arrayOf(albumId.toString()),
getSongLoaderSortOrder()
)
val songs = songRepository.songs(cursor)
val album = Album(albumId, songs) val album = Album(albumId, songs)
sortAlbumSongs(album) sortAlbumSongs(album)
return album return album

View file

@ -14,12 +14,11 @@
package code.name.monkey.retromusic.repository package code.name.monkey.retromusic.repository
import android.provider.MediaStore.Audio.AudioColumns
import code.name.monkey.retromusic.ALBUM_ARTIST
import code.name.monkey.retromusic.helper.SortOrder
import code.name.monkey.retromusic.model.Album import code.name.monkey.retromusic.model.Album
import code.name.monkey.retromusic.model.Artist import code.name.monkey.retromusic.model.Artist
import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.PreferenceUtil
import software.lavender.music.query.LongFilter
import software.lavender.music.query.StringFilter
interface ArtistRepository { interface ArtistRepository {
fun artists(): List<Artist> fun artists(): List<Artist>
@ -49,24 +48,19 @@ class RealArtistRepository(
override fun artist(artistId: Long): Artist { override fun artist(artistId: Long): Artist {
if (artistId == Artist.VARIOUS_ARTISTS_ID) { if (artistId == Artist.VARIOUS_ARTISTS_ID) {
// Get Various Artists // Get Various Artists
// TODO: song loader sort order
// TODO: why is the filtering done AFTER the query here?
val songs = songRepository.songs( val songs = songRepository.songs(
songRepository.makeSongCursor( SongQuery()
null,
null,
getSongLoaderSortOrder()
)
) )
val albums = albumRepository.splitIntoAlbums(songs) val albums = albumRepository.splitIntoAlbums(songs)
.filter { it.albumArtist == Artist.VARIOUS_ARTISTS_DISPLAY_NAME } .filter { it.albumArtist == Artist.VARIOUS_ARTISTS_DISPLAY_NAME }
return Artist(Artist.VARIOUS_ARTISTS_ID, albums) return Artist(Artist.VARIOUS_ARTISTS_ID, albums)
} }
// TODO: song loader sort order
val songs = songRepository.songs( val songs = songRepository.songs(
songRepository.makeSongCursor( SongQuery(artistId = LongFilter(eq = artistId))
AudioColumns.ARTIST_ID + "=?",
arrayOf(artistId.toString()),
getSongLoaderSortOrder()
)
) )
return Artist(artistId, albumRepository.splitIntoAlbums(songs)) return Artist(artistId, albumRepository.splitIntoAlbums(songs))
} }
@ -74,68 +68,51 @@ class RealArtistRepository(
override fun albumArtist(artistName: String): Artist { override fun albumArtist(artistName: String): Artist {
if (artistName == Artist.VARIOUS_ARTISTS_DISPLAY_NAME) { if (artistName == Artist.VARIOUS_ARTISTS_DISPLAY_NAME) {
// Get Various Artists // Get Various Artists
// TODO: song loader sort order
// TODO: why is the filtering done AFTER the query here?
val songs = songRepository.songs( val songs = songRepository.songs(
songRepository.makeSongCursor( SongQuery()
null,
null,
getSongLoaderSortOrder()
)
) )
val albums = albumRepository.splitIntoAlbums(songs) val albums = albumRepository.splitIntoAlbums(songs)
.filter { it.albumArtist == Artist.VARIOUS_ARTISTS_DISPLAY_NAME } .filter { it.albumArtist == Artist.VARIOUS_ARTISTS_DISPLAY_NAME }
return Artist(Artist.VARIOUS_ARTISTS_ID, albums, true) return Artist(Artist.VARIOUS_ARTISTS_ID, albums, true)
} }
// TODO: song loader sort order
val songs = songRepository.songs( val songs = songRepository.songs(
songRepository.makeSongCursor( SongQuery(albumArtist = StringFilter(eq = artistName))
"album_artist" + "=?",
arrayOf(artistName),
getSongLoaderSortOrder()
)
) )
return Artist(artistName, albumRepository.splitIntoAlbums(songs), true) return Artist(artistName, albumRepository.splitIntoAlbums(songs), true)
} }
override fun artists(): List<Artist> { override fun artists(): List<Artist> {
// TODO: song loader sort order
val songs = songRepository.songs( val songs = songRepository.songs(
songRepository.makeSongCursor( SongQuery()
null, null,
getSongLoaderSortOrder()
)
) )
return splitIntoArtists(albumRepository.splitIntoAlbums(songs)) return splitIntoArtists(albumRepository.splitIntoAlbums(songs))
} }
override fun albumArtists(): List<Artist> { override fun albumArtists(): List<Artist> {
// TODO: sort "lower($ALBUM_ARTIST)" + if (PreferenceUtil.artistSortOrder == SortOrder.ArtistSortOrder.ARTIST_A_Z) "" else " DESC"
val songs = songRepository.songs( val songs = songRepository.songs(
songRepository.makeSongCursor( SongQuery()
null,
null,
"lower($ALBUM_ARTIST)" +
if (PreferenceUtil.artistSortOrder == SortOrder.ArtistSortOrder.ARTIST_A_Z) "" else " DESC"
)
) )
return splitIntoAlbumArtists(albumRepository.splitIntoAlbums(songs)) return splitIntoAlbumArtists(albumRepository.splitIntoAlbums(songs))
} }
override fun albumArtists(query: String): List<Artist> { override fun albumArtists(query: String): List<Artist> {
// TODO: song loader sort order
val songs = songRepository.songs( val songs = songRepository.songs(
songRepository.makeSongCursor( SongQuery(albumArtist = StringFilter(like = "%$query%"))
"album_artist" + " LIKE ?",
arrayOf("%$query%"),
getSongLoaderSortOrder()
)
) )
return splitIntoAlbumArtists(albumRepository.splitIntoAlbums(songs)) return splitIntoAlbumArtists(albumRepository.splitIntoAlbums(songs))
} }
override fun artists(query: String): List<Artist> { override fun artists(query: String): List<Artist> {
// TODO: song loader sort oder
val songs = songRepository.songs( val songs = songRepository.songs(
songRepository.makeSongCursor( SongQuery(artistName = StringFilter(like = "%$query%"))
AudioColumns.ARTIST + " LIKE ?",
arrayOf("%$query%"),
getSongLoaderSortOrder()
)
) )
return splitIntoArtists(albumRepository.splitIntoAlbums(songs)) return splitIntoArtists(albumRepository.splitIntoAlbums(songs))
} }

View file

@ -16,7 +16,6 @@ package code.name.monkey.retromusic.repository
import android.content.ContentResolver import android.content.ContentResolver
import android.database.Cursor import android.database.Cursor
import android.net.Uri
import android.provider.BaseColumns import android.provider.BaseColumns
import android.provider.MediaStore.Audio.Genres import android.provider.MediaStore.Audio.Genres
import code.name.monkey.retromusic.Constants.IS_MUSIC import code.name.monkey.retromusic.Constants.IS_MUSIC
@ -36,6 +35,7 @@ interface GenreRepository {
fun song(genreId: Long): Song fun song(genreId: Long): Song
} }
// TODO: move genre info into song model
class RealGenreRepository( class RealGenreRepository(
private val contentResolver: ContentResolver, private val contentResolver: ContentResolver,
private val songRepository: RealSongRepository private val songRepository: RealSongRepository
@ -50,11 +50,11 @@ class RealGenreRepository(
// so we need to get songs without a genre a different way. // so we need to get songs without a genre a different way.
return if (genreId == -1L) { return if (genreId == -1L) {
getSongsWithNoGenre() getSongsWithNoGenre()
} else songRepository.songs(makeGenreSongCursor(genreId)) } else songRepository.songs()
} }
override fun song(genreId: Long): Song { override fun song(genreId: Long): Song {
return songRepository.song(makeGenreSongCursor(genreId)) return songRepository.song(SongQuery())
} }
private fun getGenreFromCursor(cursor: Cursor): Genre { private fun getGenreFromCursor(cursor: Cursor): Genre {
@ -72,31 +72,10 @@ class RealGenreRepository(
} }
private fun getSongsWithNoGenre(): List<Song> { private fun getSongsWithNoGenre(): List<Song> {
// TODO: do this somehow (probably by actually storing genres on songs in our model)
val selection = val selection =
BaseColumns._ID + " NOT IN " + "(SELECT " + Genres.Members.AUDIO_ID + " FROM audio_genres_map)" BaseColumns._ID + " NOT IN " + "(SELECT " + Genres.Members.AUDIO_ID + " FROM audio_genres_map)"
return songRepository.songs(songRepository.makeSongCursor(selection, null)) return songRepository.songs()
}
private fun hasSongsWithNoGenre(): Boolean {
val allSongsCursor = songRepository.makeSongCursor(null, null)
val allSongsWithGenreCursor = makeAllSongsWithGenreCursor()
if (allSongsCursor == null || allSongsWithGenreCursor == null) {
return false
}
val hasSongsWithNoGenre = allSongsCursor.count > allSongsWithGenreCursor.count
allSongsCursor.close()
allSongsWithGenreCursor.close()
return hasSongsWithNoGenre
}
private fun makeAllSongsWithGenreCursor(): Cursor? {
println(Genres.EXTERNAL_CONTENT_URI.toString())
return contentResolver.query(
Uri.parse("content://media/external/audio/genres/all/members"),
arrayOf(Genres.Members.AUDIO_ID), null, null, null
)
} }
private fun makeGenreSongCursor(genreId: Long): Cursor? { private fun makeGenreSongCursor(genreId: Long): Cursor? {

View file

@ -14,12 +14,11 @@
package code.name.monkey.retromusic.repository package code.name.monkey.retromusic.repository
import android.database.Cursor
import android.provider.MediaStore
import code.name.monkey.retromusic.model.Album import code.name.monkey.retromusic.model.Album
import code.name.monkey.retromusic.model.Artist import code.name.monkey.retromusic.model.Artist
import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.PreferenceUtil
import software.lavender.music.query.LongFilter
/** /**
* Created by hemanths on 16/08/17. * Created by hemanths on 16/08/17.
@ -38,7 +37,8 @@ class RealLastAddedRepository(
private val artistRepository: RealArtistRepository private val artistRepository: RealArtistRepository
) : LastAddedRepository { ) : LastAddedRepository {
override fun recentSongs(): List<Song> { override fun recentSongs(): List<Song> {
return songRepository.songs(makeLastAddedCursor()) // TODO: sort by date added, descending
return songRepository.songs(SongQuery(dateModified = LongFilter(gt = PreferenceUtil.lastAddedCutoff)))
} }
override fun recentAlbums(): List<Album> { override fun recentAlbums(): List<Album> {
@ -48,13 +48,4 @@ class RealLastAddedRepository(
override fun recentArtists(): List<Artist> { override fun recentArtists(): List<Artist> {
return artistRepository.splitIntoArtists(recentAlbums()) return artistRepository.splitIntoArtists(recentAlbums())
} }
private fun makeLastAddedCursor(): Cursor? {
val cutoff = PreferenceUtil.lastAddedCutoff
return songRepository.makeSongCursor(
MediaStore.Audio.Media.DATE_ADDED + ">?",
arrayOf(cutoff.toString()),
MediaStore.Audio.Media.DATE_ADDED + " DESC"
)
}
} }

View file

@ -15,21 +15,48 @@
package code.name.monkey.retromusic.repository package code.name.monkey.retromusic.repository
import android.content.Context import android.content.Context
import android.database.Cursor
import android.os.Environment
import android.provider.MediaStore
import android.provider.MediaStore.Audio.AudioColumns
import android.provider.MediaStore.Audio.Media
import code.name.monkey.retromusic.Constants.IS_MUSIC
import code.name.monkey.retromusic.Constants.baseProjection
import code.name.monkey.retromusic.extensions.getInt
import code.name.monkey.retromusic.extensions.getLong
import code.name.monkey.retromusic.extensions.getString
import code.name.monkey.retromusic.extensions.getStringOrNull
import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.providers.BlacklistStore import software.lavender.music.query.IntFilter
import code.name.monkey.retromusic.util.PreferenceUtil import software.lavender.music.query.LongFilter
import java.util.* import software.lavender.music.query.Query
import software.lavender.music.query.StringFilter
// TODO: sorting
data class SongQuery(
val id: LongFilter? = null,
val title: StringFilter? = null,
val year: IntFilter? = null,
val data: StringFilter? = null,
val dateModified: LongFilter? = null,
val albumId: LongFilter? = null,
val albumName: StringFilter? = null,
val artistId: LongFilter? = null,
val artistName: StringFilter? = null,
val composer: StringFilter? = null,
val albumArtist: StringFilter? = null
) : Query<Song> {
override fun apply(testee: Song): Boolean {
if (id != null && !id.matches(testee.id)) {
return false
}
if (title != null && !title.matches(testee.title)) {
return false
}
if (year != null && !year.matches(testee.year)) {
return false
}
if (data != null && !data.matches(testee.data)) {
return false
}
if (dateModified != null && !dateModified.matches(testee.dateModified)) {
return false
}
if (albumId != null && !albumId.matches(testee.albumId)) {
return false
}
return true
}
}
/** /**
* Created by hemanths on 10/08/17. * Created by hemanths on 10/08/17.
@ -38,13 +65,13 @@ interface SongRepository {
fun songs(): List<Song> fun songs(): List<Song>
fun songs(cursor: Cursor?): List<Song> fun songs(query: SongQuery): List<Song>
fun songs(query: String): List<Song> fun songs(query: String): List<Song>
fun songsByFilePath(filePath: String): List<Song> fun songsByFilePath(filePath: String): List<Song>
fun song(cursor: Cursor?): Song fun song(query: SongQuery): Song
fun song(songId: Long): Song fun song(songId: Long): Song
} }
@ -52,162 +79,54 @@ interface SongRepository {
class RealSongRepository(private val context: Context) : SongRepository { class RealSongRepository(private val context: Context) : SongRepository {
override fun songs(): List<Song> { override fun songs(): List<Song> {
return songs(makeSongCursor(null, null)) return listOf(
Song(
1,
"example",
1,
2021,
23723478,
"uuh idk what goes here yet, i think the file path or something",
1639589623,
1,
"example",
1,
"example",
"example",
"example"
)
)
} }
override fun songs(cursor: Cursor?): List<Song> { override fun songs(query: SongQuery): List<Song> {
val songs = arrayListOf<Song>() val songs = arrayListOf<Song>()
if (cursor != null && cursor.moveToFirst()) { for (song in songs()) {
do { if (query.apply(song)) {
songs.add(getSongFromCursorImpl(cursor)) songs.add(song)
} while (cursor.moveToNext()) }
} }
cursor?.close()
return songs return songs
} }
override fun song(cursor: Cursor?): Song { override fun song(query: SongQuery): Song {
val song: Song = if (cursor != null && cursor.moveToFirst()) { val songs = arrayListOf<Song>()
getSongFromCursorImpl(cursor) for (song in songs()) {
} else { if (query.apply(song)) {
Song.emptySong songs.add(song)
} }
cursor?.close() }
return song return songs.firstOrNull() ?: Song.emptySong
} }
override fun songs(query: String): List<Song> { override fun songs(query: String): List<Song> {
return songs(makeSongCursor(AudioColumns.TITLE + " LIKE ?", arrayOf("%$query%"))) return songs(SongQuery(title = StringFilter(like = "%$query%")))
} }
override fun song(songId: Long): Song { override fun song(songId: Long): Song {
return song(makeSongCursor(AudioColumns._ID + "=?", arrayOf(songId.toString()))) return song(SongQuery(id = LongFilter(eq = songId)))
} }
override fun songsByFilePath(filePath: String): List<Song> { override fun songsByFilePath(filePath: String): List<Song> {
return songs( return songs(SongQuery(data = StringFilter(eq = filePath)))
makeSongCursor(
AudioColumns.DATA + "=?",
arrayOf(filePath)
)
)
}
private fun getSongFromCursorImpl(
cursor: Cursor
): Song {
val id = cursor.getLong(AudioColumns._ID)
val title = cursor.getString(AudioColumns.TITLE)
val trackNumber = cursor.getInt(AudioColumns.TRACK)
val year = cursor.getInt(AudioColumns.YEAR)
val duration = cursor.getLong(AudioColumns.DURATION)
val data = cursor.getString(AudioColumns.DATA)
val dateModified = cursor.getLong(AudioColumns.DATE_MODIFIED)
val albumId = cursor.getLong(AudioColumns.ALBUM_ID)
val albumName = cursor.getStringOrNull(AudioColumns.ALBUM)
val artistId = cursor.getLong(AudioColumns.ARTIST_ID)
val artistName = cursor.getStringOrNull(AudioColumns.ARTIST)
val composer = cursor.getStringOrNull(AudioColumns.COMPOSER)
val albumArtist = cursor.getStringOrNull("album_artist")
return Song(
id,
title,
trackNumber,
year,
duration,
data,
dateModified,
albumId,
albumName ?: "",
artistId,
artistName ?: "",
composer ?: "",
albumArtist ?: ""
)
}
@JvmOverloads
fun makeSongCursor(
selection: String?,
selectionValues: Array<String>?,
sortOrder: String = PreferenceUtil.songSortOrder
): Cursor? {
var selectionFinal = selection
var selectionValuesFinal = selectionValues
selectionFinal = if (selection != null && selection.trim { it <= ' ' } != "") {
"$IS_MUSIC AND $selectionFinal"
} else {
IS_MUSIC
}
// Whitelist
if (PreferenceUtil.isWhiteList) {
selectionFinal =
selectionFinal + " AND " + AudioColumns.DATA + " LIKE ?"
selectionValuesFinal = addSelectionValues(
selectionValuesFinal, arrayListOf(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).canonicalPath
)
)
} else {
// Blacklist
val paths = BlacklistStore.getInstance(context).paths
if (paths.isNotEmpty()) {
selectionFinal = generateBlacklistSelection(selectionFinal, paths.size)
selectionValuesFinal = addSelectionValues(selectionValuesFinal, paths)
}
}
selectionFinal =
selectionFinal + " AND " + Media.DURATION + ">= " + (PreferenceUtil.filterLength * 1000)
val uri = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
} else {
Media.EXTERNAL_CONTENT_URI
}
return try {
context.contentResolver.query(
uri,
baseProjection,
selectionFinal,
selectionValuesFinal,
sortOrder
)
} catch (ex: SecurityException) {
return null
}
}
private fun generateBlacklistSelection(
selection: String?,
pathCount: Int
): String {
val newSelection = StringBuilder(
if (selection != null && selection.trim { it <= ' ' } != "") "$selection AND " else "")
newSelection.append(AudioColumns.DATA + " NOT LIKE ?")
for (i in 0 until pathCount - 1) {
newSelection.append(" AND " + AudioColumns.DATA + " NOT LIKE ?")
}
return newSelection.toString()
}
private fun addSelectionValues(
selectionValues: Array<String>?,
paths: ArrayList<String>
): Array<String> {
var selectionValuesFinal = selectionValues
if (selectionValuesFinal == null) {
selectionValuesFinal = emptyArray()
}
val newSelectionValues = Array(selectionValuesFinal.size + paths.size) {
"n = $it"
}
System.arraycopy(selectionValuesFinal, 0, newSelectionValues, 0, selectionValuesFinal.size)
for (i in selectionValuesFinal.size until newSelectionValues.size) {
newSelectionValues[i] = paths[i - selectionValuesFinal.size] + "%"
}
return newSelectionValues
} }
} }

View file

@ -16,8 +16,6 @@ package code.name.monkey.retromusic.repository
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
import android.provider.BaseColumns
import android.provider.MediaStore
import code.name.monkey.retromusic.Constants.NUMBER_OF_TOP_TRACKS import code.name.monkey.retromusic.Constants.NUMBER_OF_TOP_TRACKS
import code.name.monkey.retromusic.model.Album import code.name.monkey.retromusic.model.Album
import code.name.monkey.retromusic.model.Artist import code.name.monkey.retromusic.model.Artist
@ -51,29 +49,27 @@ class RealTopPlayedRepository(
) : TopPlayedRepository { ) : TopPlayedRepository {
override fun recentlyPlayedTracks(): List<Song> { override fun recentlyPlayedTracks(): List<Song> {
return songRepository.songs(makeRecentTracksCursorAndClearUpDatabase()) // TODO:
return songRepository.songs()
} }
override fun topTracks(): List<Song> { override fun topTracks(): List<Song> {
return songRepository.songs(makeTopTracksCursorAndClearUpDatabase()) // TODO:
return songRepository.songs()
} }
override fun notRecentlyPlayedTracks(): List<Song> { override fun notRecentlyPlayedTracks(): List<Song> {
val allSongs = mutableListOf<Song>().apply { val allSongs = mutableListOf<Song>().apply {
addAll( addAll(
songRepository.songs( // TODO: sort by date added asc
songRepository.makeSongCursor( songRepository.songs()
null, null,
MediaStore.Audio.Media.DATE_ADDED + " ASC"
)
)
) )
} }
val playedSongs = songRepository.songs( val playedSongs = songRepository.songs(
makePlayedTracksCursorAndClearUpDatabase() // TODO: makePlayedTracksCursorAndClearUpDatabase()
) )
val notRecentlyPlayedSongs = songRepository.songs( val notRecentlyPlayedSongs = songRepository.songs(
makeNotRecentTracksCursorAndClearUpDatabase() // TODO: makeNotRecentTracksCursorAndClearUpDatabase()
) )
allSongs.removeAll(playedSongs.toSet()) allSongs.removeAll(playedSongs.toSet())
allSongs.addAll(notRecentlyPlayedSongs) allSongs.addAll(notRecentlyPlayedSongs)
@ -131,7 +127,7 @@ class RealTopPlayedRepository(
cursor: Cursor?, idColumn: Int cursor: Cursor?, idColumn: Int
): SortedLongCursor? { ): SortedLongCursor? {
if (cursor != null && cursor.moveToFirst()) { /*if (cursor != null && cursor.moveToFirst()) {
// create the list of ids to select against // create the list of ids to select against
val selection = StringBuilder() val selection = StringBuilder()
selection.append(BaseColumns._ID) selection.append(BaseColumns._ID)
@ -164,7 +160,7 @@ class RealTopPlayedRepository(
BaseColumns._ID BaseColumns._ID
) )
} }
} } */
return null return null
} }

View file

@ -15,35 +15,18 @@
package code.name.monkey.retromusic.util; package code.name.monkey.retromusic.util;
import android.content.Context; import android.content.Context;
import android.database.Cursor;
import android.os.Environment; import android.os.Environment;
import android.provider.MediaStore;
import android.webkit.MimeTypeMap; import android.webkit.MimeTypeMap;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.StringTokenizer;
import code.name.monkey.retromusic.adapter.Storage; import code.name.monkey.retromusic.adapter.Storage;
import code.name.monkey.retromusic.model.Song; import code.name.monkey.retromusic.model.Song;
import code.name.monkey.retromusic.repository.RealSongRepository; import code.name.monkey.retromusic.repository.RealSongRepository;
import code.name.monkey.retromusic.repository.SortedCursor; import code.name.monkey.retromusic.repository.SongQuery;
import software.lavender.music.query.StringFilter;
import java.io.*;
import java.util.*;
public final class FileUtil { public final class FileUtil {
@ -63,7 +46,7 @@ public final class FileUtil {
@NonNull @NonNull
public static List<Song> matchFilesWithMediaStore( public static List<Song> matchFilesWithMediaStore(
@NonNull Context context, @Nullable List<File> files) { @NonNull Context context, @Nullable List<File> files) {
return new RealSongRepository(context).songs(makeSongCursor(context, files)); return new RealSongRepository(context).songs(makeSongQuery(files));
} }
public static String safeGetCanonicalPath(File file) { public static String safeGetCanonicalPath(File file) {
@ -76,36 +59,14 @@ public final class FileUtil {
} }
@Nullable @Nullable
public static SortedCursor makeSongCursor( public static SongQuery makeSongQuery(@Nullable final List<File> files) {
@NonNull final Context context, @Nullable final List<File> files) {
String selection = null;
String[] paths = null; String[] paths = null;
if (files != null) { if (files != null) {
paths = toPathArray(files); 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 = return new SongQuery(null, null, null, new StringFilter(null, null, null, paths, false, false), null, null, null, null, null, null, null);
new RealSongRepository(context).makeSongCursor(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 @Nullable

View file

@ -0,0 +1,14 @@
package software.lavender.music.query
fun String.like(matcher: String): Boolean {
if (matcher.startsWith('%')) {
if (matcher.endsWith('%')) {
return contains(matcher.substring(1, matcher.length - 2))
}
return endsWith(matcher.substring(1))
}
if (matcher.endsWith('%')) {
return startsWith(matcher.substring(0, matcher.length - 2))
}
return this == matcher
}

View file

@ -0,0 +1,87 @@
package software.lavender.music.query
interface QueryFilter<T> {
fun matches(testee: T): Boolean
}
abstract class NumericFilter<T : Number>(
val gt: T?,
val lt: T?,
val eq: T?,
val ne: T?
) : QueryFilter<T>
class IntFilter(
gt: Int? = null,
lt: Int? = null,
eq: Int? = null,
ne: Int? = null
) : NumericFilter<Int>(gt, lt, eq, ne) {
override fun matches(testee: Int): Boolean {
if (eq != null) {
return testee == eq
}
if (lt != null && testee >= lt) {
return false
}
if (gt != null && testee <= gt) {
return false
}
if (ne != null && testee == ne) {
return false
}
return true
}
}
class LongFilter(
gt: Long? = null,
lt: Long? = null,
eq: Long? = null,
ne: Long? = null
) : NumericFilter<Long>(gt, lt, eq, ne) {
override fun matches(testee: Long): Boolean {
if (eq != null) {
return testee == eq
}
if (lt != null && testee >= lt) {
return false
}
if (gt != null && testee <= gt) {
return false
}
if (ne != null && testee == ne) {
return false
}
return true
}
}
class StringFilter(
val eq: String? = null,
val like: String? = null,
val ne: String? = null,
val _in: Array<String>? = null,
val isNull: Boolean = false,
val lower: Boolean = false,
) : QueryFilter<String?> {
override fun matches(testee: String?): Boolean {
if (testee == null) {
return isNull
}
val other = if (lower) testee.lowercase() else testee
if (eq != null) {
return other == eq
}
if (like != null) {
return other.like(like)
}
if (ne != null) {
return other != ne
}
if (_in != null) {
return _in.contains(other)
}
return true
}
}

View file

@ -0,0 +1,5 @@
package software.lavender.music.query
interface Query<T> {
fun apply(testee: T): Boolean
}