🙆🏻 Woof that's done it Playlist
Code refactor to Kotlin
This commit is contained in:
parent
ff20b3a052
commit
e159b1a32a
73 changed files with 1482 additions and 1785 deletions
|
@ -172,7 +172,7 @@ public class AutoGeneratedPlaylistBitmap {
|
|||
private static Bitmap getBitmapWithAlbumId(@NonNull Context context, Integer id) {
|
||||
try {
|
||||
return Glide.with(context)
|
||||
.load(MusicUtil.getMediaStoreAlbumCoverUri(id))
|
||||
.load(MusicUtil.INSTANCE.getMediaStoreAlbumCoverUri(id))
|
||||
.asBitmap()
|
||||
.into(200, 200)
|
||||
.get();
|
||||
|
|
|
@ -31,15 +31,14 @@ import java.io.FileInputStream;
|
|||
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.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import code.name.monkey.retromusic.loaders.SongLoader;
|
||||
import code.name.monkey.retromusic.loaders.SortedCursor;
|
||||
import code.name.monkey.retromusic.model.Song;
|
||||
import code.name.monkey.retromusic.repository.RealSongRepository;
|
||||
import code.name.monkey.retromusic.repository.SortedCursor;
|
||||
|
||||
|
||||
public final class FileUtil {
|
||||
|
@ -59,9 +58,9 @@ public final class FileUtil {
|
|||
}
|
||||
|
||||
@NonNull
|
||||
public static ArrayList<Song> matchFilesWithMediaStore(@NonNull Context context,
|
||||
@Nullable List<File> files) {
|
||||
return SongLoader.INSTANCE.getSongs(makeSongCursor(context, files));
|
||||
public static List<Song> matchFilesWithMediaStore(@NonNull Context context,
|
||||
@Nullable List<File> files) {
|
||||
return new RealSongRepository(context).songs(makeSongCursor(context, files));
|
||||
}
|
||||
|
||||
public static String safeGetCanonicalPath(File file) {
|
||||
|
@ -89,7 +88,7 @@ public final class FileUtil {
|
|||
}
|
||||
}
|
||||
|
||||
Cursor songCursor = SongLoader.INSTANCE.makeSongCursor(context, selection, selection == null ? null : paths);
|
||||
Cursor songCursor = new RealSongRepository(context).makeSongCursor(selection, selection == null ? null : paths);
|
||||
|
||||
return songCursor == null ? null
|
||||
: new SortedCursor(songCursor, paths, MediaStore.Audio.AudioColumns.DATA);
|
||||
|
|
|
@ -1,428 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
package code.name.monkey.retromusic.util;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.provider.BaseColumns;
|
||||
import android.provider.MediaStore;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.FileProvider;
|
||||
|
||||
import org.jaudiotagger.audio.AudioFileIO;
|
||||
import org.jaudiotagger.tag.FieldKey;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import code.name.monkey.retromusic.R;
|
||||
import code.name.monkey.retromusic.helper.MusicPlayerRemote;
|
||||
import code.name.monkey.retromusic.loaders.PlaylistLoader;
|
||||
import code.name.monkey.retromusic.loaders.SongLoader;
|
||||
import code.name.monkey.retromusic.model.Artist;
|
||||
import code.name.monkey.retromusic.model.Playlist;
|
||||
import code.name.monkey.retromusic.model.Song;
|
||||
import code.name.monkey.retromusic.model.lyrics.AbsSynchronizedLyrics;
|
||||
import code.name.monkey.retromusic.service.MusicService;
|
||||
|
||||
|
||||
public class MusicUtil {
|
||||
|
||||
public static final String TAG = MusicUtil.class.getSimpleName();
|
||||
|
||||
private static Playlist playlist;
|
||||
|
||||
/**
|
||||
* Build a concatenated string from the provided arguments
|
||||
* The intended purpose is to show extra annotations
|
||||
* to a music library item.
|
||||
* Ex: for a given album --> buildInfoString(album.artist, album.songCount)
|
||||
*/
|
||||
@NonNull
|
||||
public static String buildInfoString(@Nullable final String string1, @Nullable final String string2) {
|
||||
// Skip empty strings
|
||||
if (TextUtils.isEmpty(string1)) {
|
||||
//noinspection ConstantConditions
|
||||
return TextUtils.isEmpty(string2) ? "" : string2;
|
||||
}
|
||||
if (TextUtils.isEmpty(string2)) {
|
||||
//noinspection ConstantConditions
|
||||
return TextUtils.isEmpty(string1) ? "" : string1;
|
||||
}
|
||||
|
||||
return string1 + " • " + string2;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static File createAlbumArtFile() {
|
||||
return new File(createAlbumArtDir(), String.valueOf(System.currentTimeMillis()));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Intent createShareSongFileIntent(@NonNull final Song song, @NonNull Context context) {
|
||||
try {
|
||||
return new Intent()
|
||||
.setAction(Intent.ACTION_SEND)
|
||||
.putExtra(Intent.EXTRA_STREAM, FileProvider
|
||||
.getUriForFile(context, context.getApplicationContext().getPackageName(),
|
||||
new File(song.getData())))
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
.setType("audio/*");
|
||||
} catch (IllegalArgumentException e) {
|
||||
// TODO the path is most likely not like /storage/emulated/0/... but something like /storage/28C7-75B0/...
|
||||
e.printStackTrace();
|
||||
Toast.makeText(context, "Could not share this file, I'm aware of the issue.", Toast.LENGTH_SHORT).show();
|
||||
return new Intent();
|
||||
}
|
||||
}
|
||||
|
||||
public static void deleteAlbumArt(@NonNull Context context, int albumId) {
|
||||
ContentResolver contentResolver = context.getContentResolver();
|
||||
Uri localUri = Uri.parse("content://media/external/audio/albumart");
|
||||
contentResolver.delete(ContentUris.withAppendedId(localUri, albumId), null, null);
|
||||
contentResolver.notifyChange(localUri, null);
|
||||
}
|
||||
|
||||
public static void deleteTracks(
|
||||
@NonNull final Activity activity,
|
||||
@NonNull final List<Song> songs,
|
||||
@Nullable final List<Uri> safUris,
|
||||
@Nullable final Runnable callback) {
|
||||
final String[] projection = new String[]{
|
||||
BaseColumns._ID, MediaStore.MediaColumns.DATA
|
||||
};
|
||||
|
||||
// Split the query into multiple batches, and merge the resulting cursors
|
||||
int batchStart = 0;
|
||||
int batchEnd = 0;
|
||||
final int batchSize = 1000000
|
||||
/ 10; // 10^6 being the SQLite limite on the query lenth in bytes, 10 being the max number of digits in an int, used to store the track ID
|
||||
final int songCount = songs.size();
|
||||
|
||||
while (batchEnd < songCount) {
|
||||
batchStart = batchEnd;
|
||||
|
||||
final StringBuilder selection = new StringBuilder();
|
||||
selection.append(BaseColumns._ID + " IN (");
|
||||
|
||||
for (int i = 0; (i < batchSize - 1) && (batchEnd < songCount - 1); i++, batchEnd++) {
|
||||
selection.append(songs.get(batchEnd).getId());
|
||||
selection.append(",");
|
||||
}
|
||||
// The last element of a batch
|
||||
selection.append(songs.get(batchEnd).getId());
|
||||
batchEnd++;
|
||||
selection.append(")");
|
||||
|
||||
try {
|
||||
final Cursor cursor = activity.getContentResolver().query(
|
||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection.toString(),
|
||||
null, null);
|
||||
// TODO: At this point, there is no guarantee that the size of the cursor is the same as the size of the selection string.
|
||||
// Despite that, the Step 3 assumes that the safUris elements are tracking closely the content of the cursor.
|
||||
|
||||
if (cursor != null) {
|
||||
// Step 1: Remove selected tracks from the current playlist, as well
|
||||
// as from the album art cache
|
||||
cursor.moveToFirst();
|
||||
while (!cursor.isAfterLast()) {
|
||||
final int id = cursor.getInt(0);
|
||||
final Song song = SongLoader.getSong(activity, id);
|
||||
MusicPlayerRemote.removeFromQueue(song);
|
||||
cursor.moveToNext();
|
||||
}
|
||||
|
||||
// Step 2: Remove selected tracks from the database
|
||||
activity.getContentResolver().delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||
selection.toString(), null);
|
||||
|
||||
// Step 3: Remove files from card
|
||||
cursor.moveToFirst();
|
||||
int i = batchStart;
|
||||
while (!cursor.isAfterLast()) {
|
||||
final String name = cursor.getString(1);
|
||||
final Uri safUri = safUris == null || safUris.size() <= i ? null : safUris.get(i);
|
||||
SAFUtil.delete(activity, name, safUri);
|
||||
i++;
|
||||
cursor.moveToNext();
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
} catch (SecurityException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
activity.getContentResolver().notifyChange(Uri.parse("content://media"), null);
|
||||
|
||||
activity.runOnUiThread(() -> {
|
||||
Toast.makeText(activity, activity.getString(R.string.deleted_x_songs, songCount), Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
if (callback != null) {
|
||||
callback.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String getArtistInfoString(@NonNull final Context context,
|
||||
@NonNull final Artist artist) {
|
||||
int albumCount = artist.getAlbumCount();
|
||||
int songCount = artist.getSongCount();
|
||||
String albumString = albumCount == 1 ? context.getResources().getString(R.string.album)
|
||||
: context.getResources().getString(R.string.albums);
|
||||
String songString = songCount == 1 ? context.getResources().getString(R.string.song)
|
||||
: context.getResources().getString(R.string.songs);
|
||||
return albumCount + " " + albumString + " • " + songCount + " " + songString;
|
||||
}
|
||||
|
||||
//iTunes uses for example 1002 for track 2 CD1 or 3011 for track 11 CD3.
|
||||
//this method converts those values to normal tracknumbers
|
||||
public static int getFixedTrackNumber(int trackNumberToFix) {
|
||||
return trackNumberToFix % 1000;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getLyrics(@NonNull Song song) {
|
||||
String lyrics = null;
|
||||
|
||||
File file = new File(song.getData());
|
||||
|
||||
try {
|
||||
lyrics = AudioFileIO.read(file).getTagOrCreateDefault().getFirst(FieldKey.LYRICS);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
if (lyrics == null || lyrics.trim().isEmpty() || !AbsSynchronizedLyrics
|
||||
.isSynchronized(lyrics)) {
|
||||
File dir = file.getAbsoluteFile().getParentFile();
|
||||
|
||||
if (dir != null && dir.exists() && dir.isDirectory()) {
|
||||
String format = ".*%s.*\\.(lrc|txt)";
|
||||
String filename = Pattern.quote(FileUtil.stripExtension(file.getName()));
|
||||
String songtitle = Pattern.quote(song.getTitle());
|
||||
|
||||
final ArrayList<Pattern> patterns = new ArrayList<>();
|
||||
patterns.add(Pattern.compile(String.format(format, filename),
|
||||
Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE));
|
||||
patterns.add(Pattern.compile(String.format(format, songtitle),
|
||||
Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE));
|
||||
|
||||
File[] files = dir.listFiles(f -> {
|
||||
for (Pattern pattern : patterns) {
|
||||
if (pattern.matcher(f.getName()).matches()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (files != null && files.length > 0) {
|
||||
for (File f : files) {
|
||||
try {
|
||||
String newLyrics = FileUtil.read(f);
|
||||
if (newLyrics != null && !newLyrics.trim().isEmpty()) {
|
||||
if (AbsSynchronizedLyrics.isSynchronized(newLyrics)) {
|
||||
return newLyrics;
|
||||
}
|
||||
lyrics = newLyrics;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lyrics;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Uri getMediaStoreAlbumCoverUri(int albumId) {
|
||||
final Uri sArtworkUri = Uri.parse("content://media/external/audio/albumart");
|
||||
return ContentUris.withAppendedId(sArtworkUri, albumId);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Playlist getPlaylist() {
|
||||
return playlist;
|
||||
}
|
||||
|
||||
public static void setPlaylist(@NonNull Playlist playlist) {
|
||||
MusicUtil.playlist = playlist;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String getPlaylistInfoString(@NonNull final Context context, @NonNull List<Song> songs) {
|
||||
final long duration = getTotalDuration(songs);
|
||||
|
||||
return MusicUtil.buildInfoString(
|
||||
MusicUtil.getSongCountString(context, songs.size()),
|
||||
MusicUtil.getReadableDurationString(duration)
|
||||
);
|
||||
}
|
||||
|
||||
public static String getReadableDurationString(long songDurationMillis) {
|
||||
long minutes = (songDurationMillis / 1000) / 60;
|
||||
long seconds = (songDurationMillis / 1000) % 60;
|
||||
if (minutes < 60) {
|
||||
return String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds);
|
||||
} else {
|
||||
long hours = minutes / 60;
|
||||
minutes = minutes % 60;
|
||||
return String.format(Locale.getDefault(), "%02d:%02d:%02d", hours, minutes, seconds);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String getSectionName(@Nullable String musicMediaTitle) {
|
||||
try {
|
||||
if (TextUtils.isEmpty(musicMediaTitle)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
musicMediaTitle = musicMediaTitle.trim().toLowerCase();
|
||||
if (musicMediaTitle.startsWith("the ")) {
|
||||
musicMediaTitle = musicMediaTitle.substring(4);
|
||||
} else if (musicMediaTitle.startsWith("a ")) {
|
||||
musicMediaTitle = musicMediaTitle.substring(2);
|
||||
}
|
||||
if (musicMediaTitle.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return musicMediaTitle.substring(0, 1).toUpperCase();
|
||||
} catch (Exception e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String getSongCountString(@NonNull final Context context, int songCount) {
|
||||
final String songString = songCount == 1 ? context.getResources().getString(R.string.song)
|
||||
: context.getResources().getString(R.string.songs);
|
||||
return songCount + " " + songString;
|
||||
}
|
||||
|
||||
public static Uri getSongFileUri(int songId) {
|
||||
return ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, songId);
|
||||
}
|
||||
|
||||
public static long getTotalDuration(@NonNull List<Song> songs) {
|
||||
long duration = 0;
|
||||
for (int i = 0; i < songs.size(); i++) {
|
||||
duration += songs.get(i).getDuration();
|
||||
}
|
||||
return duration;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String getYearString(int year) {
|
||||
return year > 0 ? String.valueOf(year) : "-";
|
||||
}
|
||||
|
||||
public static int indexOfSongInList(@NonNull List<Song> songs, int songId) {
|
||||
for (int i = 0; i < songs.size(); i++) {
|
||||
if (songs.get(i).getId() == songId) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static void insertAlbumArt(@NonNull Context context, int albumId, String path) {
|
||||
ContentResolver contentResolver = context.getContentResolver();
|
||||
|
||||
Uri artworkUri = Uri.parse("content://media/external/audio/albumart");
|
||||
contentResolver.delete(ContentUris.withAppendedId(artworkUri, albumId), null, null);
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("album_id", albumId);
|
||||
values.put("_data", path);
|
||||
|
||||
contentResolver.insert(artworkUri, values);
|
||||
contentResolver.notifyChange(artworkUri, null);
|
||||
}
|
||||
|
||||
public static boolean isArtistNameUnknown(@Nullable String artistName) {
|
||||
if (TextUtils.isEmpty(artistName)) {
|
||||
return false;
|
||||
}
|
||||
if (artistName.equals(Artist.UNKNOWN_ARTIST_DISPLAY_NAME)) {
|
||||
return true;
|
||||
}
|
||||
String tempName = artistName.trim().toLowerCase();
|
||||
return tempName.equals("unknown") || tempName.equals("<unknown>");
|
||||
}
|
||||
|
||||
public static boolean isFavorite(@NonNull final Context context, @NonNull final Song song) {
|
||||
return PlaylistsUtil
|
||||
.doPlaylistContains(context, getFavoritesPlaylist(context).id, song.getId());
|
||||
}
|
||||
|
||||
public static boolean isFavoritePlaylist(@NonNull final Context context,
|
||||
@NonNull final Playlist playlist) {
|
||||
return playlist.name != null && playlist.name.equals(context.getString(R.string.favorites));
|
||||
}
|
||||
|
||||
public static void toggleFavorite(@NonNull final Context context, @NonNull final Song song) {
|
||||
if (isFavorite(context, song)) {
|
||||
PlaylistsUtil.removeFromPlaylist(context, song, getFavoritesPlaylist(context).id);
|
||||
} else {
|
||||
PlaylistsUtil.addToPlaylist(context, song, getOrCreateFavoritesPlaylist(context).id,
|
||||
false);
|
||||
}
|
||||
context.sendBroadcast(new Intent(MusicService.FAVORITE_STATE_CHANGED));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
private static File createAlbumArtDir() {
|
||||
File albumArtDir = new File(Environment.getExternalStorageDirectory(), "/albumthumbs/");
|
||||
if (!albumArtDir.exists()) {
|
||||
albumArtDir.mkdirs();
|
||||
try {
|
||||
new File(albumArtDir, ".nomedia").createNewFile();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return albumArtDir;
|
||||
}
|
||||
|
||||
private static Playlist getFavoritesPlaylist(@NonNull final Context context) {
|
||||
return PlaylistLoader.INSTANCE.getPlaylist(context, context.getString(R.string.favorites));
|
||||
}
|
||||
|
||||
private static Playlist getOrCreateFavoritesPlaylist(@NonNull final Context context) {
|
||||
return PlaylistLoader.INSTANCE.getPlaylist(context,
|
||||
PlaylistsUtil.createPlaylist(context, context.getString(R.string.favorites)));
|
||||
}
|
||||
}
|
418
app/src/main/java/code/name/monkey/retromusic/util/MusicUtil.kt
Normal file
418
app/src/main/java/code/name/monkey/retromusic/util/MusicUtil.kt
Normal file
|
@ -0,0 +1,418 @@
|
|||
package code.name.monkey.retromusic.util
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.provider.BaseColumns
|
||||
import android.provider.MediaStore
|
||||
import android.text.TextUtils
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import code.name.monkey.retromusic.R
|
||||
import code.name.monkey.retromusic.helper.MusicPlayerRemote.removeFromQueue
|
||||
import code.name.monkey.retromusic.model.Artist
|
||||
import code.name.monkey.retromusic.model.Playlist
|
||||
import code.name.monkey.retromusic.model.Song
|
||||
import code.name.monkey.retromusic.model.lyrics.AbsSynchronizedLyrics
|
||||
import code.name.monkey.retromusic.repository.RealPlaylistRepository
|
||||
import code.name.monkey.retromusic.repository.SongRepository
|
||||
import code.name.monkey.retromusic.service.MusicService
|
||||
import org.jaudiotagger.audio.AudioFileIO
|
||||
import org.jaudiotagger.tag.FieldKey
|
||||
import org.koin.core.KoinComponent
|
||||
import org.koin.core.get
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
|
||||
|
||||
object MusicUtil : KoinComponent {
|
||||
fun createShareSongFileIntent(song: Song, context: Context): Intent? {
|
||||
return try {
|
||||
Intent().setAction(Intent.ACTION_SEND).putExtra(
|
||||
Intent.EXTRA_STREAM,
|
||||
FileProvider.getUriForFile(
|
||||
context,
|
||||
context.applicationContext.packageName,
|
||||
File(song.data)
|
||||
)
|
||||
).addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION).setType("audio/*")
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// TODO the path is most likely not like /storage/emulated/0/... but something like /storage/28C7-75B0/...
|
||||
e.printStackTrace()
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Could not share this file, I'm aware of the issue.",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
Intent()
|
||||
}
|
||||
}
|
||||
|
||||
fun buildInfoString(string1: String?, string2: String?): String {
|
||||
if (string1.isNullOrEmpty()) {
|
||||
return if (string2.isNullOrEmpty()) "" else string2
|
||||
}
|
||||
return if (string2.isNullOrEmpty()) if (string1.isNullOrEmpty()) "" else string1 else "$string1 • $string2"
|
||||
}
|
||||
|
||||
fun createAlbumArtFile(): File {
|
||||
return File(
|
||||
createAlbumArtDir(),
|
||||
System.currentTimeMillis().toString()
|
||||
)
|
||||
}
|
||||
|
||||
private fun createAlbumArtDir(): File {
|
||||
val albumArtDir = File(Environment.getExternalStorageDirectory(), "/albumthumbs/")
|
||||
if (!albumArtDir.exists()) {
|
||||
albumArtDir.mkdirs()
|
||||
try {
|
||||
File(albumArtDir, ".nomedia").createNewFile()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
return albumArtDir
|
||||
}
|
||||
|
||||
fun deleteAlbumArt(context: Context, albumId: Int) {
|
||||
val contentResolver = context.contentResolver
|
||||
val localUri = Uri.parse("content://media/external/audio/albumart")
|
||||
contentResolver.delete(ContentUris.withAppendedId(localUri, albumId.toLong()), null, null)
|
||||
contentResolver.notifyChange(localUri, null)
|
||||
}
|
||||
|
||||
fun getArtistInfoString(
|
||||
context: Context,
|
||||
artist: Artist
|
||||
): String {
|
||||
val albumCount = artist.albumCount
|
||||
val songCount = artist.songCount
|
||||
val albumString =
|
||||
if (albumCount == 1) context.resources.getString(R.string.album)
|
||||
else context.resources.getString(R.string.albums)
|
||||
val songString =
|
||||
if (songCount == 1) context.resources.getString(R.string.song)
|
||||
else context.resources.getString(R.string.songs)
|
||||
return "$albumCount $albumString • $songCount $songString"
|
||||
}
|
||||
|
||||
//iTunes uses for example 1002 for track 2 CD1 or 3011 for track 11 CD3.
|
||||
//this method converts those values to normal tracknumbers
|
||||
fun getFixedTrackNumber(trackNumberToFix: Int): Int {
|
||||
return trackNumberToFix % 1000
|
||||
}
|
||||
|
||||
fun getLyrics(song: Song): String? {
|
||||
var lyrics: String? = null
|
||||
val file = File(song.data)
|
||||
try {
|
||||
lyrics = AudioFileIO.read(file).tagOrCreateDefault.getFirst(FieldKey.LYRICS)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
if (lyrics == null || lyrics.trim { it <= ' ' }.isEmpty() || !AbsSynchronizedLyrics
|
||||
.isSynchronized(lyrics)
|
||||
) {
|
||||
val dir = file.absoluteFile.parentFile
|
||||
if (dir != null && dir.exists() && dir.isDirectory) {
|
||||
val format = ".*%s.*\\.(lrc|txt)"
|
||||
val filename = Pattern.quote(
|
||||
FileUtil.stripExtension(file.name)
|
||||
)
|
||||
val songtitle = Pattern.quote(song.title)
|
||||
val patterns =
|
||||
ArrayList<Pattern>()
|
||||
patterns.add(
|
||||
Pattern.compile(
|
||||
String.format(format, filename),
|
||||
Pattern.CASE_INSENSITIVE or Pattern.UNICODE_CASE
|
||||
)
|
||||
)
|
||||
patterns.add(
|
||||
Pattern.compile(
|
||||
String.format(format, songtitle),
|
||||
Pattern.CASE_INSENSITIVE or Pattern.UNICODE_CASE
|
||||
)
|
||||
)
|
||||
val files =
|
||||
dir.listFiles { f: File ->
|
||||
for (pattern in patterns) {
|
||||
if (pattern.matcher(f.name).matches()) {
|
||||
return@listFiles true
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
if (files != null && files.size > 0) {
|
||||
for (f in files) {
|
||||
try {
|
||||
val newLyrics =
|
||||
FileUtil.read(f)
|
||||
if (newLyrics != null && !newLyrics.trim { it <= ' ' }.isEmpty()) {
|
||||
if (AbsSynchronizedLyrics.isSynchronized(newLyrics)) {
|
||||
return newLyrics
|
||||
}
|
||||
lyrics = newLyrics
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return lyrics
|
||||
}
|
||||
|
||||
fun getMediaStoreAlbumCoverUri(albumId: Int): Uri {
|
||||
val sArtworkUri =
|
||||
Uri.parse("content://media/external/audio/albumart")
|
||||
return ContentUris.withAppendedId(sArtworkUri, albumId.toLong())
|
||||
}
|
||||
|
||||
|
||||
fun getPlaylistInfoString(
|
||||
context: Context,
|
||||
songs: List<Song>
|
||||
): String {
|
||||
val duration = getTotalDuration(songs)
|
||||
return buildInfoString(
|
||||
getSongCountString(context, songs.size),
|
||||
getReadableDurationString(duration)
|
||||
)
|
||||
}
|
||||
|
||||
fun getReadableDurationString(songDurationMillis: Long): String? {
|
||||
var minutes = songDurationMillis / 1000 / 60
|
||||
val seconds = songDurationMillis / 1000 % 60
|
||||
return if (minutes < 60) {
|
||||
String.format(
|
||||
Locale.getDefault(),
|
||||
"%02d:%02d",
|
||||
minutes,
|
||||
seconds
|
||||
)
|
||||
} else {
|
||||
val hours = minutes / 60
|
||||
minutes = minutes % 60
|
||||
String.format(
|
||||
Locale.getDefault(),
|
||||
"%02d:%02d:%02d",
|
||||
hours,
|
||||
minutes,
|
||||
seconds
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getSectionName(musicMediaTitle: String?): String {
|
||||
var musicMediaTitle = musicMediaTitle
|
||||
return try {
|
||||
if (TextUtils.isEmpty(musicMediaTitle)) {
|
||||
return ""
|
||||
}
|
||||
musicMediaTitle = musicMediaTitle!!.trim { it <= ' ' }.toLowerCase()
|
||||
if (musicMediaTitle.startsWith("the ")) {
|
||||
musicMediaTitle = musicMediaTitle.substring(4)
|
||||
} else if (musicMediaTitle.startsWith("a ")) {
|
||||
musicMediaTitle = musicMediaTitle.substring(2)
|
||||
}
|
||||
if (musicMediaTitle.isEmpty()) {
|
||||
""
|
||||
} else musicMediaTitle.substring(0, 1).toUpperCase()
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
fun getSongCountString(context: Context, songCount: Int): String {
|
||||
val songString = if (songCount == 1) context.resources
|
||||
.getString(R.string.song) else context.resources.getString(R.string.songs)
|
||||
return "$songCount $songString"
|
||||
}
|
||||
|
||||
fun getSongFileUri(songId: Int): Uri {
|
||||
return ContentUris.withAppendedId(
|
||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||
songId.toLong()
|
||||
)
|
||||
}
|
||||
|
||||
fun getTotalDuration(songs: List<Song>): Long {
|
||||
var duration: Long = 0
|
||||
for (i in songs.indices) {
|
||||
duration += songs[i].duration
|
||||
}
|
||||
return duration
|
||||
}
|
||||
|
||||
fun getYearString(year: Int): String {
|
||||
return if (year > 0) year.toString() else "-"
|
||||
}
|
||||
|
||||
fun indexOfSongInList(songs: List<Song>, songId: Int): Int {
|
||||
for (i in songs.indices) {
|
||||
if (songs[i].id == songId) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
fun insertAlbumArt(
|
||||
context: Context,
|
||||
albumId: Int,
|
||||
path: String?
|
||||
) {
|
||||
val contentResolver = context.contentResolver
|
||||
val artworkUri =
|
||||
Uri.parse("content://media/external/audio/albumart")
|
||||
contentResolver.delete(ContentUris.withAppendedId(artworkUri, albumId.toLong()), null, null)
|
||||
val values = ContentValues()
|
||||
values.put("album_id", albumId)
|
||||
values.put("_data", path)
|
||||
contentResolver.insert(artworkUri, values)
|
||||
contentResolver.notifyChange(artworkUri, null)
|
||||
}
|
||||
|
||||
fun isArtistNameUnknown(artistName: String?): Boolean {
|
||||
if (TextUtils.isEmpty(artistName)) {
|
||||
return false
|
||||
}
|
||||
if (artistName == Artist.UNKNOWN_ARTIST_DISPLAY_NAME) {
|
||||
return true
|
||||
}
|
||||
val tempName = artistName!!.trim { it <= ' ' }.toLowerCase()
|
||||
return tempName == "unknown" || tempName == "<unknown>"
|
||||
}
|
||||
|
||||
fun isFavorite(context: Context, song: Song): Boolean {
|
||||
return PlaylistsUtil
|
||||
.doPlaylistContains(context, getFavoritesPlaylist(context).id.toLong(), song.id)
|
||||
}
|
||||
|
||||
fun isFavoritePlaylist(
|
||||
context: Context,
|
||||
playlist: Playlist
|
||||
): Boolean {
|
||||
return playlist.name != null && playlist.name == context.getString(R.string.favorites)
|
||||
}
|
||||
|
||||
fun toggleFavorite(context: Context, song: Song) {
|
||||
if (isFavorite(context, song)) {
|
||||
PlaylistsUtil.removeFromPlaylist(context, song, getFavoritesPlaylist(context).id)
|
||||
} else {
|
||||
PlaylistsUtil.addToPlaylist(
|
||||
context, song, getOrCreateFavoritesPlaylist(context).id,
|
||||
false
|
||||
)
|
||||
}
|
||||
context.sendBroadcast(Intent(MusicService.FAVORITE_STATE_CHANGED))
|
||||
}
|
||||
|
||||
private fun getFavoritesPlaylist(context: Context): Playlist {
|
||||
return RealPlaylistRepository(context.contentResolver).playlist(context.getString(R.string.favorites))
|
||||
}
|
||||
|
||||
private fun getOrCreateFavoritesPlaylist(context: Context): Playlist {
|
||||
return RealPlaylistRepository(context.contentResolver).playlist(
|
||||
PlaylistsUtil.createPlaylist(
|
||||
context,
|
||||
context.getString(R.string.favorites)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteTracks(
|
||||
activity: FragmentActivity,
|
||||
songs: List<Song>,
|
||||
safUris: List<Uri>?,
|
||||
callback: Runnable?
|
||||
) {
|
||||
val songRepository: SongRepository = get()
|
||||
val projection = arrayOf(
|
||||
BaseColumns._ID, MediaStore.MediaColumns.DATA
|
||||
)
|
||||
// Split the query into multiple batches, and merge the resulting cursors
|
||||
var batchStart = 0
|
||||
var batchEnd = 0
|
||||
val batchSize =
|
||||
1000000 / 10 // 10^6 being the SQLite limite on the query lenth in bytes, 10 being the max number of digits in an int, used to store the track ID
|
||||
val songCount = songs.size
|
||||
|
||||
while (batchEnd < songCount) {
|
||||
batchStart = batchEnd
|
||||
|
||||
val selection = StringBuilder()
|
||||
selection.append(BaseColumns._ID + " IN (")
|
||||
|
||||
var i = 0
|
||||
while (i < batchSize - 1 && batchEnd < songCount - 1) {
|
||||
selection.append(songs[batchEnd].id)
|
||||
selection.append(",")
|
||||
i++
|
||||
batchEnd++
|
||||
}
|
||||
// The last element of a batch
|
||||
// The last element of a batch
|
||||
selection.append(songs[batchEnd].id)
|
||||
batchEnd++
|
||||
selection.append(")")
|
||||
|
||||
try {
|
||||
val cursor = activity.contentResolver.query(
|
||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection.toString(),
|
||||
null, null
|
||||
)
|
||||
if (cursor != null) {
|
||||
// Step 1: Remove selected tracks from the current playlist, as well
|
||||
// as from the album art cache
|
||||
cursor.moveToFirst()
|
||||
while (!cursor.isAfterLast) {
|
||||
val id = cursor.getInt(0)
|
||||
val song: Song = songRepository.song(id)
|
||||
removeFromQueue(song)
|
||||
cursor.moveToNext()
|
||||
}
|
||||
|
||||
// Step 2: Remove selected tracks from the database
|
||||
activity.contentResolver.delete(
|
||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||
selection.toString(), null
|
||||
)
|
||||
// Step 3: Remove files from card
|
||||
cursor.moveToFirst()
|
||||
var index = batchStart
|
||||
while (!cursor.isAfterLast) {
|
||||
val name = cursor.getString(1)
|
||||
val safUri =
|
||||
if (safUris == null || safUris.size <= index) null else safUris[index]
|
||||
SAFUtil.delete(activity, name, safUri)
|
||||
index++
|
||||
cursor.moveToNext()
|
||||
}
|
||||
cursor.close()
|
||||
}
|
||||
} catch (ignored: SecurityException) {
|
||||
|
||||
}
|
||||
activity.contentResolver.notifyChange(Uri.parse("content://media"), null)
|
||||
activity.runOnUiThread {
|
||||
Toast.makeText(
|
||||
activity,
|
||||
activity.getString(R.string.deleted_x_songs, songCount),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
callback?.run()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -87,7 +87,7 @@ public class PlaylistsUtil {
|
|||
final StringBuilder selection = new StringBuilder();
|
||||
selection.append(MediaStore.Audio.Playlists._ID + " IN (");
|
||||
for (int i = 0; i < playlists.size(); i++) {
|
||||
selection.append(playlists.get(i).id);
|
||||
selection.append(playlists.get(i).getId());
|
||||
if (i < playlists.size() - 1) {
|
||||
selection.append(",");
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue