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:
Prathamesh More 2021-09-09 00:00:20 +05:30
parent fc42767031
commit bce6dbfa27
421 changed files with 13285 additions and 5757 deletions

View file

@ -60,7 +60,7 @@ object AppRater {
}
}
editor.commit()
editor.apply()
}
private fun showPlayStoreReviewDialog(context: Activity, editor: SharedPreferences.Editor) {

View file

@ -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)));
}
}

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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")

View file

@ -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
}

View file

@ -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() {}

View file

@ -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
*/

View file

@ -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 "";
}
}

View file

@ -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,

View file

@ -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)
}
}

View file

@ -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 {

View file

@ -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()

View file

@ -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 {

View file

@ -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) }
}

View file

@ -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];

View file

@ -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 "";
}
}

View file

@ -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

View file

@ -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;

View file

@ -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 {

View file

@ -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;

View file

@ -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

View file

@ -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);
}
}

View file

@ -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.