V5 Push
Here's a list of changes/features: https://github.com/RetroMusicPlayer/RetroMusicPlayer/releases/tag/v5.0 Internal Changes: 1) Migrated to ViewBinding 2) Migrated to Glide V4 3) Migrated to kotlin version of Material Dialogs
This commit is contained in:
parent
fc42767031
commit
bce6dbfa27
421 changed files with 13285 additions and 5757 deletions
|
@ -60,7 +60,7 @@ object AppRater {
|
|||
}
|
||||
}
|
||||
|
||||
editor.commit()
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
private fun showPlayStoreReviewDialog(context: Activity, editor: SharedPreferences.Editor) {
|
||||
|
|
|
@ -14,11 +14,12 @@
|
|||
|
||||
package code.name.monkey.retromusic.util;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import com.bumptech.glide.signature.StringSignature;
|
||||
|
||||
import com.bumptech.glide.signature.ObjectKey;
|
||||
|
||||
/** @author Karim Abou Zeid (kabouzeid) */
|
||||
public class ArtistSignatureUtil {
|
||||
|
@ -39,16 +40,15 @@ public class ArtistSignatureUtil {
|
|||
return sInstance;
|
||||
}
|
||||
|
||||
@SuppressLint("CommitPrefEdits")
|
||||
public void updateArtistSignature(String artistName) {
|
||||
mPreferences.edit().putLong(artistName, System.currentTimeMillis()).commit();
|
||||
mPreferences.edit().putLong(artistName, System.currentTimeMillis()).apply();
|
||||
}
|
||||
|
||||
public long getArtistSignatureRaw(String artistName) {
|
||||
return mPreferences.getLong(artistName, 0);
|
||||
}
|
||||
|
||||
public StringSignature getArtistSignature(String artistName) {
|
||||
return new StringSignature(String.valueOf(getArtistSignatureRaw(artistName)));
|
||||
public ObjectKey getArtistSignature(String artistName) {
|
||||
return new ObjectKey(String.valueOf(getArtistSignatureRaw(artistName)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,49 +31,19 @@ public class AutoGeneratedPlaylistBitmap {
|
|||
*/
|
||||
public static Bitmap getBitmap(
|
||||
Context context, List<Song> songPlaylist, boolean round, boolean blur) {
|
||||
if (songPlaylist == null || songPlaylist.isEmpty()) return null;
|
||||
long start = System.currentTimeMillis();
|
||||
// lấy toàn bộ album id, loại bỏ trùng nhau
|
||||
if (songPlaylist == null || songPlaylist.isEmpty()) return getDefaultBitmap(context, round);
|
||||
if (songPlaylist.size() == 1) return getBitmapWithAlbumId(context, songPlaylist.get(0).getAlbumId());
|
||||
List<Long> albumID = new ArrayList<>();
|
||||
for (Song song : songPlaylist) {
|
||||
if (!albumID.contains(song.getAlbumId())) albumID.add(song.getAlbumId());
|
||||
}
|
||||
|
||||
long start2 = System.currentTimeMillis() - start;
|
||||
|
||||
// lấy toàn bộ art tồn tại
|
||||
List<Bitmap> art = new ArrayList<Bitmap>();
|
||||
List<Bitmap> art = new ArrayList<>();
|
||||
for (Long id : albumID) {
|
||||
Bitmap bitmap = getBitmapWithAlbumId(context, id);
|
||||
if (bitmap != null) art.add(bitmap);
|
||||
if (art.size() == 6) break;
|
||||
if (art.size() == 9) break;
|
||||
}
|
||||
return MergedImageUtils.INSTANCE.joinImages(art);
|
||||
/*
|
||||
|
||||
long start3 = System.currentTimeMillis() - start2 - start;
|
||||
Bitmap ret;
|
||||
switch (art.size()) {
|
||||
// lấy hình mặc định
|
||||
case 0:
|
||||
ret = getDefaultBitmap(context, round).copy(Bitmap.Config.ARGB_8888, false);
|
||||
break;
|
||||
// dùng hình duy nhất
|
||||
case 1:
|
||||
if (round)
|
||||
ret = BitmapEditor.getRoundedCornerBitmap(art.get(0), art.get(0).getWidth() / 40);
|
||||
else ret = art.get(0);
|
||||
break;
|
||||
// từ 2 trở lên ta cần vẽ canvas
|
||||
default:
|
||||
ret = getBitmapCollection(art, round);
|
||||
}
|
||||
int w = ret.getWidth();
|
||||
if (blur)
|
||||
return BitmapEditor.GetRoundedBitmapWithBlurShadow(context, ret, w / 24, w / 24, w / 24, w / 24, 0, 200, w / 40, 1);
|
||||
|
||||
Log.d(TAG, "getBitmap: time = " + (System.currentTimeMillis() - start) + ", start2 = " + start2 + ", start3 = " + start3);
|
||||
return ret;*/
|
||||
}
|
||||
|
||||
private static Bitmap getBitmapCollection(ArrayList<Bitmap> art, boolean round) {
|
||||
|
@ -175,9 +145,9 @@ public class AutoGeneratedPlaylistBitmap {
|
|||
private static Bitmap getBitmapWithAlbumId(@NonNull Context context, Long id) {
|
||||
try {
|
||||
return Glide.with(context)
|
||||
.load(MusicUtil.INSTANCE.getMediaStoreAlbumCoverUri(id))
|
||||
.asBitmap()
|
||||
.into(200, 200)
|
||||
.load(MusicUtil.INSTANCE.getMediaStoreAlbumCoverUri(id))
|
||||
.submit(200, 200)
|
||||
.get();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
|
@ -185,8 +155,6 @@ public class AutoGeneratedPlaylistBitmap {
|
|||
}
|
||||
|
||||
public static Bitmap getDefaultBitmap(@NonNull Context context, boolean round) {
|
||||
if (round)
|
||||
return BitmapFactory.decodeResource(context.getResources(), R.drawable.default_album_art);
|
||||
return BitmapFactory.decodeResource(context.getResources(), R.drawable.default_album_art);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
package code.name.monkey.retromusic.util;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.palette.graphics.Palette;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ package code.name.monkey.retromusic.util;
|
|||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
|
|
|
@ -27,8 +27,8 @@ import code.name.monkey.retromusic.App
|
|||
import code.name.monkey.retromusic.model.Artist
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.request.animation.GlideAnimation
|
||||
import com.bumptech.glide.request.target.SimpleTarget
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
@ -38,34 +38,26 @@ import java.util.*
|
|||
|
||||
class CustomArtistImageUtil private constructor(context: Context) {
|
||||
|
||||
private val mPreferences: SharedPreferences
|
||||
|
||||
init {
|
||||
mPreferences = context.applicationContext.getSharedPreferences(
|
||||
CUSTOM_ARTIST_IMAGE_PREFS,
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
}
|
||||
private val mPreferences: SharedPreferences = context.applicationContext.getSharedPreferences(
|
||||
CUSTOM_ARTIST_IMAGE_PREFS,
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
|
||||
fun setCustomArtistImage(artist: Artist, uri: Uri) {
|
||||
Glide.with(App.getContext())
|
||||
.load(uri)
|
||||
.asBitmap()
|
||||
.load(uri)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.into(object : SimpleTarget<Bitmap>() {
|
||||
override fun onLoadFailed(e: Exception?, errorDrawable: Drawable?) {
|
||||
super.onLoadFailed(e, errorDrawable)
|
||||
e!!.printStackTrace()
|
||||
Toast.makeText(App.getContext(), e.toString(), Toast.LENGTH_LONG).show()
|
||||
override fun onLoadFailed(errorDrawable: Drawable?) {
|
||||
super.onLoadFailed(errorDrawable)
|
||||
Toast.makeText(App.getContext(), "Load Failed", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun onResourceReady(
|
||||
resource: Bitmap,
|
||||
glideAnimation: GlideAnimation<in Bitmap>
|
||||
) {
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
|
||||
object : AsyncTask<Void, Void, Void>() {
|
||||
@SuppressLint("ApplySharedPref")
|
||||
override fun doInBackground(vararg params: Void): Void? {
|
||||
val dir = File(App.getContext().filesDir, FOLDER_NAME)
|
||||
if (!dir.exists()) {
|
||||
|
@ -87,7 +79,7 @@ class CustomArtistImageUtil private constructor(context: Context) {
|
|||
}
|
||||
|
||||
if (succesful) {
|
||||
mPreferences.edit().putBoolean(getFileName(artist), true).commit()
|
||||
mPreferences.edit().putBoolean(getFileName(artist), true).apply()
|
||||
ArtistSignatureUtil.getInstance(App.getContext())
|
||||
.updateArtistSignature(artist.name)
|
||||
App.getContext().contentResolver.notifyChange(
|
||||
|
@ -102,6 +94,7 @@ class CustomArtistImageUtil private constructor(context: Context) {
|
|||
})
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
fun resetCustomArtistImage(artist: Artist) {
|
||||
object : AsyncTask<Void, Void, Void>() {
|
||||
@SuppressLint("ApplySharedPref")
|
||||
|
|
|
@ -13,9 +13,7 @@
|
|||
*/
|
||||
package code.name.monkey.retromusic.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.util.DisplayMetrics
|
||||
import android.util.TypedValue
|
||||
|
||||
/**
|
||||
|
@ -23,14 +21,12 @@ import android.util.TypedValue
|
|||
*/
|
||||
object DensityUtil {
|
||||
fun getScreenHeight(context: Context): Int {
|
||||
val displayMetrics = DisplayMetrics()
|
||||
(context as Activity).windowManager.defaultDisplay.getMetrics(displayMetrics)
|
||||
val displayMetrics = context.resources.displayMetrics
|
||||
return displayMetrics.heightPixels
|
||||
}
|
||||
|
||||
fun getScreenWidth(context: Context): Int {
|
||||
val displayMetrics = DisplayMetrics()
|
||||
(context as Activity).windowManager.defaultDisplay.getMetrics(displayMetrics)
|
||||
val displayMetrics = context.resources.displayMetrics
|
||||
return displayMetrics.widthPixels
|
||||
}
|
||||
|
||||
|
|
|
@ -19,11 +19,10 @@ import android.database.Cursor;
|
|||
import android.os.Environment;
|
||||
import android.provider.MediaStore;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import code.name.monkey.retromusic.model.Song;
|
||||
import code.name.monkey.retromusic.repository.RealSongRepository;
|
||||
import code.name.monkey.retromusic.repository.SortedCursor;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
|
@ -37,6 +36,10 @@ import java.util.Collections;
|
|||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
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 {
|
||||
|
||||
private FileUtil() {}
|
||||
|
|
|
@ -26,17 +26,20 @@ import android.graphics.PorterDuffColorFilter;
|
|||
import android.graphics.drawable.Drawable;
|
||||
import android.media.ExifInterface;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
|
||||
import code.name.monkey.appthemehelper.util.TintHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import code.name.monkey.appthemehelper.util.TintHelper;
|
||||
|
||||
/**
|
||||
* Created on : June 18, 2016 Author : zetbaitsu Name : Zetra GitHub : https://github.com/zetbaitsu
|
||||
*/
|
||||
|
|
|
@ -15,120 +15,175 @@
|
|||
package code.name.monkey.retromusic.util;
|
||||
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/** Created by hefuyi on 2016/11/8. */
|
||||
import code.name.monkey.retromusic.model.Song;
|
||||
|
||||
/**
|
||||
* Created by hefuyi on 2016/11/8.
|
||||
*/
|
||||
public class LyricUtil {
|
||||
|
||||
private static final String lrcRootPath =
|
||||
android.os.Environment.getExternalStorageDirectory().toString() + "/RetroMusic/lyrics/";
|
||||
private static final String TAG = "LyricUtil";
|
||||
private static final String lrcRootPath =
|
||||
android.os.Environment.getExternalStorageDirectory().toString() + "/RetroMusic/lyrics/";
|
||||
private static final String TAG = "LyricUtil";
|
||||
|
||||
@Nullable
|
||||
public static File writeLrcToLoc(
|
||||
@NonNull String title, @NonNull String artist, @NonNull String lrcContext) {
|
||||
FileWriter writer = null;
|
||||
try {
|
||||
File file = new File(getLrcPath(title, artist));
|
||||
if (!file.getParentFile().exists()) {
|
||||
file.getParentFile().mkdirs();
|
||||
}
|
||||
writer = new FileWriter(getLrcPath(title, artist));
|
||||
writer.write(lrcContext);
|
||||
return file;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
} finally {
|
||||
try {
|
||||
if (writer != null) writer.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
@Nullable
|
||||
public static File writeLrcToLoc(
|
||||
@NonNull String title, @NonNull String artist, @NonNull String lrcContext) {
|
||||
FileWriter writer = null;
|
||||
try {
|
||||
File file = new File(getLrcPath(title, artist));
|
||||
if (!file.getParentFile().exists()) {
|
||||
file.getParentFile().mkdirs();
|
||||
}
|
||||
writer = new FileWriter(getLrcPath(title, artist));
|
||||
writer.write(lrcContext);
|
||||
return file;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
} finally {
|
||||
try {
|
||||
if (writer != null) writer.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean deleteLrcFile(@NonNull String title, @NonNull String artist) {
|
||||
File file = new File(getLrcPath(title, artist));
|
||||
return file.delete();
|
||||
}
|
||||
//So in Retro, Lrc file can be same folder as Music File or in RetroMusic Folder
|
||||
// In this case we pass location of the file and Contents to write to file
|
||||
public static void writeLrc(@NonNull Song song, @NonNull String lrcContext) {
|
||||
FileWriter writer = null;
|
||||
File location;
|
||||
try {
|
||||
if (isLrcOriginalFileExist(song.getData())) {
|
||||
location = getLocalLyricOriginalFile(song.getData());
|
||||
} else if (isLrcFileExist(song.getTitle(), song.getArtistName())) {
|
||||
location = getLocalLyricFile(song.getTitle(), song.getArtistName());
|
||||
} else {
|
||||
location = new File(getLrcPath(song.getTitle(), song.getArtistName()));
|
||||
if (!location.getParentFile().exists()) {
|
||||
location.getParentFile().mkdirs();
|
||||
}
|
||||
}
|
||||
writer = new FileWriter(location);
|
||||
writer.write(lrcContext);
|
||||
|
||||
public static boolean isLrcFileExist(@NonNull String title, @NonNull String artist) {
|
||||
File file = new File(getLrcPath(title, artist));
|
||||
return file.exists();
|
||||
}
|
||||
|
||||
public static boolean isLrcOriginalFileExist(@NonNull String path) {
|
||||
File file = new File(getLrcOriginalPath(path));
|
||||
return file.exists();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static File getLocalLyricFile(@NonNull String title, @NonNull String artist) {
|
||||
File file = new File(getLrcPath(title, artist));
|
||||
if (file.exists()) {
|
||||
return file;
|
||||
} else {
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
try {
|
||||
if (writer != null) writer.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static File getLocalLyricOriginalFile(@NonNull String path) {
|
||||
File file = new File(getLrcOriginalPath(path));
|
||||
if (file.exists()) {
|
||||
return file;
|
||||
} else {
|
||||
return null;
|
||||
public static boolean deleteLrcFile(@NonNull String title, @NonNull String artist) {
|
||||
File file = new File(getLrcPath(title, artist));
|
||||
return file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
private static String getLrcPath(String title, String artist) {
|
||||
return lrcRootPath + title + " - " + artist + ".lrc";
|
||||
}
|
||||
|
||||
private static String getLrcOriginalPath(String filePath) {
|
||||
return filePath.replace(filePath.substring(filePath.lastIndexOf(".") + 1), "lrc");
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String decryptBASE64(@NonNull String str) {
|
||||
if (str == null || str.length() == 0) {
|
||||
return null;
|
||||
public static boolean isLrcFileExist(@NonNull String title, @NonNull String artist) {
|
||||
File file = new File(getLrcPath(title, artist));
|
||||
return file.exists();
|
||||
}
|
||||
byte[] encode = str.getBytes(StandardCharsets.UTF_8);
|
||||
// base64 解密
|
||||
return new String(
|
||||
Base64.decode(encode, 0, encode.length, Base64.DEFAULT), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String getStringFromFile(@NonNull String title, @NonNull String artist)
|
||||
throws Exception {
|
||||
File file = new File(getLrcPath(title, artist));
|
||||
FileInputStream fin = new FileInputStream(file);
|
||||
String ret = convertStreamToString(fin);
|
||||
fin.close();
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static String convertStreamToString(InputStream is) throws Exception {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line = null;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
sb.append(line).append("\n");
|
||||
public static boolean isLrcOriginalFileExist(@NonNull String path) {
|
||||
File file = new File(getLrcOriginalPath(path));
|
||||
return file.exists();
|
||||
}
|
||||
reader.close();
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static File getLocalLyricFile(@NonNull String title, @NonNull String artist) {
|
||||
File file = new File(getLrcPath(title, artist));
|
||||
if (file.exists()) {
|
||||
return file;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static File getLocalLyricOriginalFile(@NonNull String path) {
|
||||
File file = new File(getLrcOriginalPath(path));
|
||||
if (file.exists()) {
|
||||
return file;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String getLrcPath(String title, String artist) {
|
||||
return lrcRootPath + title + " - " + artist + ".lrc";
|
||||
}
|
||||
|
||||
private static String getLrcOriginalPath(String filePath) {
|
||||
return filePath.replace(filePath.substring(filePath.lastIndexOf(".") + 1), "lrc");
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String decryptBASE64(@NonNull String str) {
|
||||
if (str == null || str.length() == 0) {
|
||||
return null;
|
||||
}
|
||||
byte[] encode = str.getBytes(StandardCharsets.UTF_8);
|
||||
// base64 解密
|
||||
return new String(
|
||||
Base64.decode(encode, 0, encode.length, Base64.DEFAULT), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String getStringFromFile(@NonNull String title, @NonNull String artist)
|
||||
throws Exception {
|
||||
File file = new File(getLrcPath(title, artist));
|
||||
FileInputStream fin = new FileInputStream(file);
|
||||
String ret = convertStreamToString(fin);
|
||||
fin.close();
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static String convertStreamToString(InputStream is) throws Exception {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
sb.append(line).append("\n");
|
||||
}
|
||||
reader.close();
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String getStringFromLrc(File file) {
|
||||
try {
|
||||
BufferedReader reader = new BufferedReader(new FileReader(file));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
sb.append(line).append("\n");
|
||||
}
|
||||
reader.close();
|
||||
return sb.toString();
|
||||
} catch (Exception e) {
|
||||
Log.i("Error", "Error Occurred");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ internal object MergedImageUtils {
|
|||
fun joinImages(list: List<Bitmap>): Bitmap {
|
||||
assertBackgroundThread()
|
||||
|
||||
val arranged = arrangeBitmaps(list.shuffled())
|
||||
val arranged = arrangeBitmaps(list)
|
||||
|
||||
val mergedImage = create(
|
||||
arranged,
|
||||
|
|
|
@ -29,8 +29,10 @@ import code.name.monkey.retromusic.repository.RealSongRepository
|
|||
import code.name.monkey.retromusic.repository.Repository
|
||||
import code.name.monkey.retromusic.repository.SongRepository
|
||||
import code.name.monkey.retromusic.service.MusicService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jaudiotagger.audio.AudioFileIO
|
||||
import org.jaudiotagger.tag.FieldKey
|
||||
import org.koin.core.KoinComponent
|
||||
|
@ -127,7 +129,7 @@ object MusicUtil : KoinComponent {
|
|||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
if (lyrics == null || lyrics.trim { it <= ' ' }.isEmpty() || !AbsSynchronizedLyrics
|
||||
if (lyrics == null || lyrics.trim { it <= ' ' }.isEmpty() || AbsSynchronizedLyrics
|
||||
.isSynchronized(lyrics)
|
||||
) {
|
||||
val dir = file.absoluteFile.parentFile
|
||||
|
@ -205,7 +207,7 @@ object MusicUtil : KoinComponent {
|
|||
return getSongCountString(context, songs.size)
|
||||
}
|
||||
|
||||
fun getReadableDurationString(songDurationMillis: Long): String? {
|
||||
fun getReadableDurationString(songDurationMillis: Long): String {
|
||||
var minutes = songDurationMillis / 1000 / 60
|
||||
val seconds = songDurationMillis / 1000 % 60
|
||||
return if (minutes < 60) {
|
||||
|
@ -217,7 +219,7 @@ object MusicUtil : KoinComponent {
|
|||
)
|
||||
} else {
|
||||
val hours = minutes / 60
|
||||
minutes = minutes % 60
|
||||
minutes %= 60
|
||||
String.format(
|
||||
Locale.getDefault(),
|
||||
"%02d:%02d:%02d",
|
||||
|
@ -228,13 +230,13 @@ object MusicUtil : KoinComponent {
|
|||
}
|
||||
}
|
||||
|
||||
fun getSectionName(musicMediaTitle: String?): String {
|
||||
var musicMediaTitle = musicMediaTitle
|
||||
fun getSectionName(mediaTitle: String?): String {
|
||||
var musicMediaTitle = mediaTitle
|
||||
return try {
|
||||
if (TextUtils.isEmpty(musicMediaTitle)) {
|
||||
return ""
|
||||
}
|
||||
musicMediaTitle = musicMediaTitle!!.trim { it <= ' ' }.toLowerCase()
|
||||
musicMediaTitle = musicMediaTitle!!.trim { it <= ' ' }.lowercase()
|
||||
if (musicMediaTitle.startsWith("the ")) {
|
||||
musicMediaTitle = musicMediaTitle.substring(4)
|
||||
} else if (musicMediaTitle.startsWith("a ")) {
|
||||
|
@ -242,7 +244,7 @@ object MusicUtil : KoinComponent {
|
|||
}
|
||||
if (musicMediaTitle.isEmpty()) {
|
||||
""
|
||||
} else musicMediaTitle.substring(0, 1).toUpperCase()
|
||||
} else musicMediaTitle.substring(0, 1).uppercase()
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
|
@ -257,10 +259,21 @@ object MusicUtil : KoinComponent {
|
|||
fun getSongFileUri(songId: Long): Uri {
|
||||
return ContentUris.withAppendedId(
|
||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||
songId.toLong()
|
||||
songId
|
||||
)
|
||||
}
|
||||
|
||||
fun getSongFilePath(context: Context, uri: Uri): String? {
|
||||
val projection = arrayOf(MediaStore.MediaColumns.DATA)
|
||||
return context.contentResolver.query(uri, projection, null, null, null)?.use {
|
||||
if (it.moveToFirst()) {
|
||||
it.getString(0)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getTotalDuration(songs: List<Song>): Long {
|
||||
var duration: Long = 0
|
||||
for (i in songs.indices) {
|
||||
|
@ -299,7 +312,7 @@ object MusicUtil : KoinComponent {
|
|||
if (artistName == Artist.UNKNOWN_ARTIST_DISPLAY_NAME) {
|
||||
return true
|
||||
}
|
||||
val tempName = artistName!!.trim { it <= ' ' }.toLowerCase()
|
||||
val tempName = artistName!!.trim { it <= ' ' }.lowercase()
|
||||
return tempName == "unknown" || tempName == "<unknown>"
|
||||
}
|
||||
|
||||
|
@ -442,7 +455,7 @@ object MusicUtil : KoinComponent {
|
|||
}
|
||||
}
|
||||
|
||||
fun deleteTracks(context: Context, songs: List<Song>) {
|
||||
suspend fun deleteTracks(context: Context, songs: List<Song>) {
|
||||
val projection = arrayOf(BaseColumns._ID, MediaStore.MediaColumns.DATA)
|
||||
val selection = StringBuilder()
|
||||
selection.append(BaseColumns._ID + " IN (")
|
||||
|
@ -501,13 +514,19 @@ object MusicUtil : KoinComponent {
|
|||
}
|
||||
cursor.close()
|
||||
}
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.deleted_x_songs, deletedCount),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.deleted_x_songs, deletedCount),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
} catch (ignored: SecurityException) {
|
||||
}
|
||||
}
|
||||
|
||||
fun songByGenre(genreId: Long): Song {
|
||||
return repository.getSongByGenre(genreId)
|
||||
}
|
||||
}
|
|
@ -21,8 +21,12 @@ import android.content.Context;
|
|||
import android.content.Intent;
|
||||
import android.media.audiofx.AudioEffect;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import code.name.monkey.retromusic.R;
|
||||
import code.name.monkey.retromusic.activities.DriveModeActivity;
|
||||
import code.name.monkey.retromusic.activities.LicenseActivity;
|
||||
|
@ -34,7 +38,6 @@ import code.name.monkey.retromusic.activities.UserInfoActivity;
|
|||
import code.name.monkey.retromusic.activities.WhatsNewActivity;
|
||||
import code.name.monkey.retromusic.activities.bugreport.BugReportActivity;
|
||||
import code.name.monkey.retromusic.helper.MusicPlayerRemote;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class NavigationUtil {
|
||||
|
||||
|
|
|
@ -0,0 +1,347 @@
|
|||
/*
|
||||
* 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.Manifest.permission.BIND_NOTIFICATION_LISTENER_SERVICE
|
||||
import android.Manifest.permission.MEDIA_CONTENT_CONTROL
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.XmlResourceParser
|
||||
import android.os.Process
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import androidx.annotation.XmlRes
|
||||
import androidx.media.MediaBrowserServiceCompat
|
||||
import code.name.monkey.retromusic.BuildConfig
|
||||
import org.xmlpull.v1.XmlPullParserException
|
||||
import java.io.IOException
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
|
||||
/**
|
||||
* Validates that the calling package is authorized to browse a [MediaBrowserServiceCompat].
|
||||
*
|
||||
* The list of allowed signing certificates and their corresponding package names is defined in
|
||||
* res/xml/allowed_media_browser_callers.xml.
|
||||
*
|
||||
* If you want to add a new caller to allowed_media_browser_callers.xml and you don't know
|
||||
* its signature, this class will print to logcat (INFO level) a message with the proper
|
||||
* xml tags to add to allow the caller.
|
||||
*
|
||||
* For more information, see res/xml/allowed_media_browser_callers.xml.
|
||||
*/
|
||||
class PackageValidator(
|
||||
context: Context,
|
||||
@XmlRes xmlResId: Int
|
||||
) {
|
||||
private val context: Context
|
||||
private val packageManager: PackageManager
|
||||
|
||||
private val certificateWhitelist: Map<String, KnownCallerInfo>
|
||||
private val platformSignature: String
|
||||
|
||||
private val callerChecked = mutableMapOf<String, Pair<Int, Boolean>>()
|
||||
|
||||
init {
|
||||
val parser = context.resources.getXml(xmlResId)
|
||||
this.context = context.applicationContext
|
||||
this.packageManager = this.context.packageManager
|
||||
|
||||
certificateWhitelist = buildCertificateWhitelist(parser)
|
||||
platformSignature = getSystemSignature()
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the caller attempting to connect to a [MediaBrowserServiceCompat] is known.
|
||||
* See [MusicService.onGetRoot] for where this is utilized.
|
||||
*
|
||||
* @param callingPackage The package name of the caller.
|
||||
* @param callingUid The user id of the caller.
|
||||
* @return `true` if the caller is known, `false` otherwise.
|
||||
*/
|
||||
fun isKnownCaller(callingPackage: String, callingUid: Int): Boolean {
|
||||
// If the caller has already been checked, return the previous result here.
|
||||
val (checkedUid, checkResult) = callerChecked[callingPackage] ?: Pair(0, false)
|
||||
if (checkedUid == callingUid) {
|
||||
return checkResult
|
||||
}
|
||||
|
||||
/**
|
||||
* Because some of these checks can be slow, we save the results in [callerChecked] after
|
||||
* this code is run.
|
||||
*
|
||||
* In particular, there's little reason to recompute the calling package's certificate
|
||||
* signature (SHA-256) each call.
|
||||
*
|
||||
* This is safe to do as we know the UID matches the package's UID (from the check above),
|
||||
* and app UIDs are set at install time. Additionally, a package name + UID is guaranteed to
|
||||
* be constant until a reboot. (After a reboot then a previously assigned UID could be
|
||||
* reassigned.)
|
||||
*/
|
||||
|
||||
// Build the caller info for the rest of the checks here.
|
||||
val callerPackageInfo = buildCallerInfo(callingPackage)
|
||||
?: throw IllegalStateException("Caller wasn't found in the system?")
|
||||
|
||||
// Verify that things aren't ... broken. (This test should always pass.)
|
||||
if (callerPackageInfo.uid != callingUid) {
|
||||
throw IllegalStateException("Caller's package UID doesn't match caller's actual UID?")
|
||||
}
|
||||
|
||||
val callerSignature = callerPackageInfo.signature
|
||||
val isPackageInWhitelist = certificateWhitelist[callingPackage]?.signatures?.first {
|
||||
it.signature == callerSignature
|
||||
} != null
|
||||
|
||||
val isCallerKnown = when {
|
||||
// If it's our own app making the call, allow it.
|
||||
callingUid == Process.myUid() -> true
|
||||
// If it's one of the apps on the whitelist, allow it.
|
||||
isPackageInWhitelist -> true
|
||||
// If the system is making the call, allow it.
|
||||
callingUid == Process.SYSTEM_UID -> true
|
||||
// If the app was signed by the same certificate as the platform itself, also allow it.
|
||||
callerSignature == platformSignature -> true
|
||||
/**
|
||||
* [MEDIA_CONTENT_CONTROL] permission is only available to system applications, and
|
||||
* while it isn't required to allow these apps to connect to a
|
||||
* [MediaBrowserServiceCompat], allowing this ensures optimal compatability with apps
|
||||
* such as Android TV and the Google Assistant.
|
||||
*/
|
||||
callerPackageInfo.permissions.contains(MEDIA_CONTENT_CONTROL) -> true
|
||||
/**
|
||||
* This last permission can be specifically granted to apps, and, in addition to
|
||||
* allowing them to retrieve notifications, it also allows them to connect to an
|
||||
* active [MediaSessionCompat].
|
||||
* As with the above, it's not required to allow apps holding this permission to
|
||||
* connect to your [MediaBrowserServiceCompat], but it does allow easy comparability
|
||||
* with apps such as Wear OS.
|
||||
*/
|
||||
callerPackageInfo.permissions.contains(BIND_NOTIFICATION_LISTENER_SERVICE) -> true
|
||||
// If none of the pervious checks succeeded, then the caller is unrecognized.
|
||||
else -> false
|
||||
}
|
||||
|
||||
if (!isCallerKnown) {
|
||||
logUnknownCaller(callerPackageInfo)
|
||||
}
|
||||
|
||||
// Save our work for next time.
|
||||
callerChecked[callingPackage] = Pair(callingUid, isCallerKnown)
|
||||
return isCallerKnown
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs an info level message with details of how to add a caller to the allowed callers list
|
||||
* when the app is debuggable.
|
||||
*/
|
||||
private fun logUnknownCaller(callerPackageInfo: CallerPackageInfo) {
|
||||
if (BuildConfig.DEBUG && callerPackageInfo.signature != null) {
|
||||
Log.i(TAG, "PackageValidator call" + callerPackageInfo.name + callerPackageInfo.packageName + callerPackageInfo.signature)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a [CallerPackageInfo] for a given package that can be used for all the
|
||||
* various checks that are performed before allowing an app to connect to a
|
||||
* [MediaBrowserServiceCompat].
|
||||
*/
|
||||
private fun buildCallerInfo(callingPackage: String): CallerPackageInfo? {
|
||||
val packageInfo = getPackageInfo(callingPackage) ?: return null
|
||||
|
||||
val appName = packageInfo.applicationInfo.loadLabel(packageManager).toString()
|
||||
val uid = packageInfo.applicationInfo.uid
|
||||
val signature = getSignature(packageInfo)
|
||||
|
||||
val requestedPermissions = packageInfo.requestedPermissions
|
||||
val permissionFlags = packageInfo.requestedPermissionsFlags
|
||||
val activePermissions = mutableSetOf<String>()
|
||||
requestedPermissions?.forEachIndexed { index, permission ->
|
||||
if (permissionFlags[index] and REQUESTED_PERMISSION_GRANTED != 0) {
|
||||
activePermissions += permission
|
||||
}
|
||||
}
|
||||
|
||||
return CallerPackageInfo(appName, callingPackage, uid, signature, activePermissions.toSet())
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up the [PackageInfo] for a package name.
|
||||
* This requests both the signatures (for checking if an app is on the whitelist) and
|
||||
* the app's permissions, which allow for more flexibility in the whitelist.
|
||||
*
|
||||
* @return [PackageInfo] for the package name or null if it's not found.
|
||||
*/
|
||||
@SuppressLint("PackageManagerGetSignatures")
|
||||
private fun getPackageInfo(callingPackage: String): PackageInfo? =
|
||||
packageManager.getPackageInfo(callingPackage,
|
||||
PackageManager.GET_SIGNATURES or PackageManager.GET_PERMISSIONS)
|
||||
|
||||
/**
|
||||
* Gets the signature of a given package's [PackageInfo].
|
||||
*
|
||||
* The "signature" is a SHA-256 hash of the public key of the signing certificate used by
|
||||
* the app.
|
||||
*
|
||||
* If the app is not found, or if the app does not have exactly one signature, this method
|
||||
* returns `null` as the signature.
|
||||
*/
|
||||
private fun getSignature(packageInfo: PackageInfo): String? {
|
||||
// Security best practices dictate that an app should be signed with exactly one (1)
|
||||
// signature. Because of this, if there are multiple signatures, reject it.
|
||||
if (packageInfo.signatures == null || packageInfo.signatures.size != 1) {
|
||||
return null
|
||||
} else {
|
||||
val certificate = packageInfo.signatures[0].toByteArray()
|
||||
return getSignatureSha256(certificate)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildCertificateWhitelist(parser: XmlResourceParser): Map<String, KnownCallerInfo> {
|
||||
|
||||
val certificateWhitelist = LinkedHashMap<String, KnownCallerInfo>()
|
||||
try {
|
||||
var eventType = parser.next()
|
||||
while (eventType != XmlResourceParser.END_DOCUMENT) {
|
||||
if (eventType == XmlResourceParser.START_TAG) {
|
||||
val callerInfo = when (parser.name) {
|
||||
"signing_certificate" -> parseV1Tag(parser)
|
||||
"signature" -> parseV2Tag(parser)
|
||||
else -> null
|
||||
}
|
||||
|
||||
callerInfo?.let { info ->
|
||||
val packageName = info.packageName
|
||||
val existingCallerInfo = certificateWhitelist[packageName]
|
||||
if (existingCallerInfo != null) {
|
||||
existingCallerInfo.signatures += callerInfo.signatures
|
||||
} else {
|
||||
certificateWhitelist[packageName] = callerInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eventType = parser.next()
|
||||
}
|
||||
} catch (xmlException: XmlPullParserException) {
|
||||
Log.e(TAG, "Could not read allowed callers from XML.", xmlException)
|
||||
} catch (ioException: IOException) {
|
||||
Log.e(TAG, "Could not read allowed callers from XML.", ioException)
|
||||
}
|
||||
|
||||
return certificateWhitelist
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a v1 format tag. See allowed_media_browser_callers.xml for more details.
|
||||
*/
|
||||
private fun parseV1Tag(parser: XmlResourceParser): KnownCallerInfo {
|
||||
val name = parser.getAttributeValue(null, "name")
|
||||
val packageName = parser.getAttributeValue(null, "package")
|
||||
val isRelease = parser.getAttributeBooleanValue(null, "release", false)
|
||||
val certificate = parser.nextText().replace(WHITESPACE_REGEX, "")
|
||||
val signature = getSignatureSha256(certificate)
|
||||
|
||||
val callerSignature = KnownSignature(signature, isRelease)
|
||||
return KnownCallerInfo(name, packageName, mutableSetOf(callerSignature))
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a v2 format tag. See allowed_media_browser_callers.xml for more details.
|
||||
*/
|
||||
private fun parseV2Tag(parser: XmlResourceParser): KnownCallerInfo {
|
||||
val name = parser.getAttributeValue(null, "name")
|
||||
val packageName = parser.getAttributeValue(null, "package")
|
||||
|
||||
val callerSignatures = mutableSetOf<KnownSignature>()
|
||||
var eventType = parser.next()
|
||||
while (eventType != XmlResourceParser.END_TAG) {
|
||||
val isRelease = parser.getAttributeBooleanValue(null, "release", false)
|
||||
val signature = parser.nextText().replace(WHITESPACE_REGEX, "").toLowerCase()
|
||||
callerSignatures += KnownSignature(signature, isRelease)
|
||||
|
||||
eventType = parser.next()
|
||||
}
|
||||
|
||||
return KnownCallerInfo(name, packageName, callerSignatures)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the Android platform signing key signature. This key is never null.
|
||||
*/
|
||||
private fun getSystemSignature(): String =
|
||||
getPackageInfo(ANDROID_PLATFORM)?.let { platformInfo ->
|
||||
getSignature(platformInfo)
|
||||
} ?: throw IllegalStateException("Platform signature not found")
|
||||
|
||||
/**
|
||||
* Creates a SHA-256 signature given a Base64 encoded certificate.
|
||||
*/
|
||||
private fun getSignatureSha256(certificate: String): String {
|
||||
return getSignatureSha256(Base64.decode(certificate, Base64.DEFAULT))
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a SHA-256 signature given a certificate byte array.
|
||||
*/
|
||||
private fun getSignatureSha256(certificate: ByteArray): String {
|
||||
val md: MessageDigest
|
||||
try {
|
||||
md = MessageDigest.getInstance("SHA256")
|
||||
} catch (noSuchAlgorithmException: NoSuchAlgorithmException) {
|
||||
Log.e(TAG, "No such algorithm: $noSuchAlgorithmException")
|
||||
throw RuntimeException("Could not find SHA256 hash algorithm", noSuchAlgorithmException)
|
||||
}
|
||||
md.update(certificate)
|
||||
|
||||
// This code takes the byte array generated by `md.digest()` and joins each of the bytes
|
||||
// to a string, applying the string format `%02x` on each digit before it's appended, with
|
||||
// a colon (':') between each of the items.
|
||||
// For example: input=[0,2,4,6,8,10,12], output="00:02:04:06:08:0a:0c"
|
||||
return md.digest().joinToString(":") { String.format("%02x", it) }
|
||||
}
|
||||
|
||||
private data class KnownCallerInfo(
|
||||
internal val name: String,
|
||||
internal val packageName: String,
|
||||
internal val signatures: MutableSet<KnownSignature>
|
||||
)
|
||||
|
||||
private data class KnownSignature(
|
||||
internal val signature: String,
|
||||
internal val release: Boolean
|
||||
)
|
||||
|
||||
/**
|
||||
* Convenience class to hold all of the information about an app that's being checked
|
||||
* to see if it's a known caller.
|
||||
*/
|
||||
private data class CallerPackageInfo(
|
||||
internal val name: String,
|
||||
internal val packageName: String,
|
||||
internal val uid: Int,
|
||||
internal val signature: String?,
|
||||
internal val permissions: Set<String>
|
||||
)
|
||||
}
|
||||
|
||||
private const val TAG = "PackageValidator"
|
||||
private const val ANDROID_PLATFORM = "android"
|
||||
private val WHITESPACE_REGEX = "\\s|\\n".toRegex()
|
|
@ -25,18 +25,21 @@ import android.os.Environment;
|
|||
import android.provider.BaseColumns;
|
||||
import android.provider.MediaStore;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import code.name.monkey.retromusic.R;
|
||||
import code.name.monkey.retromusic.db.PlaylistWithSongs;
|
||||
import code.name.monkey.retromusic.helper.M3UWriter;
|
||||
import code.name.monkey.retromusic.model.Playlist;
|
||||
import code.name.monkey.retromusic.model.PlaylistSong;
|
||||
import code.name.monkey.retromusic.model.Song;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class PlaylistsUtil {
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ import code.name.monkey.retromusic.helper.SortOrder.*
|
|||
import code.name.monkey.retromusic.model.CategoryInfo
|
||||
import code.name.monkey.retromusic.transform.*
|
||||
import code.name.monkey.retromusic.util.theme.ThemeMode
|
||||
import com.google.android.material.bottomnavigation.LabelVisibilityMode
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonSyntaxException
|
||||
import com.google.gson.reflect.TypeToken
|
||||
|
@ -218,11 +218,10 @@ object PreferenceUtil {
|
|||
TOGGLE_ADD_CONTROLS, false
|
||||
)
|
||||
|
||||
val typeHomeBanner
|
||||
get() = sharedPreferences.getStringOrDefault(
|
||||
TYPE_HOME_BANNER, "0"
|
||||
).toInt()
|
||||
|
||||
val isHomeBanner
|
||||
get() = sharedPreferences.getBoolean(
|
||||
TOGGLE_HOME_BANNER, false
|
||||
)
|
||||
var isClassicNotification
|
||||
get() = sharedPreferences.getBoolean(CLASSIC_NOTIFICATION, false)
|
||||
set(value) = sharedPreferences.edit { putBoolean(CLASSIC_NOTIFICATION, value) }
|
||||
|
@ -416,10 +415,10 @@ object PreferenceUtil {
|
|||
).toInt()
|
||||
val typedArray = App.getContext()
|
||||
.resources.obtainTypedArray(R.array.pref_home_grid_style_layout)
|
||||
val layoutRes = typedArray.getResourceId(position, 4)
|
||||
val layoutRes = typedArray.getResourceId(position, 0)
|
||||
typedArray.recycle()
|
||||
return if (layoutRes == 0) {
|
||||
R.layout.item_artist
|
||||
R.layout.item_image
|
||||
} else layoutRes
|
||||
}
|
||||
|
||||
|
@ -428,11 +427,11 @@ object PreferenceUtil {
|
|||
return when (sharedPreferences.getStringOrDefault(
|
||||
TAB_TEXT_MODE, "1"
|
||||
).toInt()) {
|
||||
1 -> LabelVisibilityMode.LABEL_VISIBILITY_LABELED
|
||||
0 -> LabelVisibilityMode.LABEL_VISIBILITY_AUTO
|
||||
2 -> LabelVisibilityMode.LABEL_VISIBILITY_SELECTED
|
||||
3 -> LabelVisibilityMode.LABEL_VISIBILITY_UNLABELED
|
||||
else -> LabelVisibilityMode.LABEL_VISIBILITY_LABELED
|
||||
1 -> BottomNavigationView.LABEL_VISIBILITY_LABELED
|
||||
0 -> BottomNavigationView.LABEL_VISIBILITY_AUTO
|
||||
2 -> BottomNavigationView.LABEL_VISIBILITY_SELECTED
|
||||
3 -> BottomNavigationView.LABEL_VISIBILITY_UNLABELED
|
||||
else -> BottomNavigationView.LABEL_VISIBILITY_LABELED
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -556,8 +555,7 @@ object PreferenceUtil {
|
|||
|
||||
fun getRecentlyPlayedCutoffTimeMillis(): Long {
|
||||
val calendarUtil = CalendarUtil()
|
||||
val interval: Long
|
||||
interval = when (sharedPreferences.getString(RECENTLY_PLAYED_CUTOFF, "")) {
|
||||
val interval: Long = when (sharedPreferences.getString(RECENTLY_PLAYED_CUTOFF, "")) {
|
||||
"today" -> calendarUtil.elapsedToday
|
||||
"this_week" -> calendarUtil.elapsedWeek
|
||||
"past_seven_days" -> calendarUtil.getElapsedDays(7)
|
||||
|
@ -583,4 +581,35 @@ object PreferenceUtil {
|
|||
}
|
||||
return (System.currentTimeMillis() - interval) / 1000
|
||||
}
|
||||
|
||||
val homeSuggestions: Boolean
|
||||
get() = sharedPreferences.getBoolean(
|
||||
TOGGLE_SUGGESTIONS,
|
||||
true
|
||||
)
|
||||
|
||||
var audioFadeDuration
|
||||
get() = sharedPreferences
|
||||
.getInt(AUDIO_FADE_DURATION, 0)
|
||||
set(value) = sharedPreferences.edit { putInt(AUDIO_FADE_DURATION, value) }
|
||||
|
||||
var showLyrics: Boolean
|
||||
get() = sharedPreferences.getBoolean(SHOW_LYRICS, false)
|
||||
set(value) = sharedPreferences.edit { putBoolean(SHOW_LYRICS, value) }
|
||||
|
||||
val rememberLastTab: Boolean
|
||||
get() = sharedPreferences.getBoolean(REMEMBER_LAST_TAB, true)
|
||||
|
||||
var lastTab: Int
|
||||
get() = sharedPreferences
|
||||
.getInt(LAST_USED_TAB, 0)
|
||||
set(value) = sharedPreferences.edit { putInt(LAST_USED_TAB, value) }
|
||||
|
||||
val isWhiteList: Boolean
|
||||
get() = sharedPreferences.getBoolean(WHITELIST_MUSIC, false)
|
||||
|
||||
var crossFadeDuration
|
||||
get() = sharedPreferences
|
||||
.getInt(CROSS_FADE_DURATION, 0)
|
||||
set(value) = sharedPreferences.edit { putInt(CROSS_FADE_DURATION, value) }
|
||||
}
|
||||
|
|
|
@ -18,16 +18,19 @@ import android.content.Context;
|
|||
import android.content.res.TypedArray;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.palette.graphics.Palette;
|
||||
import code.name.monkey.appthemehelper.util.ColorUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import code.name.monkey.appthemehelper.util.ColorUtil;
|
||||
|
||||
public class RetroColorUtil {
|
||||
public static int desaturateColor(int color, float ratio) {
|
||||
float[] hsv = new float[3];
|
||||
|
|
|
@ -35,14 +35,21 @@ import android.view.View;
|
|||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import code.name.monkey.appthemehelper.util.TintHelper;
|
||||
import code.name.monkey.retromusic.App;
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
public class RetroUtil {
|
||||
|
||||
|
@ -203,8 +210,43 @@ public class RetroUtil {
|
|||
|
||||
public static void setAllowDrawUnderStatusBar(@NonNull Window window) {
|
||||
window
|
||||
.getDecorView()
|
||||
.setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
|
||||
.getDecorView()
|
||||
.setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
|
||||
}
|
||||
|
||||
public static String getIpAddress(boolean useIPv4) {
|
||||
try {
|
||||
List<NetworkInterface> interfaces =
|
||||
Collections.list(NetworkInterface.getNetworkInterfaces());
|
||||
for (NetworkInterface intf : interfaces) {
|
||||
List<InetAddress> addrs = Collections.list(intf.getInetAddresses());
|
||||
for (InetAddress addr : addrs) {
|
||||
if (!addr.isLoopbackAddress()) {
|
||||
String sAddr = addr.getHostAddress();
|
||||
//boolean isIPv4 = InetAddressUtils.isIPv4Address(sAddr);
|
||||
boolean isIPv4 = sAddr.indexOf(':') < 0;
|
||||
if (useIPv4) {
|
||||
if (isIPv4) return sAddr;
|
||||
} else {
|
||||
if (!isIPv4) {
|
||||
int delim = sAddr.indexOf('%'); // drop ip6 zone suffix
|
||||
if (delim < 0) {
|
||||
return sAddr.toUpperCase();
|
||||
} else {
|
||||
return sAddr.substring(
|
||||
0,
|
||||
delim
|
||||
).toUpperCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ import android.provider.BaseColumns
|
|||
import android.provider.MediaStore
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import code.name.monkey.retromusic.R
|
||||
import code.name.monkey.retromusic.model.Song
|
||||
import code.name.monkey.retromusic.util.MusicUtil.getSongFileUri
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.content.res.ColorStateList;
|
|||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.util.StateSet;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.graphics.ColorUtils;
|
||||
|
|
|
@ -26,20 +26,24 @@ import android.provider.DocumentsContract;
|
|||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import code.name.monkey.retromusic.R;
|
||||
import code.name.monkey.retromusic.model.Song;
|
||||
|
||||
import org.jaudiotagger.audio.AudioFile;
|
||||
import org.jaudiotagger.audio.exceptions.CannotWriteException;
|
||||
import org.jaudiotagger.audio.generic.Utils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import org.jaudiotagger.audio.AudioFile;
|
||||
import org.jaudiotagger.audio.exceptions.CannotWriteException;
|
||||
import org.jaudiotagger.audio.generic.Utils;
|
||||
|
||||
import code.name.monkey.retromusic.R;
|
||||
import code.name.monkey.retromusic.model.Song;
|
||||
|
||||
public class SAFUtil {
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
package code.name.monkey.retromusic.util;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
|
|
@ -23,7 +23,6 @@ import android.widget.ProgressBar
|
|||
import android.widget.SeekBar
|
||||
import androidx.core.graphics.BlendModeColorFilterCompat
|
||||
import androidx.core.graphics.BlendModeCompat.SRC_IN
|
||||
import androidx.core.view.ViewCompat
|
||||
import code.name.monkey.appthemehelper.util.ATHUtil
|
||||
import code.name.monkey.appthemehelper.util.ColorUtil
|
||||
import code.name.monkey.appthemehelper.util.MaterialValueHelper
|
||||
|
@ -83,8 +82,8 @@ object ViewUtil {
|
|||
}
|
||||
|
||||
fun hitTest(v: View, x: Int, y: Int): Boolean {
|
||||
val tx = (ViewCompat.getTranslationX(v) + 0.5f).toInt()
|
||||
val ty = (ViewCompat.getTranslationY(v) + 0.5f).toInt()
|
||||
val tx = (v.translationX + 0.5f).toInt()
|
||||
val ty = (v.translationY + 0.5f).toInt()
|
||||
val left = v.left + tx
|
||||
val right = v.right + tx
|
||||
val top = v.top + ty
|
||||
|
|
|
@ -25,14 +25,17 @@ import android.graphics.Color;
|
|||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Handler;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.FloatRange;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.palette.graphics.Palette;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import code.name.monkey.appthemehelper.util.ATHUtil;
|
||||
import code.name.monkey.appthemehelper.util.ColorUtil;
|
||||
import code.name.monkey.retromusic.R;
|
||||
import java.util.List;
|
||||
|
||||
/** A class the processes media notifications and extracts the right text and background colors. */
|
||||
public class MediaNotificationProcessor {
|
||||
|
@ -75,12 +78,7 @@ public class MediaNotificationProcessor {
|
|||
private static final String TAG = "ColorPicking";
|
||||
private float[] mFilteredBackgroundHsl = null;
|
||||
private Palette.Filter mBlackWhiteFilter =
|
||||
new Palette.Filter() {
|
||||
@Override
|
||||
public boolean isAllowed(int rgb, @NonNull float[] hsl) {
|
||||
return !isWhiteOrBlack(hsl);
|
||||
}
|
||||
};
|
||||
(rgb, hsl) -> !isWhiteOrBlack(hsl);
|
||||
private boolean mIsLowPriority;
|
||||
private int backgroundColor;
|
||||
private int secondaryTextColor;
|
||||
|
@ -140,18 +138,10 @@ public class MediaNotificationProcessor {
|
|||
this.drawable = drawable;
|
||||
final Handler handler = new Handler();
|
||||
new Thread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
getMediaPalette();
|
||||
handler.post(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onPaletteLoadedListener.onPaletteLoaded(MediaNotificationProcessor.this);
|
||||
}
|
||||
});
|
||||
}
|
||||
() -> {
|
||||
getMediaPalette();
|
||||
handler.post(
|
||||
() -> onPaletteLoadedListener.onPaletteLoaded(MediaNotificationProcessor.this));
|
||||
})
|
||||
.start();
|
||||
}
|
||||
|
@ -175,44 +165,40 @@ public class MediaNotificationProcessor {
|
|||
double factor = Math.sqrt((float) RESIZE_BITMAP_AREA / area);
|
||||
width = (int) (factor * width);
|
||||
height = (int) (factor * height);
|
||||
}
|
||||
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
drawable.setBounds(0, 0, width, height);
|
||||
drawable.draw(canvas);
|
||||
|
||||
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
drawable.setBounds(0, 0, width, height);
|
||||
drawable.draw(canvas);
|
||||
|
||||
// for the background we only take the left side of the image to ensure
|
||||
// a smooth transition
|
||||
Palette.Builder paletteBuilder =
|
||||
Palette.from(bitmap)
|
||||
.setRegion(0, 0, bitmap.getWidth() / 2, bitmap.getHeight())
|
||||
.clearFilters() // we want all colors, red / white / black ones too!
|
||||
.resizeBitmapArea(RESIZE_BITMAP_AREA);
|
||||
Palette palette = paletteBuilder.generate();
|
||||
backgroundColor = findBackgroundColorAndFilter(drawable);
|
||||
// we want most of the full region again, slightly shifted to the right
|
||||
float textColorStartWidthFraction = 0.4f;
|
||||
paletteBuilder.setRegion(
|
||||
(int) (bitmap.getWidth() * textColorStartWidthFraction),
|
||||
0,
|
||||
bitmap.getWidth(),
|
||||
bitmap.getHeight());
|
||||
if (mFilteredBackgroundHsl != null) {
|
||||
paletteBuilder.addFilter(
|
||||
new Palette.Filter() {
|
||||
@Override
|
||||
public boolean isAllowed(int rgb, @NonNull float[] hsl) {
|
||||
// for the background we only take the left side of the image to ensure
|
||||
// a smooth transition
|
||||
Palette.Builder paletteBuilder =
|
||||
Palette.from(bitmap)
|
||||
.setRegion(0, 0, bitmap.getWidth() / 2, bitmap.getHeight())
|
||||
.clearFilters() // we want all colors, red / white / black ones too!
|
||||
.resizeBitmapArea(RESIZE_BITMAP_AREA);
|
||||
Palette palette;
|
||||
backgroundColor = findBackgroundColorAndFilter(drawable);
|
||||
// we want most of the full region again, slightly shifted to the right
|
||||
float textColorStartWidthFraction = 0.4f;
|
||||
paletteBuilder.setRegion(
|
||||
(int) (bitmap.getWidth() * textColorStartWidthFraction),
|
||||
0,
|
||||
bitmap.getWidth(),
|
||||
bitmap.getHeight());
|
||||
if (mFilteredBackgroundHsl != null) {
|
||||
paletteBuilder.addFilter(
|
||||
(rgb, hsl) -> {
|
||||
// at least 10 degrees hue difference
|
||||
float diff = Math.abs(hsl[0] - mFilteredBackgroundHsl[0]);
|
||||
return diff > 10 && diff < 350;
|
||||
}
|
||||
});
|
||||
}
|
||||
paletteBuilder.addFilter(mBlackWhiteFilter);
|
||||
palette = paletteBuilder.generate();
|
||||
int foregroundColor = selectForegroundColor(backgroundColor, palette);
|
||||
ensureColors(backgroundColor, foregroundColor);
|
||||
});
|
||||
}
|
||||
paletteBuilder.addFilter(mBlackWhiteFilter);
|
||||
palette = paletteBuilder.generate();
|
||||
int foregroundColor = selectForegroundColor(backgroundColor, palette);
|
||||
ensureColors(backgroundColor, foregroundColor);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,14 +28,17 @@ import android.text.style.ForegroundColorSpan;
|
|||
import android.text.style.TextAppearanceSpan;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.FloatRange;
|
||||
import androidx.annotation.IntRange;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import code.name.monkey.retromusic.R;
|
||||
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import code.name.monkey.retromusic.R;
|
||||
|
||||
/**
|
||||
* Helper class to process legacy (Holo) notifications to make them look like material
|
||||
* notifications.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue