Reinstate package name

Signed-off-by: Muntashir Al-Islam <muntashirakon@riseup.net>
This commit is contained in:
Muntashir Al-Islam 2023-03-15 00:26:47 +06:00
parent 9971c25649
commit 2845945763
478 changed files with 2648 additions and 2613 deletions

View file

@ -0,0 +1,50 @@
/*
* 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.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import com.bumptech.glide.signature.ObjectKey
/** @author Karim Abou Zeid (kabouzeid)
*/
class ArtistSignatureUtil private constructor(context: Context) {
private val mPreferences: SharedPreferences =
context.getSharedPreferences(ARTIST_SIGNATURE_PREFS, Context.MODE_PRIVATE)
fun updateArtistSignature(artistName: String?) {
mPreferences.edit { putLong(artistName, System.currentTimeMillis()) }
}
private fun getArtistSignatureRaw(artistName: String?): Long {
return mPreferences.getLong(artistName, 0)
}
fun getArtistSignature(artistName: String?): ObjectKey {
return ObjectKey(getArtistSignatureRaw(artistName).toString())
}
companion object {
private const val ARTIST_SIGNATURE_PREFS = "artist_signatures"
private var INSTANCE: ArtistSignatureUtil? = null
fun getInstance(context: Context): ArtistSignatureUtil {
if (INSTANCE == null) {
INSTANCE = ArtistSignatureUtil(context.applicationContext)
}
return INSTANCE!!
}
}
}

View file

@ -0,0 +1,48 @@
package code.name.monkey.retromusic.util
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.glide.GlideApp
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.util.MergedImageUtils.joinImages
import code.name.monkey.retromusic.util.MusicUtil.getMediaStoreAlbumCoverUri
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
object AutoGeneratedPlaylistBitmap {
fun getBitmap(
context: Context, songPlaylist: List<Song>?
): Bitmap? {
if (songPlaylist == null || songPlaylist.isEmpty()) return getDefaultBitmap(context)
if (songPlaylist.size == 1) return getBitmapWithAlbumId(context, songPlaylist[0].albumId)
val albumID: MutableList<Long> = ArrayList()
for (song in songPlaylist) {
if (!albumID.contains(song.albumId)) albumID.add(song.albumId)
}
val art: MutableList<Bitmap> = ArrayList()
for (id in albumID) {
val bitmap = getBitmapWithAlbumId(context, id)
if (bitmap != null) art.add(bitmap)
if (art.size == 9) break
}
return joinImages(art)
}
private fun getBitmapWithAlbumId(context: Context, id: Long): Bitmap? {
return try {
GlideApp.with(context)
.asBitmap()
.transform(RoundedCorners(20))
.load(getMediaStoreAlbumCoverUri(id))
.submit(200, 200)
.get()
} catch (e: Exception) {
null
}
}
private fun getDefaultBitmap(context: Context): Bitmap {
return BitmapFactory.decodeResource(context.resources, R.drawable.default_album_art)
}
}

View file

@ -0,0 +1,28 @@
package code.name.monkey.retromusic.util
import android.content.Context
import android.content.Intent
import androidx.core.content.FileProvider
import code.name.monkey.retromusic.extensions.showToast
import java.io.File
object BackupUtil {
fun createShareFileIntent(file: File, context: Context): Intent? {
return try {
Intent().setAction(Intent.ACTION_SEND).putExtra(
Intent.EXTRA_STREAM,
FileProvider.getUriForFile(
context,
context.applicationContext.packageName,
file
)
).addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION).setType("*/*")
} catch (e: IllegalArgumentException) {
e.printStackTrace()
context.showToast(
"Could not share this file."
)
Intent()
}
}
}

View file

@ -0,0 +1,125 @@
/*
* 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 java.util.*
/** @author Eugene Cheung (arkon)
*/
class CalendarUtil {
private val calendar = Calendar.getInstance()// Time elapsed so far today
/**
* Returns the time elapsed so far today in milliseconds.
*
* @return Time elapsed today in milliseconds.
*/
val elapsedToday: Long
get() =// Time elapsed so far today
(calendar[Calendar.HOUR_OF_DAY] * 60 + calendar[Calendar.MINUTE]) * MS_PER_MINUTE + calendar[Calendar.SECOND] * 1000 + calendar[Calendar.MILLISECOND]// Today + days passed this week
/**
* Returns the time elapsed so far this week in milliseconds.
*
* @return Time elapsed this week in milliseconds.
*/
val elapsedWeek: Long
get() {
// Today + days passed this week
var elapsed = elapsedToday
val passedWeekdays = calendar[Calendar.DAY_OF_WEEK] - 1 - calendar.firstDayOfWeek
if (passedWeekdays > 0) {
elapsed += passedWeekdays * MS_PER_DAY
}
return elapsed
}// Today + rest of this month
/**
* Returns the time elapsed so far this month in milliseconds.
*
* @return Time elapsed this month in milliseconds.
*/
val elapsedMonth: Long
get() =// Today + rest of this month
elapsedToday + (calendar[Calendar.DAY_OF_MONTH] - 1) * MS_PER_DAY
/**
* Returns the time elapsed so far this month and the last numMonths months in milliseconds.
*
* @param numMonths Additional number of months prior to the current month to calculate.
* @return Time elapsed this month and the last numMonths months in milliseconds.
*/
fun getElapsedMonths(numMonths: Int): Long {
// Today + rest of this month
var elapsed = elapsedMonth
// Previous numMonths months
var month = calendar[Calendar.MONTH]
var year = calendar[Calendar.YEAR]
for (i in 0 until numMonths) {
month--
if (month < Calendar.JANUARY) {
month = Calendar.DECEMBER
year--
}
elapsed += getDaysInMonth(month) * MS_PER_DAY
}
return elapsed
}// Today + rest of this month + previous months until January
/**
* Returns the time elapsed so far this year in milliseconds.
*
* @return Time elapsed this year in milliseconds.
*/
val elapsedYear: Long
get() {
// Today + rest of this month + previous months until January
var elapsed = elapsedMonth
var month = calendar[Calendar.MONTH] - 1
while (month > Calendar.JANUARY) {
elapsed += getDaysInMonth(month) * MS_PER_DAY
month--
}
return elapsed
}
/**
* Gets the number of days for the given month in the given year.
*
* @param month The month (1 - 12).
* @return The days in that month/year.
*/
private fun getDaysInMonth(month: Int): Int {
val monthCal: Calendar = GregorianCalendar(calendar[Calendar.YEAR], month, 1)
return monthCal.getActualMaximum(Calendar.DAY_OF_MONTH)
}
/**
* Returns the time elapsed so far last N days in milliseconds.
*
* @return Time elapsed since N days in milliseconds.
*/
fun getElapsedDays(numDays: Int): Long {
var elapsed = elapsedToday
elapsed += numDays * MS_PER_DAY
return elapsed
}
companion object {
private const val MS_PER_MINUTE = (60 * 1000).toLong()
private const val MS_PER_DAY = 24 * 60 * MS_PER_MINUTE
}
}

View file

@ -0,0 +1,57 @@
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;
public class ColorUtil {
@Nullable
public static Palette generatePalette(Bitmap bitmap) {
if (bitmap == null) return null;
return Palette.from(bitmap).generate();
}
@ColorInt
public static int getColor(@Nullable Palette palette, int fallback) {
if (palette != null) {
if (palette.getVibrantSwatch() != null) {
return palette.getVibrantSwatch().getRgb();
} else if (palette.getMutedSwatch() != null) {
return palette.getMutedSwatch().getRgb();
} else if (palette.getDarkVibrantSwatch() != null) {
return palette.getDarkVibrantSwatch().getRgb();
} else if (palette.getDarkMutedSwatch() != null) {
return palette.getDarkMutedSwatch().getRgb();
} else if (palette.getLightVibrantSwatch() != null) {
return palette.getLightVibrantSwatch().getRgb();
} else if (palette.getLightMutedSwatch() != null) {
return palette.getLightMutedSwatch().getRgb();
} else if (!palette.getSwatches().isEmpty()) {
return Collections.max(palette.getSwatches(), SwatchComparator.getInstance()).getRgb();
}
}
return fallback;
}
private static class SwatchComparator implements Comparator<Palette.Swatch> {
private static SwatchComparator sInstance;
static SwatchComparator getInstance() {
if (sInstance == null) {
sInstance = new SwatchComparator();
}
return sInstance;
}
@Override
public int compare(Palette.Swatch lhs, Palette.Swatch rhs) {
return lhs.getPopulation() - rhs.getPopulation();
}
}
}

View file

@ -0,0 +1,142 @@
/*
* 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.content.Context
import android.content.SharedPreferences
import android.graphics.Bitmap
import android.net.Uri
import android.provider.MediaStore
import android.widget.Toast
import androidx.core.content.edit
import code.name.monkey.retromusic.App
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.extensions.showToast
import code.name.monkey.retromusic.glide.GlideApp
import code.name.monkey.retromusic.model.Artist
import com.bumptech.glide.load.engine.DiskCacheStrategy
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException
import java.util.*
class CustomArtistImageUtil private constructor(context: Context) {
private val mPreferences: SharedPreferences = context.applicationContext.getSharedPreferences(
CUSTOM_ARTIST_IMAGE_PREFS,
Context.MODE_PRIVATE
)
suspend fun setCustomArtistImage(artist: Artist, uri: Uri) {
val context = App.getContext()
withContext(IO) {
runCatching {
GlideApp.with(context)
.asBitmap()
.load(uri)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.submit()
.get()
}
.onSuccess {
saveImage(context, artist, it)
}
.onFailure {
context.showToast(R.string.error_load_failed)
}
}
}
private fun saveImage(context: Context, artist: Artist, bitmap: Bitmap) {
val dir = File(context.filesDir, FOLDER_NAME)
if (!dir.exists()) {
if (!dir.mkdirs()) { // create the folder
return
}
}
val file = File(dir, getFileName(artist))
var successful = false
try {
file.outputStream().buffered().use { bos ->
successful = ImageUtil.resizeBitmap(bitmap, 2048)
.compress(Bitmap.CompressFormat.JPEG, 100, bos)
}
} catch (e: IOException) {
context.showToast(e.toString(), Toast.LENGTH_LONG)
}
if (successful) {
mPreferences.edit { putBoolean(getFileName(artist), true) }
ArtistSignatureUtil.getInstance(context)
.updateArtistSignature(artist.name)
context.contentResolver.notifyChange(
MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI,
null
) // trigger media store changed to force artist image reload
}
}
suspend fun resetCustomArtistImage(artist: Artist) {
withContext(IO) {
mPreferences.edit { putBoolean(getFileName(artist), false) }
ArtistSignatureUtil.getInstance(App.getContext()).updateArtistSignature(artist.name)
App.getContext().contentResolver.notifyChange(
MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI,
null
) // trigger media store changed to force artist image reload
val file = getFile(artist)
if (file.exists()) {
file.delete()
}
}
}
// shared prefs saves us many IO operations
fun hasCustomArtistImage(artist: Artist): Boolean {
return mPreferences.getBoolean(getFileName(artist), false)
}
companion object {
private const val CUSTOM_ARTIST_IMAGE_PREFS = "custom_artist_image"
private const val FOLDER_NAME = "/custom_artist_images/"
private var sInstance: CustomArtistImageUtil? = null
fun getInstance(context: Context): CustomArtistImageUtil {
if (sInstance == null) {
sInstance = CustomArtistImageUtil(context.applicationContext)
}
return sInstance!!
}
fun getFileName(artist: Artist): String {
var artistName = artist.name
// replace everything that is not a letter or a number with _
artistName = artistName.replace("[^a-zA-Z0-9]".toRegex(), "_")
return String.format(Locale.US, "#%d#%s.jpeg", artist.id, artistName)
}
@JvmStatic
fun getFile(artist: Artist): File {
val dir = File(App.getContext().filesDir, FOLDER_NAME)
return File(dir, getFileName(artist))
}
}
}

View file

@ -0,0 +1,45 @@
/*
* 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.content.Context
import android.util.TypedValue
/**
* Created by hefuyi on 16/7/30.
*/
object DensityUtil {
fun getScreenHeight(context: Context): Int {
val displayMetrics = context.resources.displayMetrics
return displayMetrics.heightPixels
}
fun getScreenWidth(context: Context): Int {
val displayMetrics = context.resources.displayMetrics
return displayMetrics.widthPixels
}
private fun toDP(context: Context, value: Int): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
value.toFloat(), context.resources.displayMetrics
).toInt()
}
@JvmStatic
fun dip2px(context: Context, dpVale: Float): Int {
val scale = context.resources.displayMetrics.density
return (dpVale * scale + 0.5f).toInt()
}
}

View file

@ -0,0 +1,15 @@
package code.name.monkey.retromusic.util
import android.os.Environment
object FilePathUtil {
fun blacklistFilePaths(): List<String> {
return listOf(
getExternalStoragePublicDirectory(Environment.DIRECTORY_ALARMS),
getExternalStoragePublicDirectory(Environment.DIRECTORY_RINGTONES),
getExternalStoragePublicDirectory(Environment.DIRECTORY_NOTIFICATIONS)
).map {
FileUtil.safeGetCanonicalPath(it)
}
}
}

View file

@ -0,0 +1,334 @@
/*
* 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 static code.name.monkey.retromusic.util.FileUtilsKt.getExternalStorageDirectory;
import android.content.Context;
import android.database.Cursor;
import android.os.Environment;
import android.webkit.MimeTypeMap;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.StringTokenizer;
import code.name.monkey.retromusic.Constants;
import code.name.monkey.retromusic.adapter.Storage;
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() {}
public static byte[] readBytes(InputStream stream) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int count;
while ((count = stream.read(buffer)) != -1) {
baos.write(buffer, 0, count);
}
stream.close();
return baos.toByteArray();
}
@NonNull
public static List<Song> matchFilesWithMediaStore(
@NonNull Context context, @Nullable List<File> files) {
return new RealSongRepository(context).songs(makeSongCursor(context, files));
}
public static String safeGetCanonicalPath(File file) {
try {
return file.getCanonicalPath();
} catch (IOException e) {
e.printStackTrace();
return file.getAbsolutePath();
}
}
@Nullable
public static SortedCursor makeSongCursor(
@NonNull final Context context, @Nullable final List<File> files) {
String selection = null;
String[] paths = null;
if (files != null) {
paths = toPathArray(files);
if (files.size() > 0
&& files.size() < 999) { // 999 is the max amount Androids SQL implementation can handle.
selection =
Constants.DATA + " IN (" + makePlaceholders(files.size()) + ")";
}
}
Cursor songCursor =
new RealSongRepository(context).makeSongCursor(selection, selection == null ? null : paths, PreferenceUtil.INSTANCE.getSongSortOrder(), true);
return songCursor == null
? null
: new SortedCursor(songCursor, paths, Constants.DATA);
}
private static String makePlaceholders(int len) {
StringBuilder sb = new StringBuilder(len * 2 - 1);
sb.append("?");
for (int i = 1; i < len; i++) {
sb.append(",?");
}
return sb.toString();
}
@Nullable
private static String[] toPathArray(@Nullable List<File> files) {
if (files != null) {
String[] paths = new String[files.size()];
for (int i = 0; i < files.size(); i++) {
paths[i] = safeGetCanonicalPath(files.get(i));
}
return paths;
}
return null;
}
@NonNull
public static List<File> listFiles(@NonNull File directory, @Nullable FileFilter fileFilter) {
List<File> fileList = new LinkedList<>();
File[] found = directory.listFiles(fileFilter);
if (found != null) {
Collections.addAll(fileList, found);
}
return fileList;
}
@NonNull
public static List<File> listFilesDeep(@NonNull File directory, @Nullable FileFilter fileFilter) {
List<File> files = new LinkedList<>();
internalListFilesDeep(files, directory, fileFilter);
return files;
}
@NonNull
public static List<File> listFilesDeep(
@NonNull Collection<File> files, @Nullable FileFilter fileFilter) {
List<File> resFiles = new LinkedList<>();
for (File file : files) {
if (file.isDirectory()) {
internalListFilesDeep(resFiles, file, fileFilter);
} else if (fileFilter == null || fileFilter.accept(file)) {
resFiles.add(file);
}
}
return resFiles;
}
private static void internalListFilesDeep(
@NonNull Collection<File> files, @NonNull File directory, @Nullable FileFilter fileFilter) {
File[] found = directory.listFiles(fileFilter);
if (found != null) {
for (File file : found) {
if (file.isDirectory()) {
internalListFilesDeep(files, file, fileFilter);
} else {
files.add(file);
}
}
}
}
public static boolean fileIsMimeType(File file, String mimeType, MimeTypeMap mimeTypeMap) {
if (mimeType == null || mimeType.equals("*/*")) {
return true;
} else {
// get the file mime type
String filename = file.toURI().toString();
int dotPos = filename.lastIndexOf('.');
if (dotPos == -1) {
return false;
}
String fileExtension = filename.substring(dotPos + 1).toLowerCase();
String fileType = mimeTypeMap.getMimeTypeFromExtension(fileExtension);
if (fileType == null) {
return false;
}
// check the 'type/subtype' pattern
if (fileType.equals(mimeType)) {
return true;
}
// check the 'type/*' pattern
int mimeTypeDelimiter = mimeType.lastIndexOf('/');
if (mimeTypeDelimiter == -1) {
return false;
}
String mimeTypeMainType = mimeType.substring(0, mimeTypeDelimiter);
String mimeTypeSubtype = mimeType.substring(mimeTypeDelimiter + 1);
if (!mimeTypeSubtype.equals("*")) {
return false;
}
int fileTypeDelimiter = fileType.lastIndexOf('/');
if (fileTypeDelimiter == -1) {
return false;
}
String fileTypeMainType = fileType.substring(0, fileTypeDelimiter);
if (fileTypeMainType.equals(mimeTypeMainType)) {
return true;
}
return fileTypeMainType.equals(mimeTypeMainType);
}
}
public static String stripExtension(String str) {
if (str == null) {
return null;
}
int pos = str.lastIndexOf('.');
if (pos == -1) {
return str;
}
return str.substring(0, pos);
}
public static String readFromStream(InputStream is) throws Exception {
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
if (sb.length() > 0) {
sb.append("\n");
}
sb.append(line);
}
reader.close();
return sb.toString();
}
public static String read(File file) throws Exception {
FileInputStream fin = new FileInputStream(file);
String ret = readFromStream(fin);
fin.close();
return ret;
}
public static boolean isExternalMemoryAvailable() {
Boolean isSDPresent =
Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED);
Boolean isSDSupportedDevice = Environment.isExternalStorageRemovable();
// yes SD-card is present
// Sorry
return isSDSupportedDevice && isSDPresent;
}
public static File safeGetCanonicalFile(File file) {
try {
return file.getCanonicalFile();
} catch (IOException e) {
e.printStackTrace();
return file.getAbsoluteFile();
}
}
// https://github.com/DrKLO/Telegram/blob/ab221dafadbc17459d78d9ea3e643ae18e934b16/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlertDocumentLayout.java#L939
public static ArrayList<Storage> listRoots() {
ArrayList<Storage> storageItems = new ArrayList<>();
HashSet<String> paths = new HashSet<>();
String defaultPath = getExternalStorageDirectory().getPath();
String defaultPathState = Environment.getExternalStorageState();
if (defaultPathState.equals(Environment.MEDIA_MOUNTED) || defaultPathState.equals(Environment.MEDIA_MOUNTED_READ_ONLY)) {
Storage ext = new Storage();
if (Environment.isExternalStorageRemovable()) {
ext.title = "SD Card";
} else {
ext.title = "Internal Storage";
}
ext.file = getExternalStorageDirectory();
storageItems.add(ext);
paths.add(defaultPath);
}
BufferedReader bufferedReader = null;
try {
bufferedReader = new BufferedReader(new FileReader("/proc/mounts"));
String line;
while ((line = bufferedReader.readLine()) != null) {
if (line.contains("vfat") || line.contains("/mnt")) {
StringTokenizer tokens = new StringTokenizer(line, " ");
tokens.nextToken();
String path = tokens.nextToken();
if (paths.contains(path)) {
continue;
}
if (line.contains("/dev/block/vold")) {
if (!line.contains("/mnt/secure") && !line.contains("/mnt/asec") && !line.contains("/mnt/obb") && !line.contains("/dev/mapper") && !line.contains("tmpfs")) {
if (!new File(path).isDirectory()) {
int index = path.lastIndexOf('/');
if (index != -1) {
String newPath = "/storage/" + path.substring(index + 1);
if (new File(newPath).isDirectory()) {
path = newPath;
}
}
}
paths.add(path);
try {
Storage item = new Storage();
if (path.toLowerCase().contains("sd")) {
item.title = "SD Card";
} else {
item.title = "External Storage";
}
item.file = new File(path);
storageItems.add(item);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
return storageItems;
}
}

View file

@ -0,0 +1,76 @@
package code.name.monkey.retromusic.util
import android.content.Context
import android.net.Uri
import android.os.Environment
import android.util.Log
import java.io.File
import java.io.IOException
object FileUtils {
fun copyFileToUri(context: Context, fromFile: File, toUri: Uri) {
context.contentResolver.openOutputStream(toUri)
?.use { output ->
fromFile.inputStream().use { input ->
input.copyTo(output)
}
}
}
/**
* creates a new file in storage in app specific directory.
*
* @return the file
* @throws IOException
*/
fun createFile(
context: Context,
directoryName: String,
fileName: String,
body: String,
fileType: String
): File {
val root = createDirectory(context, directoryName)
val filePath = "$root/$fileName$fileType"
val file = File(filePath)
// create file if not exist
if (!file.exists()) {
try {
// create a new file and write text in it.
file.createNewFile()
file.writeText(body)
Log.d(FileUtils::class.java.name, "File has been created and saved")
} catch (e: IOException) {
Log.d(FileUtils::class.java.name, e.message.toString())
}
}
return file
}
/**
* creates a new directory in storage in app specific directory.
*
* @return the file
*/
private fun createDirectory(context: Context, directoryName: String): File {
val file = File(
context.getExternalFilesDir(directoryName)
.toString()
)
if (!file.exists()) {
file.mkdir()
}
return file
}
}
@Suppress("Deprecation")
fun getExternalStorageDirectory(): File {
return Environment.getExternalStorageDirectory()
}
@Suppress("Deprecation")
fun getExternalStoragePublicDirectory(type: String): File {
return Environment.getExternalStoragePublicDirectory(type)
}

View file

@ -0,0 +1,77 @@
/*
* 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.graphics.Bitmap;
import androidx.annotation.NonNull;
/**
* Created on : June 18, 2016 Author : zetbaitsu Name : Zetra GitHub : https://github.com/zetbaitsu
*/
public class ImageUtil {
private ImageUtil() {}
public static Bitmap resizeBitmap(@NonNull Bitmap src, int maxForSmallerSize) {
int width = src.getWidth();
int height = src.getHeight();
final int dstWidth;
final int dstHeight;
if (width < height) {
if (maxForSmallerSize >= width) {
return src;
}
float ratio = (float) height / width;
dstWidth = maxForSmallerSize;
dstHeight = Math.round(maxForSmallerSize * ratio);
} else {
if (maxForSmallerSize >= height) {
return src;
}
float ratio = (float) width / height;
dstWidth = Math.round(maxForSmallerSize * ratio);
dstHeight = maxForSmallerSize;
}
return Bitmap.createScaledBitmap(src, dstWidth, dstHeight, false);
}
public static int calculateInSampleSize(int width, int height, int reqWidth) {
// setting reqWidth matching to desired 1:1 ratio and screen-size
if (width < height) {
reqWidth = (height / width) * reqWidth;
} else {
reqWidth = (width / height) * reqWidth;
}
int inSampleSize = 1;
if (height > reqWidth || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) > reqWidth && (halfWidth / inSampleSize) > reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
}

View file

@ -0,0 +1,174 @@
/*
* 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.util.Log
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.model.lyrics.AbsSynchronizedLyrics
import org.jaudiotagger.audio.AudioFileIO
import org.jaudiotagger.tag.FieldKey
import java.io.*
/**
* Created by hefuyi on 2016/11/8.
*/
object LyricUtil {
private val lrcRootPath =
getExternalStorageDirectory().toString() + "/RetroMusic/lyrics/"
private const val TAG = "LyricUtil"
fun writeLrcToLoc(
title: String, artist: String, lrcContext: String
): File? {
var writer: FileWriter? = null
return try {
val file = File(getLrcPath(title, artist))
if (file.parentFile?.exists() != true) {
file.parentFile?.mkdirs()
}
writer = FileWriter(getLrcPath(title, artist))
writer.write(lrcContext)
file
} catch (e: IOException) {
e.printStackTrace()
null
} finally {
try {
writer?.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
}
//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
fun writeLrc(song: Song, lrcContext: String) {
var writer: FileWriter? = null
val location: File?
try {
if (isLrcOriginalFileExist(song.data)) {
location = getLocalLyricOriginalFile(song.data)
} else if (isLrcFileExist(song.title, song.artistName)) {
location = getLocalLyricFile(song.title, song.artistName)
} else {
location = File(getLrcPath(song.title, song.artistName))
if (location.parentFile?.exists() != true) {
location.parentFile?.mkdirs()
}
}
writer = FileWriter(location)
writer.write(lrcContext)
} catch (e: IOException) {
e.printStackTrace()
} finally {
try {
writer?.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
}
fun deleteLrcFile(title: String, artist: String): Boolean {
val file = File(getLrcPath(title, artist))
return file.delete()
}
private fun isLrcFileExist(title: String, artist: String): Boolean {
val file = File(getLrcPath(title, artist))
return file.exists()
}
private fun isLrcOriginalFileExist(path: String): Boolean {
val file = File(getLrcOriginalPath(path))
return file.exists()
}
private fun getLocalLyricFile(title: String, artist: String): File? {
val file = File(getLrcPath(title, artist))
return if (file.exists()) {
file
} else {
null
}
}
private fun getLocalLyricOriginalFile(path: String): File? {
val file = File(getLrcOriginalPath(path))
return if (file.exists()) {
file
} else {
null
}
}
private fun getLrcPath(title: String, artist: String): String {
return "$lrcRootPath$title - $artist.lrc"
}
fun getLrcOriginalPath(filePath: String): String {
return filePath.replace(filePath.substring(filePath.lastIndexOf(".") + 1), "lrc")
}
@Throws(Exception::class)
fun getStringFromFile(title: String, artist: String): String {
val file = File(getLrcPath(title, artist))
val fin = FileInputStream(file)
val ret = convertStreamToString(fin)
fin.close()
return ret
}
@Throws(Exception::class)
private fun convertStreamToString(inputStream: InputStream): String {
return inputStream.bufferedReader().readLines().joinToString(separator = "\n")
}
fun getStringFromLrc(file: File?): String {
try {
val reader = BufferedReader(FileReader(file))
return reader.readLines().joinToString(separator = "\n")
} catch (e: Exception) {
Log.i("Error", "Error Occurred")
}
return ""
}
fun getSyncedLyricsFile(song: Song): File? {
return when {
isLrcOriginalFileExist(song.data) -> {
getLocalLyricOriginalFile(song.data)
}
isLrcFileExist(song.title, song.artistName) -> {
getLocalLyricFile(song.title, song.artistName)
}
else -> {
null
}
}
}
fun getEmbeddedSyncedLyrics(data: String): String? {
val embeddedLyrics = try{
AudioFileIO.read(File(data)).tagOrCreateDefault.getFirst(FieldKey.LYRICS)
} catch(e: Exception){
return null
}
return if (AbsSynchronizedLyrics.isSynchronized(embeddedLyrics)) {
embeddedLyrics
} else {
null
}
}
}

View file

@ -0,0 +1,112 @@
package code.name.monkey.retromusic.util
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Matrix
import android.graphics.Paint
import androidx.core.graphics.scale
import com.bumptech.glide.util.Util.assertBackgroundThread
internal object MergedImageUtils {
private const val IMAGE_SIZE = 1600
private const val PARTS = 3
private const val DEGREES = 9f
fun joinImages(list: List<Bitmap>): Bitmap {
assertBackgroundThread()
val arranged = arrangeBitmaps(list)
val mergedImage = create(
arranged,
IMAGE_SIZE,
PARTS
)
val finalImage = rotate(
mergedImage,
IMAGE_SIZE,
DEGREES
)
mergedImage.recycle()
return finalImage
}
private fun arrangeBitmaps(list: List<Bitmap>): List<Bitmap> {
return when {
list.size == 1 -> {
val item = list[0]
listOf(item, item, item, item, item, item, item, item, item)
}
list.size == 2 -> {
val item1 = list[0]
val item2 = list[1]
listOf(item1, item2, item1, item2, item1, item2, item1, item2, item1)
}
list.size == 3 -> {
val item1 = list[0]
val item2 = list[1]
val item3 = list[2]
listOf(item1, item2, item3, item3, item1, item2, item2, item3, item1)
}
list.size == 4 -> {
val item1 = list[0]
val item2 = list[1]
val item3 = list[2]
val item4 = list[3]
listOf(item1, item2, item3, item4, item1, item2, item3, item4, item1)
}
list.size < 9 -> { // 5 to 8
val item1 = list[0]
val item2 = list[1]
val item3 = list[2]
val item4 = list[3]
val item5 = list[4]
listOf(item1, item2, item3, item4, item5, item2, item3, item4, item1)
}
else -> list // case 9
}
}
private fun create(images: List<Bitmap>, imageSize: Int, parts: Int): Bitmap {
val result = Bitmap.createBitmap(imageSize, imageSize, Bitmap.Config.ARGB_8888)
val canvas = Canvas(result)
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
val onePartSize = imageSize / parts
images.forEachIndexed { i, bitmap ->
val bit = bitmap.scale(onePartSize, onePartSize)
canvas.drawBitmap(
bit,
(onePartSize * (i % parts)).toFloat() + (i % 3) * 50,
(onePartSize * (i / parts)).toFloat() + (i / 3) * 50,
paint
)
bit.recycle()
}
return result
}
private fun rotate(bitmap: Bitmap, imageSize: Int, degrees: Float): Bitmap {
val matrix = Matrix()
matrix.postRotate(degrees)
val rotated = Bitmap.createBitmap(bitmap, 0, 0, imageSize, imageSize, matrix, true)
bitmap.recycle()
val cropStart = imageSize * 25 / 100
val cropEnd: Int = (cropStart * 1.5).toInt()
val cropped = Bitmap.createBitmap(
rotated,
cropStart,
cropStart,
imageSize - cropEnd,
imageSize - cropEnd
)
rotated.recycle()
return cropped
}
}

View file

@ -0,0 +1,490 @@
package code.name.monkey.retromusic.util
import android.content.ContentUris
import android.content.Context
import android.content.Intent
import android.database.Cursor
import android.net.Uri
import android.provider.BaseColumns
import android.provider.MediaStore
import android.util.Log
import androidx.core.content.FileProvider
import androidx.core.content.contentValuesOf
import androidx.core.net.toUri
import androidx.fragment.app.FragmentActivity
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.Constants
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.db.PlaylistEntity
import code.name.monkey.retromusic.db.SongEntity
import code.name.monkey.retromusic.db.toSongEntity
import code.name.monkey.retromusic.extensions.getLong
import code.name.monkey.retromusic.extensions.showToast
import code.name.monkey.retromusic.helper.MusicPlayerRemote.removeFromQueue
import code.name.monkey.retromusic.model.Artist
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.model.lyrics.AbsSynchronizedLyrics
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.component.KoinComponent
import org.koin.core.component.get
import java.io.File
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*
import java.util.regex.Pattern
object MusicUtil : KoinComponent {
fun createShareSongFileIntent(song: Song, context: Context): Intent? {
return try {
Intent().setAction(Intent.ACTION_SEND).putExtra(
Intent.EXTRA_STREAM,
FileProvider.getUriForFile(
context,
context.applicationContext.packageName,
File(song.data)
)
).addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION).setType("audio/*")
} catch (e: IllegalArgumentException) {
Intent().setAction(Intent.ACTION_SEND).putExtra(
Intent.EXTRA_STREAM,
getSongFileUri(song.id)
).addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION).setType("audio/*")
}
}
fun buildInfoString(string1: String?, string2: String?): String {
if (string1.isNullOrEmpty()) {
return if (string2.isNullOrEmpty()) "" else string2
}
return if (string2.isNullOrEmpty()) if (string1.isNullOrEmpty()) "" else string1 else "$string1$string2"
}
fun createAlbumArtFile(context: Context): File {
return File(
createAlbumArtDir(context),
System.currentTimeMillis().toString()
)
}
private fun createAlbumArtDir(context: Context): File {
val albumArtDir = File(
if (VersionUtils.hasR()) context.cacheDir else getExternalStorageDirectory(),
"/albumthumbs/"
)
if (!albumArtDir.exists()) {
albumArtDir.mkdirs()
try {
File(albumArtDir, ".nomedia").createNewFile()
} catch (e: IOException) {
e.printStackTrace()
}
}
return albumArtDir
}
fun deleteAlbumArt(context: Context, albumId: Long) {
val contentResolver = context.contentResolver
val localUri = "content://media/external/audio/albumart".toUri()
contentResolver.delete(ContentUris.withAppendedId(localUri, albumId), null, null)
contentResolver.notifyChange(localUri, null)
}
fun getArtistInfoString(
context: Context,
artist: Artist
): String {
val albumCount = artist.albumCount
val songCount = artist.songCount
val albumString =
if (albumCount == 1) context.resources.getString(R.string.album)
else context.resources.getString(R.string.albums)
val songString =
if (songCount == 1) context.resources.getString(R.string.song)
else context.resources.getString(R.string.songs)
return "$albumCount $albumString$songCount $songString"
}
//iTunes uses for example 1002 for track 2 CD1 or 3011 for track 11 CD3.
//this method converts those values to normal tracknumbers
fun getFixedTrackNumber(trackNumberToFix: Int): Int {
return trackNumberToFix % 1000
}
fun getLyrics(song: Song): String? {
var lyrics: String? = "No lyrics found"
val file = File(song.data)
try {
lyrics = AudioFileIO.read(file).tagOrCreateDefault.getFirst(FieldKey.LYRICS)
} catch (e: Exception) {
e.printStackTrace()
}
if (lyrics == null || lyrics.trim { it <= ' ' }.isEmpty() || AbsSynchronizedLyrics
.isSynchronized(lyrics)
) {
val dir = file.absoluteFile.parentFile
if (dir != null && dir.exists() && dir.isDirectory) {
val format = ".*%s.*\\.(lrc|txt)"
val filename = Pattern.quote(
FileUtil.stripExtension(file.name)
)
val songtitle = Pattern.quote(song.title)
val patterns =
ArrayList<Pattern>()
patterns.add(
Pattern.compile(
String.format(format, filename),
Pattern.CASE_INSENSITIVE or Pattern.UNICODE_CASE
)
)
patterns.add(
Pattern.compile(
String.format(format, songtitle),
Pattern.CASE_INSENSITIVE or Pattern.UNICODE_CASE
)
)
val files =
dir.listFiles { f: File ->
for (pattern in patterns) {
if (pattern.matcher(f.name).matches()) {
return@listFiles true
}
}
false
}
if (files != null && files.isNotEmpty()) {
for (f in files) {
try {
val newLyrics =
FileUtil.read(f)
if (newLyrics != null && newLyrics.trim { it <= ' ' }.isNotEmpty()) {
if (AbsSynchronizedLyrics.isSynchronized(newLyrics)) {
return newLyrics
}
lyrics = newLyrics
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
}
return lyrics
}
@JvmStatic
fun getMediaStoreAlbumCoverUri(albumId: Long): Uri {
val sArtworkUri = "content://media/external/audio/albumart".toUri()
return ContentUris.withAppendedId(sArtworkUri, albumId)
}
fun getPlaylistInfoString(
context: Context,
songs: List<Song>
): String {
val duration = getTotalDuration(songs)
return buildInfoString(
getSongCountString(context, songs.size),
getReadableDurationString(duration)
)
}
fun playlistInfoString(
context: Context,
songs: List<SongEntity>
): String {
return getSongCountString(context, songs.size)
}
fun getReadableDurationString(songDurationMillis: Long): String {
var minutes = songDurationMillis / 1000 / 60
val seconds = songDurationMillis / 1000 % 60
return if (minutes < 60) {
String.format(
Locale.getDefault(),
"%02d:%02d",
minutes,
seconds
)
} else {
val hours = minutes / 60
minutes %= 60
String.format(
Locale.getDefault(),
"%02d:%02d:%02d",
hours,
minutes,
seconds
)
}
}
fun getSectionName(mediaTitle: String?): String {
var musicMediaTitle = mediaTitle
return try {
if (musicMediaTitle.isNullOrEmpty()) {
return "-"
}
musicMediaTitle = musicMediaTitle.trim { it <= ' ' }.lowercase()
if (musicMediaTitle.startsWith("the ")) {
musicMediaTitle = musicMediaTitle.substring(4)
} else if (musicMediaTitle.startsWith("a ")) {
musicMediaTitle = musicMediaTitle.substring(2)
}
if (musicMediaTitle.isEmpty()) {
""
} else musicMediaTitle.substring(0, 1).uppercase()
} catch (e: Exception) {
""
}
}
fun getSongCountString(context: Context, songCount: Int): String {
val songString = if (songCount == 1) context.resources
.getString(R.string.song) else context.resources.getString(R.string.songs)
return "$songCount $songString"
}
fun getSongFileUri(songId: Long): Uri {
return ContentUris.withAppendedId(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
songId
)
}
fun getSongFilePath(context: Context, uri: Uri): String {
val projection = arrayOf(Constants.DATA)
context.contentResolver.query(uri, projection, null, null, null)?.use {
if (it.moveToFirst()) {
return it.getString(0)
}
}
return ""
}
fun getTotalDuration(songs: List<Song>): Long {
var duration: Long = 0
for (i in songs.indices) {
duration += songs[i].duration
}
return duration
}
fun getYearString(year: Int): String {
return if (year > 0) year.toString() else "-"
}
fun indexOfSongInList(songs: List<Song>, songId: Long): Int {
return songs.indexOfFirst { it.id == songId }
}
fun getDateModifiedString(date: Long): String {
val calendar: Calendar = Calendar.getInstance()
val pattern = "dd/MM/yyyy hh:mm:ss"
calendar.timeInMillis = date
val formatter = SimpleDateFormat(pattern, Locale.ENGLISH)
return formatter.format(calendar.time)
}
fun insertAlbumArt(
context: Context,
albumId: Long,
path: String?
) {
val contentResolver = context.contentResolver
val artworkUri = "content://media/external/audio/albumart".toUri()
contentResolver.delete(ContentUris.withAppendedId(artworkUri, albumId), null, null)
val values = contentValuesOf(
"album_id" to albumId,
"_data" to path
)
contentResolver.insert(artworkUri, values)
contentResolver.notifyChange(artworkUri, null)
}
fun isArtistNameUnknown(artistName: String?): Boolean {
if (artistName.isNullOrEmpty()) {
return false
}
if (artistName == Artist.UNKNOWN_ARTIST_DISPLAY_NAME) {
return true
}
val tempName = artistName.trim { it <= ' ' }.lowercase()
return tempName == "unknown" || tempName == "<unknown>"
}
fun isVariousArtists(artistName: String?): Boolean {
if (artistName.isNullOrEmpty()) {
return false
}
if (artistName == Artist.VARIOUS_ARTISTS_DISPLAY_NAME) {
return true
}
return false
}
val repository = get<Repository>()
fun toggleFavorite(context: Context, song: Song) {
GlobalScope.launch {
val playlist: PlaylistEntity = repository.favoritePlaylist()
val songEntity = song.toSongEntity(playlist.playListId)
val isFavorite = repository.isFavoriteSong(songEntity).isNotEmpty()
if (isFavorite) {
repository.removeSongFromPlaylist(songEntity)
} else {
repository.insertSongs(listOf(song.toSongEntity(playlist.playListId)))
}
context.sendBroadcast(Intent(MusicService.FAVORITE_STATE_CHANGED))
}
}
fun deleteTracks(
activity: FragmentActivity,
songs: List<Song>,
safUris: List<Uri>?,
callback: Runnable?
) {
val songRepository: SongRepository = get()
val projection = arrayOf(
BaseColumns._ID, Constants.DATA
)
// Split the query into multiple batches, and merge the resulting cursors
var batchStart: Int
var batchEnd = 0
val batchSize =
1000000 / 10 // 10^6 being the SQLite limite on the query lenth in bytes, 10 being the max number of digits in an int, used to store the track ID
val songCount = songs.size
while (batchEnd < songCount) {
batchStart = batchEnd
val selection = StringBuilder()
selection.append(BaseColumns._ID + " IN (")
var i = 0
while (i < batchSize - 1 && batchEnd < songCount - 1) {
selection.append(songs[batchEnd].id)
selection.append(",")
i++
batchEnd++
}
// The last element of a batch
// The last element of a batch
selection.append(songs[batchEnd].id)
batchEnd++
selection.append(")")
try {
val cursor = activity.contentResolver.query(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection.toString(),
null, null
)
if (cursor != null) {
// Step 1: Remove selected tracks from the current playlist, as well
// as from the album art cache
cursor.moveToFirst()
while (!cursor.isAfterLast) {
val id = cursor.getLong(BaseColumns._ID)
val song: Song = songRepository.song(id)
removeFromQueue(song)
cursor.moveToNext()
}
// Step 2: Remove selected tracks from the database
activity.contentResolver.delete(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
selection.toString(), null
)
// Step 3: Remove files from card
cursor.moveToFirst()
var index = batchStart
while (!cursor.isAfterLast) {
val name = cursor.getString(1)
val safUri =
if (safUris == null || safUris.size <= index) null else safUris[index]
SAFUtil.delete(activity, name, safUri)
index++
cursor.moveToNext()
}
cursor.close()
}
} catch (ignored: SecurityException) {
}
activity.contentResolver.notifyChange("content://media".toUri(), null)
activity.runOnUiThread {
activity.showToast(activity.getString(R.string.deleted_x_songs, songCount))
callback?.run()
}
}
}
suspend fun deleteTracks(context: Context, songs: List<Song>) {
val projection = arrayOf(BaseColumns._ID, Constants.DATA)
val selection = StringBuilder()
selection.append(BaseColumns._ID + " IN (")
for (i in songs.indices) {
selection.append(songs[i].id)
if (i < songs.size - 1) {
selection.append(",")
}
}
selection.append(")")
var deletedCount = 0
try {
val cursor: Cursor? = context.contentResolver.query(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection.toString(),
null, null
)
if (cursor != null) {
removeFromQueue(songs)
// Step 2: Remove files from card
cursor.moveToFirst()
while (!cursor.isAfterLast) {
val id: Int = cursor.getInt(0)
val name: String = cursor.getString(1)
try { // File.delete can throw a security exception
if (SAFUtil.delete(context, name, null)) {
// Step 3: Remove selected track from the database
context.contentResolver.delete(
ContentUris.withAppendedId(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
id.toLong()
), null, null
)
deletedCount++
} else {
Log.e("MusicUtils", "Failed to delete file $name")
}
cursor.moveToNext()
} catch (ex: SecurityException) {
cursor.moveToNext()
} catch (e: NullPointerException) {
Log.e("MusicUtils", "Failed to find file $name")
}
}
cursor.close()
}
withContext(Dispatchers.Main) {
context.showToast(context.getString(R.string.deleted_x_songs, deletedCount))
}
} catch (ignored: SecurityException) {
}
}
fun songByGenre(genreId: Long): Song {
return repository.getSongByGenre(genreId)
}
}

View file

@ -0,0 +1,71 @@
/*
* Copyright (c) 2019 Hemanth Savarala.
*
* Licensed under the GNU General Public License v3
*
* This is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*/
package code.name.monkey.retromusic.util
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.media.audiofx.AudioEffect
import android.widget.Toast
import androidx.fragment.app.FragmentActivity
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.activities.*
import code.name.monkey.retromusic.activities.bugreport.BugReportActivity
import code.name.monkey.retromusic.extensions.showToast
import code.name.monkey.retromusic.helper.MusicPlayerRemote.audioSessionId
object NavigationUtil {
fun bugReport(activity: Activity) {
activity.startActivity(
Intent(activity, BugReportActivity::class.java), null
)
}
fun goToOpenSource(activity: Activity) {
activity.startActivity(
Intent(activity, code.name.monkey.retromusic.activities.LicenseActivity::class.java), null
)
}
fun gotoDriveMode(activity: Activity) {
activity.startActivity(
Intent(activity, code.name.monkey.retromusic.activities.DriveModeActivity::class.java), null
)
}
fun gotoWhatNews(activity: FragmentActivity) {
val changelogBottomSheet = WhatsNewFragment()
changelogBottomSheet.show(activity.supportFragmentManager, WhatsNewFragment.TAG)
}
fun openEqualizer(activity: Activity) {
stockEqualizer(activity)
}
private fun stockEqualizer(activity: Activity) {
val sessionId = audioSessionId
if (sessionId == AudioEffect.ERROR_BAD_VALUE) {
activity.showToast(R.string.no_audio_ID, Toast.LENGTH_LONG)
} else {
try {
val effects = Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL)
effects.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, sessionId)
effects.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)
activity.startActivityForResult(effects, 0)
} catch (notFound: ActivityNotFoundException) {
activity.showToast(R.string.no_equalizer)
}
}
}
}

View file

@ -0,0 +1,350 @@
/*
* 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
import java.util.*
/**
* 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 [MediaBrowserServiceCompat.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.
*/
@Suppress("Deprecation")
@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.
return if (packageInfo.signatures == null || packageInfo.signatures.size != 1) {
null
} else {
val certificate = packageInfo.signatures[0].toByteArray()
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, "")
.lowercase(Locale.getDefault())
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(
val name: String,
val packageName: String,
val signatures: MutableSet<KnownSignature>
)
private data class KnownSignature(
val signature: String,
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(
val name: String,
val packageName: String,
val uid: Int,
val signature: String?,
val permissions: Set<String>
)
}
private const val TAG = "PackageValidator"
private const val ANDROID_PLATFORM = "android"
private val WHITESPACE_REGEX = "\\s|\\n".toRegex()

View file

@ -0,0 +1,28 @@
/*
* 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 code.name.monkey.retromusic.db.PlaylistWithSongs
import code.name.monkey.retromusic.helper.M3UWriter.writeIO
import java.io.File
import java.io.IOException
object PlaylistsUtil {
@Throws(IOException::class)
fun savePlaylistWithSongs(playlist: PlaylistWithSongs?): File {
return writeIO(
File(getExternalStorageDirectory(), "Playlists"), playlist!!
)
}
}

View file

@ -0,0 +1,714 @@
package code.name.monkey.retromusic.util
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import androidx.core.content.edit
import androidx.core.content.res.use
import androidx.fragment.app.Fragment
import androidx.preference.PreferenceManager
import androidx.viewpager.widget.ViewPager
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.*
import code.name.monkey.retromusic.extensions.getIntRes
import code.name.monkey.retromusic.extensions.getStringOrDefault
import code.name.monkey.retromusic.fragments.AlbumCoverStyle
import code.name.monkey.retromusic.fragments.GridStyle
import code.name.monkey.retromusic.fragments.NowPlayingScreen
import code.name.monkey.retromusic.fragments.folder.FoldersFragment
import code.name.monkey.retromusic.helper.SortOrder.*
import code.name.monkey.retromusic.transform.*
import code.name.monkey.retromusic.model.CategoryInfo
import code.name.monkey.retromusic.util.theme.ThemeMode
import code.name.monkey.retromusic.views.TopAppBarLayout
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import com.google.gson.reflect.TypeToken
import java.io.File
object PreferenceUtil {
private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(App.getContext())
val defaultCategories = listOf(
CategoryInfo(CategoryInfo.Category.Home, true),
CategoryInfo(CategoryInfo.Category.Songs, true),
CategoryInfo(CategoryInfo.Category.Albums, true),
CategoryInfo(CategoryInfo.Category.Artists, true),
CategoryInfo(CategoryInfo.Category.Playlists, true),
CategoryInfo(CategoryInfo.Category.Genres, false),
CategoryInfo(CategoryInfo.Category.Folder, false),
CategoryInfo(CategoryInfo.Category.Search, false)
)
var libraryCategory: List<CategoryInfo>
get() {
val gson = Gson()
val collectionType = object : TypeToken<List<CategoryInfo>>() {}.type
val data = sharedPreferences.getStringOrDefault(
LIBRARY_CATEGORIES,
gson.toJson(defaultCategories, collectionType)
)
return try {
Gson().fromJson(data, collectionType)
} catch (e: JsonSyntaxException) {
e.printStackTrace()
return defaultCategories
}
}
set(value) {
val collectionType = object : TypeToken<List<CategoryInfo?>?>() {}.type
sharedPreferences.edit {
putString(LIBRARY_CATEGORIES, Gson().toJson(value, collectionType))
}
}
fun registerOnSharedPreferenceChangedListener(
listener: OnSharedPreferenceChangeListener,
) = sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
fun unregisterOnSharedPreferenceChangedListener(
changeListener: OnSharedPreferenceChangeListener,
) = sharedPreferences.unregisterOnSharedPreferenceChangeListener(changeListener)
val baseTheme get() = sharedPreferences.getStringOrDefault(GENERAL_THEME, "auto")
fun getGeneralThemeValue(isSystemDark: Boolean): ThemeMode {
val themeMode: String =
sharedPreferences.getStringOrDefault(GENERAL_THEME, "auto")
return if (isBlackMode && isSystemDark && themeMode != "light") {
ThemeMode.BLACK
} else {
if (isBlackMode && themeMode == "dark") {
ThemeMode.BLACK
} else {
when (themeMode) {
"light" -> ThemeMode.LIGHT
"dark" -> ThemeMode.DARK
"auto" -> ThemeMode.AUTO
else -> ThemeMode.AUTO
}
}
}
}
val languageCode: String get() = sharedPreferences.getString(LANGUAGE_NAME, "auto") ?: "auto"
var Fragment.userName
get() = sharedPreferences.getString(
USER_NAME,
getString(R.string.user_name)
)
set(value) = sharedPreferences.edit {
putString(USER_NAME, value)
}
var safSdCardUri
get() = sharedPreferences.getStringOrDefault(SAF_SDCARD_URI, "")
set(value) = sharedPreferences.edit {
putString(SAF_SDCARD_URI, value)
}
var albumArtistsOnly
get() = sharedPreferences.getBoolean(
ALBUM_ARTISTS_ONLY,
false
)
set(value) = sharedPreferences.edit { putBoolean(ALBUM_ARTISTS_ONLY, value) }
var albumDetailSongSortOrder
get() = sharedPreferences.getStringOrDefault(
ALBUM_DETAIL_SONG_SORT_ORDER,
AlbumSongSortOrder.SONG_TRACK_LIST
)
set(value) = sharedPreferences.edit { putString(ALBUM_DETAIL_SONG_SORT_ORDER, value) }
var artistDetailSongSortOrder
get() = sharedPreferences.getStringOrDefault(
ARTIST_DETAIL_SONG_SORT_ORDER,
ArtistSongSortOrder.SONG_A_Z
)
set(value) = sharedPreferences.edit { putString(ARTIST_DETAIL_SONG_SORT_ORDER, value) }
var songSortOrder
get() = sharedPreferences.getStringOrDefault(
SONG_SORT_ORDER,
SongSortOrder.SONG_A_Z
)
set(value) = sharedPreferences.edit {
putString(SONG_SORT_ORDER, value)
}
var albumSortOrder
get() = sharedPreferences.getStringOrDefault(
ALBUM_SORT_ORDER,
AlbumSortOrder.ALBUM_A_Z
)
set(value) = sharedPreferences.edit {
putString(ALBUM_SORT_ORDER, value)
}
var artistSortOrder
get() = sharedPreferences.getStringOrDefault(
ARTIST_SORT_ORDER,
ArtistSortOrder.ARTIST_A_Z
)
set(value) = sharedPreferences.edit {
putString(ARTIST_SORT_ORDER, value)
}
val albumSongSortOrder
get() = sharedPreferences.getStringOrDefault(
ALBUM_SONG_SORT_ORDER,
AlbumSongSortOrder.SONG_TRACK_LIST
)
val artistSongSortOrder
get() = sharedPreferences.getStringOrDefault(
ARTIST_SONG_SORT_ORDER,
AlbumSongSortOrder.SONG_TRACK_LIST
)
val artistAlbumSortOrder
get() = sharedPreferences.getStringOrDefault(
ARTIST_ALBUM_SORT_ORDER,
ArtistAlbumSortOrder.ALBUM_A_Z
)
var playlistSortOrder
get() = sharedPreferences.getStringOrDefault(
PLAYLIST_SORT_ORDER,
PlaylistSortOrder.PLAYLIST_A_Z
)
set(value) = sharedPreferences.edit {
putString(PLAYLIST_SORT_ORDER, value)
}
val genreSortOrder
get() = sharedPreferences.getStringOrDefault(
GENRE_SORT_ORDER,
GenreSortOrder.GENRE_A_Z
)
val isIgnoreMediaStoreArtwork
get() = sharedPreferences.getBoolean(
IGNORE_MEDIA_STORE_ARTWORK,
false
)
val isVolumeVisibilityMode
get() = sharedPreferences.getBoolean(
TOGGLE_VOLUME, false
)
var isInitializedBlacklist
get() = sharedPreferences.getBoolean(
INITIALIZED_BLACKLIST, false
)
set(value) = sharedPreferences.edit {
putBoolean(INITIALIZED_BLACKLIST, value)
}
private val isBlackMode
get() = sharedPreferences.getBoolean(
BLACK_THEME, false
)
val isExtraControls
get() = sharedPreferences.getBoolean(
TOGGLE_ADD_CONTROLS, false
)
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) }
val isScreenOnEnabled get() = sharedPreferences.getBoolean(KEEP_SCREEN_ON, false)
val isShuffleModeOn get() = sharedPreferences.getBoolean(TOGGLE_SHUFFLE, false)
val isSongInfo get() = sharedPreferences.getBoolean(EXTRA_SONG_INFO, false)
val isPauseOnZeroVolume get() = sharedPreferences.getBoolean(PAUSE_ON_ZERO_VOLUME, false)
var isSleepTimerFinishMusic
get() = sharedPreferences.getBoolean(
SLEEP_TIMER_FINISH_SONG, false
)
set(value) = sharedPreferences.edit {
putBoolean(SLEEP_TIMER_FINISH_SONG, value)
}
val isExpandPanel get() = sharedPreferences.getBoolean(EXPAND_NOW_PLAYING_PANEL, false)
val isHeadsetPlugged
get() = sharedPreferences.getBoolean(
TOGGLE_HEADSET, false
)
val isAlbumArtOnLockScreen
get() = sharedPreferences.getBoolean(
ALBUM_ART_ON_LOCK_SCREEN, false
)
val isAudioDucking
get() = sharedPreferences.getBoolean(
AUDIO_DUCKING, true
)
val isBluetoothSpeaker
get() = sharedPreferences.getBoolean(
BLUETOOTH_PLAYBACK, false
)
val isBlurredAlbumArt
get() = sharedPreferences.getBoolean(
BLURRED_ALBUM_ART, false
)
val blurAmount get() = sharedPreferences.getInt(NEW_BLUR_AMOUNT, 25)
val isCarouselEffect
get() = sharedPreferences.getBoolean(
CAROUSEL_EFFECT, false
)
var isColoredAppShortcuts
get() = sharedPreferences.getBoolean(
COLORED_APP_SHORTCUTS, true
)
set(value) = sharedPreferences.edit {
putBoolean(COLORED_APP_SHORTCUTS, value)
}
var isColoredNotification
get() = sharedPreferences.getBoolean(
COLORED_NOTIFICATION, true
)
set(value) = sharedPreferences.edit {
putBoolean(COLORED_NOTIFICATION, value)
}
var isDesaturatedColor
get() = sharedPreferences.getBoolean(
DESATURATED_COLOR, false
)
set(value) = sharedPreferences.edit {
putBoolean(DESATURATED_COLOR, value)
}
val isGapLessPlayback
get() = sharedPreferences.getBoolean(
GAP_LESS_PLAYBACK, false
)
val isAdaptiveColor
get() = sharedPreferences.getBoolean(
ADAPTIVE_COLOR_APP, false
)
val isFullScreenMode
get() = sharedPreferences.getBoolean(
TOGGLE_FULL_SCREEN, false
)
val isAudioFocusEnabled
get() = sharedPreferences.getBoolean(
MANAGE_AUDIO_FOCUS, false
)
val isLockScreen get() = sharedPreferences.getBoolean(LOCK_SCREEN, false)
var lyricsOption
get() = sharedPreferences.getInt(LYRICS_OPTIONS, 1)
set(value) = sharedPreferences.edit {
putInt(LYRICS_OPTIONS, value)
}
var songGridStyle: GridStyle
get() {
val id: Int = sharedPreferences.getInt(SONG_GRID_STYLE, 0)
// We can directly use "first" kotlin extension function here but
// there maybe layout id stored in this so to avoid a crash we use
// "firstOrNull"
return GridStyle.values().firstOrNull { gridStyle ->
gridStyle.id == id
} ?: GridStyle.Grid
}
set(value) = sharedPreferences.edit {
putInt(SONG_GRID_STYLE, value.id)
}
var albumGridStyle: GridStyle
get() {
val id: Int = sharedPreferences.getInt(ALBUM_GRID_STYLE, 0)
return GridStyle.values().firstOrNull { gridStyle ->
gridStyle.id == id
} ?: GridStyle.Grid
}
set(value) = sharedPreferences.edit {
putInt(ALBUM_GRID_STYLE, value.id)
}
var artistGridStyle: GridStyle
get() {
val id: Int = sharedPreferences.getInt(ARTIST_GRID_STYLE, 3)
return GridStyle.values().firstOrNull { gridStyle ->
gridStyle.id == id
} ?: GridStyle.Circular
}
set(value) = sharedPreferences.edit {
putInt(ARTIST_GRID_STYLE, value.id)
}
val filterLength get() = sharedPreferences.getInt(FILTER_SONG, 20)
var lastVersion
// This was stored as an integer before now it's a long, so avoid a ClassCastException
get() = try {
sharedPreferences.getLong(LAST_CHANGELOG_VERSION, 0)
} catch (e: ClassCastException) {
sharedPreferences.edit { remove(LAST_CHANGELOG_VERSION) }
0
}
set(value) = sharedPreferences.edit {
putLong(LAST_CHANGELOG_VERSION, value)
}
var lastSleepTimerValue
get() = sharedPreferences.getInt(
LAST_SLEEP_TIMER_VALUE,
30
)
set(value) = sharedPreferences.edit {
putInt(LAST_SLEEP_TIMER_VALUE, value)
}
var nextSleepTimerElapsedRealTime
get() = sharedPreferences.getInt(
NEXT_SLEEP_TIMER_ELAPSED_REALTIME,
-1
)
set(value) = sharedPreferences.edit {
putInt(NEXT_SLEEP_TIMER_ELAPSED_REALTIME, value)
}
fun themeResFromPrefValue(themePrefValue: String): Int {
return when (themePrefValue) {
"light" -> R.style.Theme_RetroMusic_Light
"dark" -> R.style.Theme_RetroMusic
else -> R.style.Theme_RetroMusic
}
}
val homeArtistGridStyle: Int
get() {
val position = sharedPreferences.getStringOrDefault(
HOME_ARTIST_GRID_STYLE, "0"
).toInt()
val layoutRes =
App.getContext().resources.obtainTypedArray(R.array.pref_home_grid_style_layout)
.use {
it.getResourceId(position, 0)
}
return if (layoutRes == 0) {
R.layout.item_artist
} else layoutRes
}
val homeAlbumGridStyle: Int
get() {
val position = sharedPreferences.getStringOrDefault(
HOME_ALBUM_GRID_STYLE, "4"
).toInt()
val layoutRes = App.getContext()
.resources.obtainTypedArray(R.array.pref_home_grid_style_layout).use {
it.getResourceId(position, 0)
}
return if (layoutRes == 0) {
R.layout.item_image
} else layoutRes
}
val tabTitleMode: Int
get() {
return when (sharedPreferences.getStringOrDefault(
TAB_TEXT_MODE, "1"
).toInt()) {
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
}
}
var songGridSize
get() = sharedPreferences.getInt(
SONG_GRID_SIZE,
App.getContext().getIntRes(R.integer.default_list_columns)
)
set(value) = sharedPreferences.edit {
putInt(SONG_GRID_SIZE, value)
}
var songGridSizeLand
get() = sharedPreferences.getInt(
SONG_GRID_SIZE_LAND,
App.getContext().getIntRes(R.integer.default_grid_columns_land)
)
set(value) = sharedPreferences.edit {
putInt(SONG_GRID_SIZE_LAND, value)
}
var albumGridSize: Int
get() = sharedPreferences.getInt(
ALBUM_GRID_SIZE,
App.getContext().getIntRes(R.integer.default_grid_columns)
)
set(value) = sharedPreferences.edit {
putInt(ALBUM_GRID_SIZE, value)
}
var albumGridSizeLand
get() = sharedPreferences.getInt(
ALBUM_GRID_SIZE_LAND,
App.getContext().getIntRes(R.integer.default_grid_columns_land)
)
set(value) = sharedPreferences.edit {
putInt(ALBUM_GRID_SIZE_LAND, value)
}
var artistGridSize
get() = sharedPreferences.getInt(
ARTIST_GRID_SIZE,
App.getContext().getIntRes(R.integer.default_grid_columns)
)
set(value) = sharedPreferences.edit {
putInt(ARTIST_GRID_SIZE, value)
}
var artistGridSizeLand
get() = sharedPreferences.getInt(
ARTIST_GRID_SIZE_LAND,
App.getContext().getIntRes(R.integer.default_grid_columns_land)
)
set(value) = sharedPreferences.edit {
putInt(ALBUM_GRID_SIZE_LAND, value)
}
var playlistGridSize
get() = sharedPreferences.getInt(
PLAYLIST_GRID_SIZE,
App.getContext().getIntRes(R.integer.default_grid_columns)
)
set(value) = sharedPreferences.edit {
putInt(PLAYLIST_GRID_SIZE, value)
}
var playlistGridSizeLand
get() = sharedPreferences.getInt(
PLAYLIST_GRID_SIZE_LAND,
App.getContext().getIntRes(R.integer.default_grid_columns_land)
)
set(value) = sharedPreferences.edit {
putInt(PLAYLIST_GRID_SIZE, value)
}
var albumCoverStyle: AlbumCoverStyle
get() {
val id: Int = sharedPreferences.getInt(ALBUM_COVER_STYLE, 0)
for (albumCoverStyle in AlbumCoverStyle.values()) {
if (albumCoverStyle.id == id) {
return albumCoverStyle
}
}
return AlbumCoverStyle.Card
}
set(value) = sharedPreferences.edit { putInt(ALBUM_COVER_STYLE, value.id) }
var nowPlayingScreen: NowPlayingScreen
get() {
val id: Int = sharedPreferences.getInt(NOW_PLAYING_SCREEN_ID, 0)
for (nowPlayingScreen in NowPlayingScreen.values()) {
if (nowPlayingScreen.id == id) {
return nowPlayingScreen
}
}
return NowPlayingScreen.Adaptive
}
set(value) = sharedPreferences.edit {
putInt(NOW_PLAYING_SCREEN_ID, value.id)
// Also set a cover theme for that now playing
value.defaultCoverTheme?.let { coverTheme -> albumCoverStyle = coverTheme }
}
val albumCoverTransform: ViewPager.PageTransformer
get() {
val style = sharedPreferences.getStringOrDefault(
ALBUM_COVER_TRANSFORM,
"0"
).toInt()
return when (style) {
0 -> NormalPageTransformer()
1 -> CascadingPageTransformer()
2 -> DepthTransformation()
3 -> HorizontalFlipTransformation()
4 -> VerticalFlipTransformation()
5 -> HingeTransformation()
6 -> VerticalStackTransformer()
else -> ViewPager.PageTransformer { _, _ -> }
}
}
var startDirectory: File
get() {
val folderPath = FoldersFragment.defaultStartDirectory.path
val filePath: String = sharedPreferences.getStringOrDefault(START_DIRECTORY, folderPath)
return File(filePath)
}
set(value) = sharedPreferences.edit {
putString(
START_DIRECTORY,
FileUtil.safeGetCanonicalPath(value)
)
}
fun getRecentlyPlayedCutoffTimeMillis(): Long {
val calendarUtil = CalendarUtil()
val interval: Long = when (sharedPreferences.getString(RECENTLY_PLAYED_CUTOFF, "")) {
"today" -> calendarUtil.elapsedToday
"this_week" -> calendarUtil.elapsedWeek
"past_seven_days" -> calendarUtil.getElapsedDays(7)
"past_three_months" -> calendarUtil.getElapsedMonths(3)
"this_year" -> calendarUtil.elapsedYear
"this_month" -> calendarUtil.elapsedMonth
else -> calendarUtil.elapsedMonth
}
return System.currentTimeMillis() - interval
}
val lastAddedCutoff: Long
get() {
val calendarUtil = CalendarUtil()
val interval =
when (sharedPreferences.getStringOrDefault(LAST_ADDED_CUTOFF, "this_month")) {
"today" -> calendarUtil.elapsedToday
"this_week" -> calendarUtil.elapsedWeek
"past_three_months" -> calendarUtil.getElapsedMonths(3)
"this_year" -> calendarUtil.elapsedYear
"this_month" -> calendarUtil.elapsedMonth
else -> calendarUtil.elapsedMonth
}
return (System.currentTimeMillis() - interval) / 1000
}
val homeSuggestions: Boolean
get() = sharedPreferences.getBoolean(
TOGGLE_SUGGESTIONS,
true
)
val pauseHistory: Boolean
get() = sharedPreferences.getBoolean(
PAUSE_HISTORY,
false
)
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)
val crossFadeDuration
get() = sharedPreferences
.getInt(CROSS_FADE_DURATION, 0)
val isCrossfadeEnabled get() = crossFadeDuration > 0
val materialYou
get() = sharedPreferences.getBoolean(MATERIAL_YOU, VersionUtils.hasS())
val isCustomFont
get() = sharedPreferences.getBoolean(CUSTOM_FONT, false)
val isSnowFalling
get() = sharedPreferences.getBoolean(SNOWFALL, false)
val lyricsType: LyricsType
get() = if (sharedPreferences.getString(LYRICS_TYPE, "0") == "0") {
LyricsType.REPLACE_COVER
} else {
LyricsType.OVER_COVER
}
var playbackSpeed
get() = sharedPreferences
.getFloat(PLAYBACK_SPEED, 1F)
set(value) = sharedPreferences.edit { putFloat(PLAYBACK_SPEED, value) }
var playbackPitch
get() = sharedPreferences
.getFloat(PLAYBACK_PITCH, 1F)
set(value) = sharedPreferences.edit { putFloat(PLAYBACK_PITCH, value) }
val appBarMode: TopAppBarLayout.AppBarMode
get() = if (sharedPreferences.getString(APPBAR_MODE, "1") == "0") {
TopAppBarLayout.AppBarMode.COLLAPSING
} else {
TopAppBarLayout.AppBarMode.SIMPLE
}
val wallpaperAccent
get() = sharedPreferences.getBoolean(
WALLPAPER_ACCENT,
VersionUtils.hasOreoMR1() && !VersionUtils.hasS()
)
val lyricsScreenOn
get() = sharedPreferences.getBoolean(SCREEN_ON_LYRICS, false)
val circlePlayButton
get() = sharedPreferences.getBoolean(CIRCLE_PLAY_BUTTON, false)
val swipeAnywhereToChangeSong
get() = sharedPreferences.getBoolean(SWIPE_ANYWHERE_NOW_PLAYING, true)
val swipeDownToDismiss
get() = sharedPreferences.getBoolean(SWIPE_DOWN_DISMISS, true)
}
enum class LyricsType {
REPLACE_COVER, OVER_COVER
}

View file

@ -0,0 +1,249 @@
/*
* 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.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.core.content.ContextCompat;
import androidx.palette.graphics.Palette;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import code.name.monkey.appthemehelper.ThemeStore;
import code.name.monkey.appthemehelper.util.ColorUtil;
import code.name.monkey.appthemehelper.util.VersionUtils;
import io.github.muntashirakon.music.R;
public class RetroColorUtil {
public static int desaturateColor(int color, float ratio) {
float[] hsv = new float[3];
Color.colorToHSV(color, hsv);
hsv[1] = (hsv[1] * ratio) + (0.2f * (1.0f - ratio));
return Color.HSVToColor(hsv);
}
@Nullable
public static Palette generatePalette(@Nullable Bitmap bitmap) {
return bitmap == null ? null : Palette.from(bitmap).clearFilters().generate();
}
public static int getTextColor(@Nullable Palette palette) {
if (palette == null) {
return -1;
}
int inverse = -1;
if (palette.getVibrantSwatch() != null) {
inverse = palette.getVibrantSwatch().getRgb();
} else if (palette.getLightVibrantSwatch() != null) {
inverse = palette.getLightVibrantSwatch().getRgb();
} else if (palette.getDarkVibrantSwatch() != null) {
inverse = palette.getDarkVibrantSwatch().getRgb();
}
int background = getSwatch(palette).getRgb();
if (inverse != -1) {
return ColorUtil.INSTANCE.getReadableText(inverse, background, 150);
}
return ColorUtil.INSTANCE.stripAlpha(getSwatch(palette).getTitleTextColor());
}
@NonNull
public static Palette.Swatch getSwatch(@Nullable Palette palette) {
if (palette == null) {
return new Palette.Swatch(Color.WHITE, 1);
}
return getBestPaletteSwatchFrom(palette.getSwatches());
}
public static int getMatColor(Context context, String typeColor) {
int returnColor = Color.BLACK;
int arrayId =
context
.getResources()
.getIdentifier(
"md_" + typeColor, "array", context.getApplicationContext().getPackageName());
if (arrayId != 0) {
TypedArray colors = context.getResources().obtainTypedArray(arrayId);
int index = (int) (Math.random() * colors.length());
returnColor = colors.getColor(index, Color.BLACK);
colors.recycle();
}
return returnColor;
}
@ColorInt
public static int getColor(@Nullable Palette palette, int fallback) {
if (palette != null) {
if (palette.getVibrantSwatch() != null) {
return palette.getVibrantSwatch().getRgb();
} else if (palette.getDarkVibrantSwatch() != null) {
return palette.getDarkVibrantSwatch().getRgb();
} else if (palette.getLightVibrantSwatch() != null) {
return palette.getLightVibrantSwatch().getRgb();
} else if (palette.getMutedSwatch() != null) {
return palette.getMutedSwatch().getRgb();
} else if (palette.getLightMutedSwatch() != null) {
return palette.getLightMutedSwatch().getRgb();
} else if (palette.getDarkMutedSwatch() != null) {
return palette.getDarkMutedSwatch().getRgb();
} else if (!palette.getSwatches().isEmpty()) {
return Collections.max(palette.getSwatches(), SwatchComparator.getInstance()).getRgb();
}
}
return fallback;
}
private static Palette.Swatch getTextSwatch(@Nullable Palette palette) {
if (palette == null) {
return new Palette.Swatch(Color.BLACK, 1);
}
if (palette.getVibrantSwatch() != null) {
return palette.getVibrantSwatch();
} else {
return new Palette.Swatch(Color.BLACK, 1);
}
}
@ColorInt
public static int getBackgroundColor(@Nullable Palette palette) {
return getProperBackgroundSwatch(palette).getRgb();
}
private static Palette.Swatch getProperBackgroundSwatch(@Nullable Palette palette) {
if (palette == null) {
return new Palette.Swatch(Color.BLACK, 1);
}
if (palette.getDarkMutedSwatch() != null) {
return palette.getDarkMutedSwatch();
} else if (palette.getMutedSwatch() != null) {
return palette.getMutedSwatch();
} else if (palette.getLightMutedSwatch() != null) {
return palette.getLightMutedSwatch();
} else {
return new Palette.Swatch(Color.BLACK, 1);
}
}
private static Palette.Swatch getBestPaletteSwatchFrom(Palette palette) {
if (palette != null) {
if (palette.getVibrantSwatch() != null) {
return palette.getVibrantSwatch();
} else if (palette.getMutedSwatch() != null) {
return palette.getMutedSwatch();
} else if (palette.getDarkVibrantSwatch() != null) {
return palette.getDarkVibrantSwatch();
} else if (palette.getDarkMutedSwatch() != null) {
return palette.getDarkMutedSwatch();
} else if (palette.getLightVibrantSwatch() != null) {
return palette.getLightVibrantSwatch();
} else if (palette.getLightMutedSwatch() != null) {
return palette.getLightMutedSwatch();
} else if (!palette.getSwatches().isEmpty()) {
return getBestPaletteSwatchFrom(palette.getSwatches());
}
}
return null;
}
private static Palette.Swatch getBestPaletteSwatchFrom(List<Palette.Swatch> swatches) {
if (swatches == null) {
return null;
}
return Collections.max(
swatches,
(opt1, opt2) -> {
int a = opt1 == null ? 0 : opt1.getPopulation();
int b = opt2 == null ? 0 : opt2.getPopulation();
return a - b;
});
}
public static int getDominantColor(Bitmap bitmap, int defaultFooterColor) {
List<Palette.Swatch> swatchesTemp = Palette.from(bitmap).generate().getSwatches();
List<Palette.Swatch> swatches = new ArrayList<>(swatchesTemp);
Collections.sort(
swatches, (swatch1, swatch2) -> swatch2.getPopulation() - swatch1.getPopulation());
return swatches.size() > 0 ? swatches.get(0).getRgb() : defaultFooterColor;
}
@ColorInt
public static int shiftBackgroundColorForLightText(@ColorInt int backgroundColor) {
while (ColorUtil.INSTANCE.isColorLight(backgroundColor)) {
backgroundColor = ColorUtil.INSTANCE.darkenColor(backgroundColor);
}
return backgroundColor;
}
@ColorInt
public static int shiftBackgroundColorForDarkText(@ColorInt int backgroundColor) {
int color = backgroundColor;
while (!ColorUtil.INSTANCE.isColorLight(backgroundColor)) {
color = ColorUtil.INSTANCE.lightenColor(backgroundColor);
}
return color;
}
@ColorInt
public static int shiftBackgroundColor(@ColorInt int backgroundColor) {
int color = backgroundColor;
if (ColorUtil.INSTANCE.isColorLight(color)) {
color = ColorUtil.INSTANCE.shiftColor(color, 0.5F);
} else {
color = ColorUtil.INSTANCE.shiftColor(color, 1.5F);
}
return color;
}
public static int getMD3AccentColor(@NotNull Context context) {
if (VersionUtils.hasS()) {
return ContextCompat.getColor(context, R.color.m3_accent_color);
} else {
return ThemeStore.Companion.accentColor(context);
}
}
private static class SwatchComparator implements Comparator<Palette.Swatch> {
private static SwatchComparator sInstance;
static SwatchComparator getInstance() {
if (sInstance == null) {
sInstance = new SwatchComparator();
}
return sInstance;
}
@Override
public int compare(Palette.Swatch lhs, Palette.Swatch rhs) {
return lhs.getPopulation() - rhs.getPopulation();
}
}
}

View file

@ -0,0 +1,117 @@
/*
* 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.content.Context
import android.content.res.Configuration
import android.graphics.Point
import code.name.monkey.retromusic.App.Companion.getContext
import java.net.InetAddress
import java.net.NetworkInterface
import java.text.DecimalFormat
import java.util.*
object RetroUtil {
fun formatValue(numValue: Float): String {
var value = numValue
val arr = arrayOf("", "K", "M", "B", "T", "P", "E")
var index = 0
while (value / 1000 >= 1) {
value /= 1000
index++
}
val decimalFormat = DecimalFormat("#.##")
return String.format("%s %s", decimalFormat.format(value.toDouble()), arr[index])
}
fun frequencyCount(frequency: Int): Float {
return (frequency / 1000.0).toFloat()
}
fun getScreenSize(context: Context): Point {
val x: Int = context.resources.displayMetrics.widthPixels
val y: Int = context.resources.displayMetrics.heightPixels
return Point(x, y)
}
val statusBarHeight: Int
get() {
var result = 0
val resourceId = getContext()
.resources
.getIdentifier("status_bar_height", "dimen", "android")
if (resourceId > 0) {
result = getContext().resources.getDimensionPixelSize(resourceId)
}
return result
}
val navigationBarHeight: Int
get() {
var result = 0
val resourceId = getContext()
.resources
.getIdentifier("navigation_bar_height", "dimen", "android")
if (resourceId > 0) {
result = getContext().resources.getDimensionPixelSize(resourceId)
}
return result
}
val isLandscape: Boolean
get() = (getContext().resources.configuration.orientation
== Configuration.ORIENTATION_LANDSCAPE)
val isTablet: Boolean
get() = (getContext().resources.configuration.smallestScreenWidthDp
>= 600)
fun getIpAddress(useIPv4: Boolean): String? {
try {
val interfaces: List<NetworkInterface> =
Collections.list(NetworkInterface.getNetworkInterfaces())
for (intf in interfaces) {
val addrs: List<InetAddress> = Collections.list(intf.inetAddresses)
for (addr in addrs) {
if (!addr.isLoopbackAddress) {
val sAddr = addr.hostAddress
if (sAddr != null) {
val isIPv4 = sAddr.indexOf(':') < 0
if (useIPv4) {
if (isIPv4) return sAddr
} else {
if (!isIPv4) {
val delim = sAddr.indexOf('%') // drop ip6 zone suffix
return if (delim < 0) {
sAddr.uppercase()
} else {
sAddr.substring(
0,
delim
).uppercase()
}
}
}
} else {
return null
}
}
}
}
} catch (ignored: Exception) {
}
return ""
}
}

View file

@ -0,0 +1,76 @@
/*
* 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.content.Context
import android.content.Intent
import android.provider.BaseColumns
import android.provider.MediaStore
import android.provider.Settings
import androidx.core.net.toUri
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.extensions.showToast
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.util.MusicUtil.getSongFileUri
import com.google.android.material.dialog.MaterialAlertDialogBuilder
object RingtoneManager {
fun setRingtone(context: Context, song: Song) {
val uri = getSongFileUri(song.id)
val resolver = context.contentResolver
try {
val cursor = resolver.query(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
arrayOf(MediaStore.MediaColumns.TITLE),
BaseColumns._ID + "=?",
arrayOf(song.id.toString()), null
)
cursor.use { cursorSong ->
if (cursorSong != null && cursorSong.count == 1) {
cursorSong.moveToFirst()
Settings.System.putString(resolver, Settings.System.RINGTONE, uri.toString())
val message = context
.getString(R.string.x_has_been_set_as_ringtone, cursorSong.getString(0))
context.showToast(message)
}
}
} catch (ignored: SecurityException) {
}
}
fun requiresDialog(context: Context): Boolean {
if (VersionUtils.hasMarshmallow()) {
if (!Settings.System.canWrite(context)) {
return true
}
}
return false
}
fun showDialog(context: Context) {
return MaterialAlertDialogBuilder(context, R.style.MaterialAlertDialogTheme)
.setTitle(R.string.dialog_title_set_ringtone)
.setMessage(R.string.dialog_message_set_ringtone)
.setPositiveButton(android.R.string.ok) { _, _ ->
val intent = Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS)
intent.data = ("package:" + context.applicationContext.packageName).toUri()
context.startActivity(intent)
}
.setNegativeButton(android.R.string.cancel, null)
.create().show()
}
}

View file

@ -0,0 +1,319 @@
/*
* 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.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.UriPermission;
import android.net.Uri;
import android.os.Build;
import android.os.ParcelFileDescriptor;
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 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 io.github.muntashirakon.music.R;
import io.github.muntashirakon.music.activities.saf.SAFRequestActivity;
import code.name.monkey.retromusic.model.Song;
public class SAFUtil {
public static final String TAG = SAFUtil.class.getSimpleName();
public static final String SEPARATOR = "###/SAF/###";
public static final int REQUEST_SAF_PICK_FILE = 42;
public static final int REQUEST_SAF_PICK_TREE = 43;
public static boolean isSAFRequired(File file) {
return !file.canWrite();
}
public static boolean isSAFRequired(String path) {
return isSAFRequired(new File(path));
}
public static boolean isSAFRequired(AudioFile audio) {
return isSAFRequired(audio.getFile());
}
public static boolean isSAFRequired(Song song) {
return isSAFRequired(song.getData());
}
public static boolean isSAFRequired(List<String> paths) {
for (String path : paths) {
if (isSAFRequired(path)) return true;
}
return false;
}
public static boolean isSAFRequiredForSongs(List<Song> songs) {
for (Song song : songs) {
if (isSAFRequired(song)) return true;
}
return false;
}
@TargetApi(Build.VERSION_CODES.KITKAT)
public static void openFilePicker(Activity activity) {
Intent i = new Intent(Intent.ACTION_CREATE_DOCUMENT);
i.addCategory(Intent.CATEGORY_OPENABLE);
i.setType("audio/*");
i.putExtra("android.content.extra.SHOW_ADVANCED", true);
activity.startActivityForResult(i, SAFUtil.REQUEST_SAF_PICK_FILE);
}
@TargetApi(Build.VERSION_CODES.KITKAT)
public static void openFilePicker(Fragment fragment) {
Intent i = new Intent(Intent.ACTION_CREATE_DOCUMENT);
i.addCategory(Intent.CATEGORY_OPENABLE);
i.setType("audio/*");
i.putExtra("android.content.extra.SHOW_ADVANCED", true);
fragment.startActivityForResult(i, SAFUtil.REQUEST_SAF_PICK_FILE);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static void openTreePicker(Activity activity) {
Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
i.putExtra("android.content.extra.SHOW_ADVANCED", true);
activity.startActivityForResult(i, SAFUtil.REQUEST_SAF_PICK_TREE);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static void openTreePicker(Fragment fragment) {
Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
i.putExtra("android.content.extra.SHOW_ADVANCED", true);
fragment.startActivityForResult(i, SAFUtil.REQUEST_SAF_PICK_TREE);
}
@TargetApi(Build.VERSION_CODES.KITKAT)
public static void saveTreeUri(Context context, Intent data) {
Uri uri = data.getData();
if (uri != null) {
context.getContentResolver().takePersistableUriPermission(
uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
PreferenceUtil.INSTANCE.setSafSdCardUri(uri.toString());
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static boolean isTreeUriSaved() {
return !TextUtils.isEmpty(PreferenceUtil.INSTANCE.getSafSdCardUri());
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static boolean isSDCardAccessGranted(Context context) {
if (!isTreeUriSaved()) return false;
String sdcardUri = PreferenceUtil.INSTANCE.getSafSdCardUri();
List<UriPermission> perms = context.getContentResolver().getPersistedUriPermissions();
for (UriPermission perm : perms) {
if (perm.getUri().toString().equals(sdcardUri) && perm.isWritePermission()) return true;
}
return false;
}
/**
* https://github.com/vanilla-music/vanilla-music-tag-editor/commit/e00e87fef289f463b6682674aa54be834179ccf0#diff-d436417358d5dfbb06846746d43c47a5R359
* Finds needed file through Document API for SAF. It's not optimized yet - you can still gain
* wrong URI on files such as "/a/b/c.mp3" and "/b/a/c.mp3", but I consider it complete enough to
* be usable.
*
* @param dir - document file representing current dir of search
* @param segments - path segments that are left to find
* @return URI for found file. Null if nothing found.
*/
@Nullable
public static Uri findDocument(DocumentFile dir, List<String> segments) {
if (dir == null) {
return null;
}
for (DocumentFile file : dir.listFiles()) {
int index = segments.indexOf(file.getName());
if (index == -1) {
continue;
}
if (file.isDirectory()) {
segments.remove(file.getName());
return findDocument(file, segments);
}
if (file.isFile() && index == segments.size() - 1) {
// got to the last part
return file.getUri();
}
}
return null;
}
public static void write(Context context, AudioFile audio, Uri safUri) {
if (isSAFRequired(audio)) {
writeSAF(context, audio, safUri);
} else {
try {
writeFile(audio);
} catch (CannotWriteException e) {
e.printStackTrace();
}
}
}
public static void writeFile(AudioFile audio) throws CannotWriteException {
audio.commit();
}
public static void writeSAF(Context context, AudioFile audio, Uri safUri) {
Uri uri = null;
if (context == null) {
Log.e(TAG, "writeSAF: context == null");
return;
}
if (isTreeUriSaved()) {
List<String> pathSegments =
new ArrayList<>(Arrays.asList(audio.getFile().getAbsolutePath().split("/")));
Uri sdcard = Uri.parse(PreferenceUtil.INSTANCE.getSafSdCardUri());
uri = findDocument(DocumentFile.fromTreeUri(context, sdcard), pathSegments);
}
if (uri == null) {
uri = safUri;
}
if (uri == null) {
Log.e(TAG, "writeSAF: Can't get SAF URI");
toast(context, context.getString(R.string.saf_error_uri));
return;
}
try {
// copy file to app folder to use jaudiotagger
final File original = audio.getFile();
File temp = File.createTempFile("tmp-media", '.' + Utils.getExtension(original));
Utils.copy(original, temp);
temp.deleteOnExit();
audio.setFile(temp);
writeFile(audio);
ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "rw");
if (pfd == null) {
Log.e(TAG, "writeSAF: SAF provided incorrect URI: " + uri);
return;
}
// now read persisted data and write it to real FD provided by SAF
FileInputStream fis = new FileInputStream(temp);
byte[] audioContent = FileUtil.readBytes(fis);
FileOutputStream fos = new FileOutputStream(pfd.getFileDescriptor());
fos.write(audioContent);
fos.close();
temp.delete();
} catch (final Exception e) {
Log.e(TAG, "writeSAF: Failed to write to file descriptor provided by SAF", e);
toast(
context,
String.format(context.getString(R.string.saf_write_failed), e.getLocalizedMessage()));
}
}
public static boolean delete(Context context, String path, Uri safUri) {
if (isSAFRequired(path)) {
return deleteUsingSAF(context, path, safUri);
} else {
try {
deleteFile(path);
} catch (NullPointerException e) {
Log.e("MusicUtils", "Failed to find file " + path);
} catch (Exception e) {
e.printStackTrace();
}
}
return false;
}
public static void deleteFile(String path) {
new File(path).delete();
}
@TargetApi(Build.VERSION_CODES.KITKAT)
private static boolean deleteUsingSAF(Context context, String path, Uri safUri) {
if (context == null) {
Log.e(TAG, "deleteSAF: context == null");
return false;
}
if (safUri == null && isTreeUriSaved()) {
List<String> pathSegments = new ArrayList<>(Arrays.asList(path.split("/")));
Uri sdcard = Uri.parse(PreferenceUtil.INSTANCE.getSafSdCardUri());
safUri = findDocument(DocumentFile.fromTreeUri(context, sdcard), pathSegments);
}
if (safUri == null) {
requestSAF(context);
toast(context, context.getString(R.string.saf_error_uri));
return false;
}
try {
DocumentsContract.deleteDocument(context.getContentResolver(), safUri);
} catch (final Exception e) {
Log.e(TAG, "deleteSAF: Failed to delete a file descriptor provided by SAF", e);
toast(
context,
String.format(context.getString(R.string.saf_delete_failed), e.getLocalizedMessage()));
return false;
}
return true;
}
private static void requestSAF(Context context) {
Intent intent = new Intent(context, SAFRequestActivity.class);
context.startActivity(intent);
}
private static void toast(final Context context, final String message) {
if (context instanceof Activity) {
((Activity) context)
.runOnUiThread(() -> Toast.makeText(context, message, Toast.LENGTH_SHORT).show());
}
}
}

View file

@ -0,0 +1,46 @@
/*
* Copyright (c) 2020 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.content.Context
import android.content.Intent
import android.net.Uri
import androidx.core.content.FileProvider
import java.io.File
/**
* Created by hemanths on 2020-02-02.
*/
object Share {
fun shareStoryToSocial(context: Context, uri: Uri) {
val feedIntent = Intent(Intent.ACTION_SEND)
feedIntent.type = "image/*"
feedIntent.putExtra(Intent.EXTRA_STREAM, uri)
context.startActivity(feedIntent, null)
}
fun shareFile(context: Context, file: File) {
val attachmentUri = FileProvider.getUriForFile(
context,
context.applicationContext.packageName,
file
)
val sharingIntent = Intent(Intent.ACTION_SEND)
sharingIntent.type = "text/*"
sharingIntent.putExtra(Intent.EXTRA_STREAM, attachmentUri)
context.startActivity(Intent.createChooser(sharingIntent, "send bug report"))
}
}

View file

@ -0,0 +1,73 @@
/*
* 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.graphics.Canvas;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
public class SwipeAndDragHelper extends ItemTouchHelper.Callback {
private final ActionCompletionContract contract;
public SwipeAndDragHelper(@NonNull ActionCompletionContract contract) {
this.contract = contract;
}
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
return makeMovementFlags(dragFlags, 0);
}
@Override
public boolean onMove(
@NonNull RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder,
RecyclerView.ViewHolder target) {
contract.onViewMoved(viewHolder.getLayoutPosition(), target.getLayoutPosition());
return true;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {}
@Override
public boolean isLongPressDragEnabled() {
return false;
}
@Override
public void onChildDraw(
@NonNull Canvas c,
@NonNull RecyclerView recyclerView,
@NonNull RecyclerView.ViewHolder viewHolder,
float dX,
float dY,
int actionState,
boolean isCurrentlyActive) {
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
float alpha = 1 - (Math.abs(dX) / recyclerView.getWidth());
viewHolder.itemView.setAlpha(alpha);
}
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
public interface ActionCompletionContract {
void onViewMoved(int oldPosition, int newPosition);
}
}

View file

@ -0,0 +1,80 @@
/*
* 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;
/** @author Hemanth S (h4h13). */
public class TempUtils {
// Enums
public static final int TEMPO_STROLL = 0;
public static final int TEMPO_WALK = 1;
public static final int TEMPO_LIGHT_JOG = 2;
public static final int TEMPO_JOG = 3;
public static final int TEMPO_RUN = 4;
public static final int TEMPO_SPRINT = 5;
public static final int TEMPO_UNKNOWN = 6;
// take BPM as an int
public static int getTempoFromBPM(int bpm) {
// STROLL less than 60
if (bpm < 60) {
return TEMPO_STROLL;
}
// WALK between 60 and 70, or between 120 and 140
else if (bpm < 70 || bpm >= 120 && bpm < 140) {
return TEMPO_WALK;
}
// LIGHT_JOG between 70 and 80, or between 140 and 160
else if (bpm < 80 || bpm >= 140 && bpm < 160) {
return TEMPO_LIGHT_JOG;
}
// JOG between 80 and 90, or between 160 and 180
else if (bpm < 90 || bpm >= 160 && bpm < 180) {
return TEMPO_JOG;
}
// RUN between 90 and 100, or between 180 and 200
else if (bpm < 100 || bpm >= 180 && bpm < 200) {
return TEMPO_RUN;
}
// SPRINT between 100 and 120
else if (bpm < 120) {
return TEMPO_SPRINT;
}
// UNKNOWN
else {
return TEMPO_UNKNOWN;
}
}
// take BPM as a string
public static int getTempoFromBPM(String bpm) {
// cast to an int from string
try {
// convert the string to an int
return getTempoFromBPM(Integer.parseInt(bpm.trim()));
} catch (NumberFormatException nfe) {
//
return TEMPO_UNKNOWN;
}
}
}

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2020 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.view.ViewGroup
import code.name.monkey.appthemehelper.ThemeStore.Companion.accentColor
import code.name.monkey.appthemehelper.util.ColorUtil.isColorLight
import code.name.monkey.appthemehelper.util.MaterialValueHelper.getPrimaryTextColor
import code.name.monkey.appthemehelper.util.TintHelper
import code.name.monkey.retromusic.views.PopupBackground
import me.zhanghai.android.fastscroll.FastScroller
import me.zhanghai.android.fastscroll.FastScrollerBuilder
import me.zhanghai.android.fastscroll.PopupStyles
import me.zhanghai.android.fastscroll.R
object ThemedFastScroller {
fun create(view: ViewGroup): FastScroller {
val context = view.context
val color = accentColor(context)
val textColor = getPrimaryTextColor(context, isColorLight(color))
val fastScrollerBuilder = FastScrollerBuilder(view)
fastScrollerBuilder.useMd2Style()
fastScrollerBuilder.setPopupStyle { popupText ->
PopupStyles.MD2.accept(popupText)
popupText.background = PopupBackground(context, color)
popupText.setTextColor(textColor)
}
fastScrollerBuilder.setThumbDrawable(
TintHelper.createTintedDrawable(
context,
R.drawable.afs_md2_thumb,
color
)
)
return fastScrollerBuilder.build()
}
}

View file

@ -0,0 +1,26 @@
package code.name.monkey.retromusic.util
import android.content.ContentUris
import android.content.Context
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import androidx.annotation.RequiresApi
import code.name.monkey.retromusic.Constants
object UriUtil {
@RequiresApi(Build.VERSION_CODES.Q)
fun getUriFromPath(context: Context, path: String): Uri {
val uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
val proj = arrayOf(MediaStore.Files.FileColumns._ID)
context.contentResolver.query(
uri, proj, Constants.DATA + "=?", arrayOf(path), null
)?.use { cursor ->
if (cursor.count != 0) {
cursor.moveToFirst()
return ContentUris.withAppendedId(uri, cursor.getLong(0))
}
}
return Uri.EMPTY
}
}

View file

@ -0,0 +1,93 @@
/*
* 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.content.res.ColorStateList
import android.content.res.Resources
import android.graphics.drawable.LayerDrawable
import android.os.Build
import android.view.View
import android.widget.ProgressBar
import android.widget.SeekBar
import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat.SRC_IN
import code.name.monkey.appthemehelper.util.ATHUtil
import code.name.monkey.appthemehelper.util.ColorUtil
import code.name.monkey.appthemehelper.util.MaterialValueHelper
object ViewUtil {
const val RETRO_MUSIC_ANIM_TIME = 1000
fun setProgressDrawable(progressSlider: SeekBar, newColor: Int, thumbTint: Boolean = false) {
if (thumbTint) {
progressSlider.thumbTintList = ColorStateList.valueOf(newColor)
}
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) {
val layerDrawable = progressSlider.progressDrawable as LayerDrawable
val progressDrawable = layerDrawable.findDrawableByLayerId(android.R.id.progress)
progressDrawable.colorFilter =
BlendModeColorFilterCompat.createBlendModeColorFilterCompat(newColor, SRC_IN)
} else {
progressSlider.progressTintList = ColorStateList.valueOf(newColor)
}
}
fun setProgressDrawable(progressSlider: ProgressBar, newColor: Int) {
val layerDrawable = progressSlider.progressDrawable as LayerDrawable
val progress = layerDrawable.findDrawableByLayerId(android.R.id.progress)
progress.colorFilter =
BlendModeColorFilterCompat.createBlendModeColorFilterCompat(newColor, SRC_IN)
val background = layerDrawable.findDrawableByLayerId(android.R.id.background)
val primaryColor =
ATHUtil.resolveColor(progressSlider.context, android.R.attr.windowBackground)
background.colorFilter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(
MaterialValueHelper.getPrimaryDisabledTextColor(
progressSlider.context,
ColorUtil.isColorLight(primaryColor)
), SRC_IN
)
val secondaryProgress = layerDrawable.findDrawableByLayerId(android.R.id.secondaryProgress)
secondaryProgress?.colorFilter =
BlendModeColorFilterCompat.createBlendModeColorFilterCompat(
ColorUtil.withAlpha(
newColor,
0.65f
), SRC_IN
)
}
fun hitTest(v: View, x: Int, y: Int): Boolean {
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
val bottom = v.bottom + ty
return x in left..right && y >= top && y <= bottom
}
fun convertDpToPixel(dp: Float, resources: Resources): Float {
val metrics = resources.displayMetrics
return dp * metrics.density
}
}

View file

@ -0,0 +1,140 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package code.name.monkey.retromusic.util.color;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
/**
* Utility class for image analysis and processing.
*
* @hide
*/
public class ImageUtils {
// Amount (max is 255) that two channels can differ before the color is no longer "gray".
private static final int TOLERANCE = 20;
// Alpha amount for which values below are considered transparent.
private static final int ALPHA_TOLERANCE = 50;
// Size of the smaller bitmap we're actually going to scan.
private static final int COMPACT_BITMAP_SIZE = 64; // pixels
private final Matrix mTempMatrix = new Matrix();
private int[] mTempBuffer;
private Bitmap mTempCompactBitmap;
private Canvas mTempCompactBitmapCanvas;
private Paint mTempCompactBitmapPaint;
/**
* Classifies a color as grayscale or not. Grayscale here means "very close to a perfect gray"; if
* all three channels are approximately equal, this will return true.
*
* <p>Note that really transparent colors are always grayscale.
*/
public static boolean isGrayscale(int color) {
int alpha = 0xFF & (color >> 24);
if (alpha < ALPHA_TOLERANCE) {
return true;
}
int r = 0xFF & (color >> 16);
int g = 0xFF & (color >> 8);
int b = 0xFF & color;
return Math.abs(r - g) < TOLERANCE
&& Math.abs(r - b) < TOLERANCE
&& Math.abs(g - b) < TOLERANCE;
}
/** Convert a drawable to a bitmap, scaled to fit within maxWidth and maxHeight. */
public static Bitmap buildScaledBitmap(Drawable drawable, int maxWidth, int maxHeight) {
if (drawable == null) {
return null;
}
int originalWidth = drawable.getIntrinsicWidth();
int originalHeight = drawable.getIntrinsicHeight();
if ((originalWidth <= maxWidth)
&& (originalHeight <= maxHeight)
&& (drawable instanceof BitmapDrawable)) {
return ((BitmapDrawable) drawable).getBitmap();
}
if (originalHeight <= 0 || originalWidth <= 0) {
return null;
}
// create a new bitmap, scaling down to fit the max dimensions of
// a large notification icon if necessary
float ratio =
Math.min(
(float) maxWidth / (float) originalWidth, (float) maxHeight / (float) originalHeight);
ratio = Math.min(1.0f, ratio);
int scaledWidth = (int) (ratio * originalWidth);
int scaledHeight = (int) (ratio * originalHeight);
Bitmap result = Bitmap.createBitmap(scaledWidth, scaledHeight, Config.ARGB_8888);
// and paint our app bitmap on it
Canvas canvas = new Canvas(result);
drawable.setBounds(0, 0, scaledWidth, scaledHeight);
drawable.draw(canvas);
return result;
}
/**
* Checks whether a bitmap is grayscale. Grayscale here means "very close to a perfect gray".
*
* <p>Instead of scanning every pixel in the bitmap, we first resize the bitmap to no more than
* COMPACT_BITMAP_SIZE^2 pixels using filtering. The hope is that any non-gray color elements will
* survive the squeezing process, contaminating the result with color.
*/
public boolean isGrayscale(Bitmap bitmap) {
int height = bitmap.getHeight();
int width = bitmap.getWidth();
// shrink to a more manageable (yet hopefully no more or less colorful) size
if (height > COMPACT_BITMAP_SIZE || width > COMPACT_BITMAP_SIZE) {
if (mTempCompactBitmap == null) {
mTempCompactBitmap =
Bitmap.createBitmap(COMPACT_BITMAP_SIZE, COMPACT_BITMAP_SIZE, Config.ARGB_8888);
mTempCompactBitmapCanvas = new Canvas(mTempCompactBitmap);
mTempCompactBitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTempCompactBitmapPaint.setFilterBitmap(true);
}
mTempMatrix.reset();
mTempMatrix.setScale(
(float) COMPACT_BITMAP_SIZE / width, (float) COMPACT_BITMAP_SIZE / height, 0, 0);
mTempCompactBitmapCanvas.drawColor(0, PorterDuff.Mode.SRC); // select all, erase
mTempCompactBitmapCanvas.drawBitmap(bitmap, mTempMatrix, mTempCompactBitmapPaint);
bitmap = mTempCompactBitmap;
width = height = COMPACT_BITMAP_SIZE;
}
final int size = height * width;
ensureBufferSize(size);
bitmap.getPixels(mTempBuffer, 0, width, 0, 0, width, height);
for (int i = 0; i < size; i++) {
if (!isGrayscale(mTempBuffer[i])) {
return false;
}
}
return true;
}
/** Makes sure that {@code mTempBuffer} has at least length {@code size}. */
private void ensureBufferSize(int size) {
if (mTempBuffer == null || mTempBuffer.length < size) {
mTempBuffer = new int[size];
}
}
}

View file

@ -0,0 +1,485 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package code.name.monkey.retromusic.util.color;
import static androidx.core.graphics.ColorUtils.RGBToXYZ;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
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 io.github.muntashirakon.music.R;
/** A class the processes media notifications and extracts the right text and background colors. */
public class MediaNotificationProcessor {
/** The fraction below which we select the vibrant instead of the light/dark vibrant color */
private static final float POPULATION_FRACTION_FOR_MORE_VIBRANT = 1.0f;
/**
* Minimum saturation that a muted color must have if there exists if deciding between two colors
*/
private static final float MIN_SATURATION_WHEN_DECIDING = 0.19f;
/** Minimum fraction that any color must have to be picked up as a text color */
private static final double MINIMUM_IMAGE_FRACTION = 0.002;
/**
* The population fraction to select the dominant color as the text color over a the colored ones.
*/
private static final float POPULATION_FRACTION_FOR_DOMINANT = 0.01f;
/** The population fraction to select a white or black color as the background over a color. */
private static final float POPULATION_FRACTION_FOR_WHITE_OR_BLACK = 2.5f;
private static final float BLACK_MAX_LIGHTNESS = 0.08f;
private static final float WHITE_MIN_LIGHTNESS = 0.90f;
private static final int RESIZE_BITMAP_AREA = 150 * 150;
private static final ThreadLocal<double[]> TEMP_ARRAY = new ThreadLocal<>();
/**
* The lightness difference that has to be added to the primary text color to obtain the secondary
* text color when the background is light.
*/
private static final int LIGHTNESS_TEXT_DIFFERENCE_LIGHT = 20;
/**
* The lightness difference that has to be added to the primary text color to obtain the secondary
* text color when the background is dark. A bit less then the above value, since it looks better
* on dark backgrounds.
*/
private static final int LIGHTNESS_TEXT_DIFFERENCE_DARK = -10;
private float[] mFilteredBackgroundHsl = null;
private final Palette.Filter mBlackWhiteFilter =
(rgb, hsl) -> !isWhiteOrBlack(hsl);
private int backgroundColor;
private int secondaryTextColor;
private int primaryTextColor;
private int actionBarColor;
private Drawable drawable;
private final Context context;
public MediaNotificationProcessor(Context context, Drawable drawable) {
this.context = context;
this.drawable = drawable;
getMediaPalette();
}
public MediaNotificationProcessor(Context context, Bitmap bitmap) {
this.context = context;
this.drawable = new BitmapDrawable(context.getResources(), bitmap);
getMediaPalette();
}
public MediaNotificationProcessor(Context context) {
this.context = context;
}
private static boolean isColorLight(int backgroundColor) {
return calculateLuminance(backgroundColor) > 0.5f;
}
/**
* Returns the luminance of a color as a float between {@code 0.0} and {@code 1.0}.
*
* <p>Defined as the Y component in the XYZ representation of {@code color}.
*/
@FloatRange(from = 0.0, to = 1.0)
private static double calculateLuminance(@ColorInt int color) {
final double[] result = getTempDouble3Array();
colorToXYZ(color, result);
// Luminance is the Y component
return result[1] / 100;
}
private static double[] getTempDouble3Array() {
double[] result = TEMP_ARRAY.get();
if (result == null) {
result = new double[3];
TEMP_ARRAY.set(result);
}
return result;
}
private static void colorToXYZ(@ColorInt int color, @NonNull double[] outXyz) {
RGBToXYZ(Color.red(color), Color.green(color), Color.blue(color), outXyz);
}
public void getPaletteAsync(
final OnPaletteLoadedListener onPaletteLoadedListener, Drawable drawable) {
this.drawable = drawable;
final Handler handler = new Handler();
new Thread(
() -> {
getMediaPalette();
handler.post(
() -> onPaletteLoadedListener.onPaletteLoaded(MediaNotificationProcessor.this));
})
.start();
}
public void getPaletteAsync(OnPaletteLoadedListener onPaletteLoadedListener, Bitmap bitmap) {
this.drawable = new BitmapDrawable(context.getResources(), bitmap);
getPaletteAsync(onPaletteLoadedListener, this.drawable);
}
/** Processes a drawable and calculates the appropriate colors that should be used. */
private void getMediaPalette() {
Bitmap bitmap;
if (drawable != null) {
// We're transforming the builder, let's make sure all baked in RemoteViews are
// rebuilt!
int width = drawable.getIntrinsicWidth();
int height = drawable.getIntrinsicHeight();
int area = width * height;
if (area > RESIZE_BITMAP_AREA) {
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);
// 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);
}
}
private int selectForegroundColor(int backgroundColor, Palette palette) {
if (isColorLight(backgroundColor)) {
return selectForegroundColorForSwatches(
palette.getDarkVibrantSwatch(),
palette.getVibrantSwatch(),
palette.getDarkMutedSwatch(),
palette.getMutedSwatch(),
palette.getDominantSwatch(),
Color.BLACK);
} else {
return selectForegroundColorForSwatches(
palette.getLightVibrantSwatch(),
palette.getVibrantSwatch(),
palette.getLightMutedSwatch(),
palette.getMutedSwatch(),
palette.getDominantSwatch(),
Color.WHITE);
}
}
public boolean isLight() {
return isColorLight(backgroundColor);
}
private int selectForegroundColorForSwatches(
Palette.Swatch moreVibrant,
Palette.Swatch vibrant,
Palette.Swatch moreMutedSwatch,
Palette.Swatch mutedSwatch,
Palette.Swatch dominantSwatch,
int fallbackColor) {
Palette.Swatch coloredCandidate = selectVibrantCandidate(moreVibrant, vibrant);
if (coloredCandidate == null) {
coloredCandidate = selectMutedCandidate(mutedSwatch, moreMutedSwatch);
}
if (coloredCandidate != null) {
if (dominantSwatch == coloredCandidate) {
return coloredCandidate.getRgb();
} else if ((float) coloredCandidate.getPopulation() / dominantSwatch.getPopulation()
< POPULATION_FRACTION_FOR_DOMINANT
&& dominantSwatch.getHsl()[1] > MIN_SATURATION_WHEN_DECIDING) {
return dominantSwatch.getRgb();
} else {
return coloredCandidate.getRgb();
}
} else if (hasEnoughPopulation(dominantSwatch)) {
return dominantSwatch.getRgb();
} else {
return fallbackColor;
}
}
private Palette.Swatch selectMutedCandidate(Palette.Swatch first, Palette.Swatch second) {
boolean firstValid = hasEnoughPopulation(first);
boolean secondValid = hasEnoughPopulation(second);
if (firstValid && secondValid) {
float firstSaturation = first.getHsl()[1];
float secondSaturation = second.getHsl()[1];
float populationFraction = first.getPopulation() / (float) second.getPopulation();
if (firstSaturation * populationFraction > secondSaturation) {
return first;
} else {
return second;
}
} else if (firstValid) {
return first;
} else if (secondValid) {
return second;
}
return null;
}
private Palette.Swatch selectVibrantCandidate(Palette.Swatch first, Palette.Swatch second) {
boolean firstValid = hasEnoughPopulation(first);
boolean secondValid = hasEnoughPopulation(second);
if (firstValid && secondValid) {
int firstPopulation = first.getPopulation();
int secondPopulation = second.getPopulation();
if (firstPopulation / (float) secondPopulation < POPULATION_FRACTION_FOR_MORE_VIBRANT) {
return second;
} else {
return first;
}
} else if (firstValid) {
return first;
} else if (secondValid) {
return second;
}
return null;
}
private boolean hasEnoughPopulation(Palette.Swatch swatch) {
// We want a fraction that is at least 1% of the image
return swatch != null
&& (swatch.getPopulation() / (float) RESIZE_BITMAP_AREA > MINIMUM_IMAGE_FRACTION);
}
public int findBackgroundColorAndFilter(Drawable drawable) {
int width = drawable.getIntrinsicWidth();
int height = drawable.getIntrinsicHeight();
int area = width * height;
double factor = Math.sqrt((float) RESIZE_BITMAP_AREA / area);
width = (int) (factor * width);
height = (int) (factor * height);
Bitmap 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();
// by default we use the dominant palette
Palette.Swatch dominantSwatch = palette.getDominantSwatch();
if (dominantSwatch == null) {
// We're not filtering on white or black
mFilteredBackgroundHsl = null;
return Color.WHITE;
}
if (!isWhiteOrBlack(dominantSwatch.getHsl())) {
mFilteredBackgroundHsl = dominantSwatch.getHsl();
return dominantSwatch.getRgb();
}
// Oh well, we selected black or white. Lets look at the second color!
List<Palette.Swatch> swatches = palette.getSwatches();
float highestNonWhitePopulation = -1;
Palette.Swatch second = null;
for (Palette.Swatch swatch : swatches) {
if (swatch != dominantSwatch
&& swatch.getPopulation() > highestNonWhitePopulation
&& !isWhiteOrBlack(swatch.getHsl())) {
second = swatch;
highestNonWhitePopulation = swatch.getPopulation();
}
}
if (second == null) {
// We're not filtering on white or black
mFilteredBackgroundHsl = null;
return dominantSwatch.getRgb();
}
if (dominantSwatch.getPopulation() / highestNonWhitePopulation
> POPULATION_FRACTION_FOR_WHITE_OR_BLACK) {
// The dominant swatch is very dominant, lets take it!
// We're not filtering on white or black
mFilteredBackgroundHsl = null;
return dominantSwatch.getRgb();
} else {
mFilteredBackgroundHsl = second.getHsl();
return second.getRgb();
}
}
private boolean isWhiteOrBlack(float[] hsl) {
return isBlack(hsl) || isWhite(hsl);
}
/** @return true if the color represents a color which is close to black. */
private boolean isBlack(float[] hslColor) {
return hslColor[2] <= BLACK_MAX_LIGHTNESS;
}
/** @return true if the color represents a color which is close to white. */
private boolean isWhite(float[] hslColor) {
return hslColor[2] >= WHITE_MIN_LIGHTNESS;
}
private void ensureColors(int backgroundColor, int mForegroundColor) {
{
double backLum = NotificationColorUtil.calculateLuminance(backgroundColor);
double textLum = NotificationColorUtil.calculateLuminance(mForegroundColor);
double contrast = NotificationColorUtil.calculateContrast(mForegroundColor, backgroundColor);
// We only respect the given colors if worst case Black or White still has
// contrast
boolean backgroundLight =
backLum > textLum
&& NotificationColorUtil.satisfiesTextContrast(backgroundColor, Color.BLACK)
|| backLum <= textLum
&& !NotificationColorUtil.satisfiesTextContrast(backgroundColor, Color.WHITE);
if (contrast < 4.5f) {
if (backgroundLight) {
secondaryTextColor =
NotificationColorUtil.findContrastColor(
mForegroundColor, backgroundColor, true /* findFG */, 4.5f);
primaryTextColor =
NotificationColorUtil.changeColorLightness(
secondaryTextColor, -LIGHTNESS_TEXT_DIFFERENCE_LIGHT);
} else {
secondaryTextColor =
NotificationColorUtil.findContrastColorAgainstDark(
mForegroundColor, backgroundColor, true /* findFG */, 4.5f);
primaryTextColor =
NotificationColorUtil.changeColorLightness(
secondaryTextColor, -LIGHTNESS_TEXT_DIFFERENCE_DARK);
}
} else {
primaryTextColor = mForegroundColor;
secondaryTextColor =
NotificationColorUtil.changeColorLightness(
primaryTextColor,
backgroundLight ? LIGHTNESS_TEXT_DIFFERENCE_LIGHT : LIGHTNESS_TEXT_DIFFERENCE_DARK);
if (NotificationColorUtil.calculateContrast(secondaryTextColor, backgroundColor) < 4.5f) {
// oh well the secondary is not good enough
if (backgroundLight) {
secondaryTextColor =
NotificationColorUtil.findContrastColor(
secondaryTextColor, backgroundColor, true /* findFG */, 4.5f);
} else {
secondaryTextColor =
NotificationColorUtil.findContrastColorAgainstDark(
secondaryTextColor, backgroundColor, true /* findFG */, 4.5f);
}
primaryTextColor =
NotificationColorUtil.changeColorLightness(
secondaryTextColor,
backgroundLight
? -LIGHTNESS_TEXT_DIFFERENCE_LIGHT
: -LIGHTNESS_TEXT_DIFFERENCE_DARK);
}
}
}
actionBarColor = NotificationColorUtil.resolveActionBarColor(context, backgroundColor);
}
public int getPrimaryTextColor() {
return primaryTextColor;
}
public int getSecondaryTextColor() {
return secondaryTextColor;
}
public int getActionBarColor() {
return actionBarColor;
}
public int getBackgroundColor() {
return backgroundColor;
}
boolean isWhiteColor(int color) {
return calculateLuminance(color) > 0.6f;
}
public int getMightyColor() {
boolean isDarkBg =
ColorUtil.INSTANCE.isColorLight(
ATHUtil.INSTANCE.resolveColor(context, R.attr.colorSurface));
if (isDarkBg) {
if (isColorLight(backgroundColor)) {
return primaryTextColor;
} else {
return backgroundColor;
}
} else {
if (isColorLight(backgroundColor)) {
return backgroundColor;
} else {
return primaryTextColor;
}
}
}
public interface OnPaletteLoadedListener {
void onPaletteLoaded(MediaNotificationProcessor mediaNotificationProcessor);
}
public static MediaNotificationProcessor errorColor(Context context) {
MediaNotificationProcessor errorColors = new MediaNotificationProcessor(context);
errorColors.backgroundColor = -15724528;
errorColors.primaryTextColor = -6974059;
errorColors.secondaryTextColor = -8684677;
errorColors.actionBarColor = -6974059;
return errorColors;
}
}

View file

@ -0,0 +1,992 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package code.name.monkey.retromusic.util.color;
import android.app.Notification;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.BackgroundColorSpan;
import android.text.style.CharacterStyle;
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 java.util.WeakHashMap;
import io.github.muntashirakon.music.R;
/**
* Helper class to process legacy (Holo) notifications to make them look like material
* notifications.
*
* @hide
*/
public class NotificationColorUtil {
private static final String TAG = "NotificationColorUtil";
private static final boolean DEBUG = false;
private static final Object sLock = new Object();
private static NotificationColorUtil sInstance;
private final ImageUtils mImageUtils = new ImageUtils();
private final WeakHashMap<Bitmap, Pair<Boolean, Integer>> mGrayscaleBitmapCache =
new WeakHashMap<>();
private final int mGrayscaleIconMaxSize; // @dimen/notification_large_icon_width (64dp)
private NotificationColorUtil(Context context) {
mGrayscaleIconMaxSize =
context.getResources().getDimensionPixelSize(R.dimen.notification_large_icon_width);
}
public static NotificationColorUtil getInstance(Context context) {
synchronized (sLock) {
if (sInstance == null) {
sInstance = new NotificationColorUtil(context);
}
return sInstance;
}
}
/**
* Clears all color spans of a text
*
* @param charSequence the input text
* @return the same text but without color spans
*/
public static CharSequence clearColorSpans(CharSequence charSequence) {
if (charSequence instanceof Spanned) {
Spanned ss = (Spanned) charSequence;
Object[] spans = ss.getSpans(0, ss.length(), Object.class);
SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString());
for (Object span : spans) {
Object resultSpan = span;
if (resultSpan instanceof CharacterStyle) {
resultSpan = ((CharacterStyle) span).getUnderlying();
}
if (resultSpan instanceof TextAppearanceSpan) {
TextAppearanceSpan originalSpan = (TextAppearanceSpan) resultSpan;
if (originalSpan.getTextColor() != null) {
resultSpan =
new TextAppearanceSpan(
originalSpan.getFamily(),
originalSpan.getTextStyle(),
originalSpan.getTextSize(),
null,
originalSpan.getLinkTextColor());
}
} else if (resultSpan instanceof ForegroundColorSpan
|| (resultSpan instanceof BackgroundColorSpan)) {
continue;
} else {
resultSpan = span;
}
builder.setSpan(
resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span), ss.getSpanFlags(span));
}
return builder;
}
return charSequence;
}
// /**
// * Inverts all the grayscale colors set by {@link android.text.style.TextAppearanceSpan}s on
// * the text.
// *
// * @param charSequence The text to process.
// * @return The color inverted text.
// */
// public CharSequence invertCharSequenceColors(CharSequence charSequence) {
// if (charSequence instanceof Spanned) {
// Spanned ss = (Spanned) charSequence;
// Object[] spans = ss.getSpans(0, ss.length(), Object.class);
// SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString());
// for (Object span : spans) {
// Object resultSpan = span;
// if (resultSpan instanceof CharacterStyle) {
// resultSpan = ((CharacterStyle) span).getUnderlying();
// }
// if (resultSpan instanceof TextAppearanceSpan) {
// TextAppearanceSpan processedSpan = processTextAppearanceSpan(
// (TextAppearanceSpan) span);
// if (processedSpan != resultSpan) {
// resultSpan = processedSpan;
// } else {
// // we need to still take the orgininal for wrapped spans
// resultSpan = span;
// }
// } else if (resultSpan instanceof ForegroundColorSpan) {
// ForegroundColorSpan originalSpan = (ForegroundColorSpan) resultSpan;
// int foregroundColor = originalSpan.getForegroundColor();
// resultSpan = new ForegroundColorSpan(processColor(foregroundColor));
// } else {
// resultSpan = span;
// }
// builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span),
// ss.getSpanFlags(span));
// }
// return builder;
// }
// return charSequence;
// }
// private TextAppearanceSpan processTextAppearanceSpan(TextAppearanceSpan span) {
// ColorStateList colorStateList = span.getTextColor();
// if (colorStateList != null) {
// int[] colors = colorStateList.getColors();
// boolean changed = false;
// for (int i = 0; i < colors.length; i++) {
// if (ImageUtils.isGrayscale(colors[i])) {
//
// // Allocate a new array so we don't change the colors in the old color state
// // list.
// if (!changed) {
// colors = Arrays.copyOf(colors, colors.length);
// }
// colors[i] = processColor(colors[i]);
// changed = true;
// }
// }
// if (changed) {
// return new TextAppearanceSpan(
// span.getFamily(), span.getTextStyle(), span.getTextSize(),
// new ColorStateList(colorStateList.getStates(), colors),
// span.getLinkTextColor());
// }
// }
// return span;
// }
/**
* Finds a suitable color such that there's enough contrast.
*
* @param color the color to start searching from.
* @param other the color to ensure contrast against. Assumed to be lighter than {@param color}
* @param findFg if true, we assume {@param color} is a foreground, otherwise a background.
* @param minRatio the minimum contrast ratio required.
* @return a color with the same hue as {@param color}, potentially darkened to meet the contrast
* ratio.
*/
public static int findContrastColor(int color, int other, boolean findFg, double minRatio) {
int fg = findFg ? color : other;
int bg = findFg ? other : color;
if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) {
return color;
}
double[] lab = new double[3];
ColorUtilsFromCompat.colorToLAB(findFg ? fg : bg, lab);
double low = 0, high = lab[0];
final double a = lab[1], b = lab[2];
for (int i = 0; i < 15 && high - low > 0.00001; i++) {
final double l = (low + high) / 2;
if (findFg) {
fg = ColorUtilsFromCompat.LABToColor(l, a, b);
} else {
bg = ColorUtilsFromCompat.LABToColor(l, a, b);
}
if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) {
low = l;
} else {
high = l;
}
}
return ColorUtilsFromCompat.LABToColor(low, a, b);
}
/**
* Finds a suitable alpha such that there's enough contrast.
*
* @param color the color to start searching from.
* @param backgroundColor the color to ensure contrast against.
* @param minRatio the minimum contrast ratio required.
* @return the same color as {@param color} with potentially modified alpha to meet contrast
*/
public static int findAlphaToMeetContrast(int color, int backgroundColor, double minRatio) {
int fg = color;
int bg = backgroundColor;
if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) {
return color;
}
int startAlpha = Color.alpha(color);
int r = Color.red(color);
int g = Color.green(color);
int b = Color.blue(color);
int low = startAlpha, high = 255;
for (int i = 0; i < 15 && high - low > 0; i++) {
final int alpha = (low + high) / 2;
fg = Color.argb(alpha, r, g, b);
if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) {
high = alpha;
} else {
low = alpha;
}
}
return Color.argb(high, r, g, b);
}
/**
* Finds a suitable color such that there's enough contrast.
*
* @param color the color to start searching from.
* @param other the color to ensure contrast against. Assumed to be darker than {@param color}
* @param findFg if true, we assume {@param color} is a foreground, otherwise a background.
* @param minRatio the minimum contrast ratio required.
* @return a color with the same hue as {@param color}, potentially darkened to meet the contrast
* ratio.
*/
public static int findContrastColorAgainstDark(
int color, int other, boolean findFg, double minRatio) {
int fg = findFg ? color : other;
int bg = findFg ? other : color;
if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) {
return color;
}
float[] hsl = new float[3];
ColorUtilsFromCompat.colorToHSL(findFg ? fg : bg, hsl);
float low = hsl[2], high = 1;
for (int i = 0; i < 15 && high - low > 0.00001; i++) {
final float l = (low + high) / 2;
hsl[2] = l;
if (findFg) {
fg = ColorUtilsFromCompat.HSLToColor(hsl);
} else {
bg = ColorUtilsFromCompat.HSLToColor(hsl);
}
if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) {
high = l;
} else {
low = l;
}
}
return findFg ? fg : bg;
}
public static int ensureTextContrastOnBlack(int color) {
return findContrastColorAgainstDark(color, Color.BLACK, true /* fg */, 12);
}
/**
* Finds a large text color with sufficient contrast over bg that has the same or darker hue as
* the original color, depending on the value of {@code isBgDarker}.
*
* @param isBgDarker {@code true} if {@code bg} is darker than {@code color}.
*/
public static int ensureLargeTextContrast(int color, int bg, boolean isBgDarker) {
return isBgDarker
? findContrastColorAgainstDark(color, bg, true, 3)
: findContrastColor(color, bg, true, 3);
}
/**
* Finds a text color with sufficient contrast over bg that has the same or darker hue as the
* original color, depending on the value of {@code isBgDarker}.
*
* @param isBgDarker {@code true} if {@code bg} is darker than {@code color}.
*/
private static int ensureTextContrast(int color, int bg, boolean isBgDarker) {
return isBgDarker
? findContrastColorAgainstDark(color, bg, true, 4.5)
: findContrastColor(color, bg, true, 4.5);
}
/**
* Finds a background color for a text view with given text color and hint text color, that has
* the same hue as the original color.
*/
public static int ensureTextBackgroundColor(int color, int textColor, int hintColor) {
color = findContrastColor(color, hintColor, false, 3.0);
return findContrastColor(color, textColor, false, 4.5);
}
private static String contrastChange(int colorOld, int colorNew, int bg) {
return String.format(
"from %.2f:1 to %.2f:1",
ColorUtilsFromCompat.calculateContrast(colorOld, bg),
ColorUtilsFromCompat.calculateContrast(colorNew, bg));
}
/**
* Change a color by a specified value
*
* @param baseColor the base color to lighten
* @param amount the amount to lighten the color from 0 to 100. This corresponds to the L increase
* in the LAB color space. A negative value will darken the color and a positive will lighten
* it.
* @return the changed color
*/
public static int changeColorLightness(int baseColor, int amount) {
final double[] result = ColorUtilsFromCompat.getTempDouble3Array();
ColorUtilsFromCompat.colorToLAB(baseColor, result);
result[0] = Math.max(Math.min(100, result[0] + amount), 0);
return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]);
}
public static int resolvePrimaryColor(Context context, int backgroundColor) {
boolean useDark = shouldUseDark(backgroundColor);
return ContextCompat.getColor(context, android.R.color.primary_text_light);
}
public static int resolveSecondaryColor(Context context, int backgroundColor) {
boolean useDark = shouldUseDark(backgroundColor);
if (useDark) {
return ContextCompat.getColor(context, android.R.color.secondary_text_light);
} else {
return ContextCompat.getColor(context, android.R.color.secondary_text_dark);
}
}
public static int resolveActionBarColor(Context context, int backgroundColor) {
if (backgroundColor == Notification.COLOR_DEFAULT) {
return Color.BLACK;
}
return getShiftedColor(backgroundColor, 7);
}
/** Resolves {@param color} to an actual color if it is {@link Notification#COLOR_DEFAULT} */
public static int resolveColor(Context context, int color) {
if (color == Notification.COLOR_DEFAULT) {
return ContextCompat.getColor(context, android.R.color.background_dark);
}
return color;
}
//
// public static int resolveContrastColor(Context context, int notificationColor,
// int backgroundColor) {
// return NotificationColorUtil.resolveContrastColor(context, notificationColor,
// backgroundColor, false /* isDark */);
// }
// /**
// * Resolves a Notification's color such that it has enough contrast to be used as the
// * color for the Notification's action and header text.
// *
// * @param notificationColor the color of the notification or {@link
// Notification#COLOR_DEFAULT}
// * @param backgroundColor the background color to ensure the contrast against.
// * @param isDark whether or not the {@code notificationColor} will be placed on a background
// * that is darker than the color itself
// * @return a color of the same hue with enough contrast against the backgrounds.
// */
// public static int resolveContrastColor(Context context, int notificationColor,
// int backgroundColor, boolean isDark) {
// final int resolvedColor = resolveColor(context, notificationColor);
//
// final int actionBg = context.getColor(
// com.android.internal.R.color.notification_action_list);
//
// int color = resolvedColor;
// color = NotificationColorUtil.ensureLargeTextContrast(color, actionBg, isDark);
// color = NotificationColorUtil.ensureTextContrast(color, backgroundColor, isDark);
//
// if (color != resolvedColor) {
// if (DEBUG){
// Log.w(TAG, String.format(
// "Enhanced contrast of notification for %s %s (over action)"
// + " and %s (over background) by changing #%s to %s",
// context.getPackageName(),
// NotificationColorUtil.contrastChange(resolvedColor, color, actionBg),
// NotificationColorUtil.contrastChange(resolvedColor, color,
// backgroundColor),
// Integer.toHexString(resolvedColor), Integer.toHexString(color)));
// }
// }
// return color;
// }
/**
* Get a color that stays in the same tint, but darkens or lightens it by a certain amount. This
* also looks at the lightness of the provided color and shifts it appropriately.
*
* @param color the base color to use
* @param amount the amount from 1 to 100 how much to modify the color
* @return the now color that was modified
*/
public static int getShiftedColor(int color, int amount) {
final double[] result = ColorUtilsFromCompat.getTempDouble3Array();
ColorUtilsFromCompat.colorToLAB(color, result);
if (result[0] >= 4) {
result[0] = Math.max(0, result[0] - amount);
} else {
result[0] = Math.min(100, result[0] + amount);
}
return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]);
}
public static int resolveAmbientColor(Context context, int notificationColor) {
final int resolvedColor = resolveColor(context, notificationColor);
int color = resolvedColor;
color = NotificationColorUtil.ensureTextContrastOnBlack(color);
if (color != resolvedColor) {
if (DEBUG) {
Log.w(
TAG,
String.format(
"Ambient contrast of notification for %s is %s (over black)"
+ " by changing #%s to #%s",
context.getPackageName(),
NotificationColorUtil.contrastChange(resolvedColor, color, Color.BLACK),
Integer.toHexString(resolvedColor),
Integer.toHexString(color)));
}
}
return color;
}
private static boolean shouldUseDark(int backgroundColor) {
boolean useDark = backgroundColor == Notification.COLOR_DEFAULT;
if (!useDark) {
useDark = ColorUtilsFromCompat.calculateLuminance(backgroundColor) > 0.5;
}
return useDark;
}
public static double calculateLuminance(int backgroundColor) {
return ColorUtilsFromCompat.calculateLuminance(backgroundColor);
}
public static double calculateContrast(int foregroundColor, int backgroundColor) {
return ColorUtilsFromCompat.calculateContrast(foregroundColor, backgroundColor);
}
public static boolean satisfiesTextContrast(int backgroundColor, int foregroundColor) {
return NotificationColorUtil.calculateContrast(foregroundColor, backgroundColor) >= 4.5;
}
/** Composite two potentially translucent colors over each other and returns the result. */
public static int compositeColors(int foreground, int background) {
return ColorUtilsFromCompat.compositeColors(foreground, background);
}
public static boolean isColorLight(int backgroundColor) {
return calculateLuminance(backgroundColor) > 0.5f;
}
/**
* Checks whether a Bitmap is a small grayscale icon. Grayscale here means "very close to a
* perfect gray"; icon means "no larger than 64dp".
*
* @param bitmap The bitmap to test.
* @return True if the bitmap is grayscale; false if it is color or too large to examine.
*/
public boolean isGrayscaleIcon(Bitmap bitmap) {
// quick test: reject large bitmaps
if (bitmap.getWidth() > mGrayscaleIconMaxSize || bitmap.getHeight() > mGrayscaleIconMaxSize) {
return false;
}
synchronized (sLock) {
Pair<Boolean, Integer> cached = mGrayscaleBitmapCache.get(bitmap);
if (cached != null) {
if (cached.second == bitmap.getGenerationId()) {
return cached.first;
}
}
}
boolean result;
int generationId;
synchronized (mImageUtils) {
result = mImageUtils.isGrayscale(bitmap);
// generationId and the check whether the Bitmap is grayscale can't be read atomically
// here. However, since the thread is in the process of posting the notification, we can
// assume that it doesn't modify the bitmap while we are checking the pixels.
generationId = bitmap.getGenerationId();
}
synchronized (sLock) {
mGrayscaleBitmapCache.put(bitmap, Pair.create(result, generationId));
}
return result;
}
private int processColor(int color) {
return Color.argb(
Color.alpha(color),
255 - Color.red(color),
255 - Color.green(color),
255 - Color.blue(color));
}
/** Framework copy of functions needed from android.support.v4.graphics.ColorUtils. */
private static class ColorUtilsFromCompat {
private static final double XYZ_WHITE_REFERENCE_X = 95.047;
private static final double XYZ_WHITE_REFERENCE_Y = 100;
private static final double XYZ_WHITE_REFERENCE_Z = 108.883;
private static final double XYZ_EPSILON = 0.008856;
private static final double XYZ_KAPPA = 903.3;
private static final int MIN_ALPHA_SEARCH_MAX_ITERATIONS = 10;
private static final int MIN_ALPHA_SEARCH_PRECISION = 1;
private static final ThreadLocal<double[]> TEMP_ARRAY = new ThreadLocal<>();
private ColorUtilsFromCompat() {}
/** Composite two potentially translucent colors over each other and returns the result. */
public static int compositeColors(@ColorInt int foreground, @ColorInt int background) {
int bgAlpha = Color.alpha(background);
int fgAlpha = Color.alpha(foreground);
int a = compositeAlpha(fgAlpha, bgAlpha);
int r = compositeComponent(Color.red(foreground), fgAlpha, Color.red(background), bgAlpha, a);
int g =
compositeComponent(Color.green(foreground), fgAlpha, Color.green(background), bgAlpha, a);
int b =
compositeComponent(Color.blue(foreground), fgAlpha, Color.blue(background), bgAlpha, a);
return Color.argb(a, r, g, b);
}
private static int compositeAlpha(int foregroundAlpha, int backgroundAlpha) {
return 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF);
}
private static int compositeComponent(int fgC, int fgA, int bgC, int bgA, int a) {
if (a == 0) return 0;
return ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (a * 0xFF);
}
/**
* Returns the luminance of a color as a float between {@code 0.0} and {@code 1.0}.
*
* <p>Defined as the Y component in the XYZ representation of {@code color}.
*/
@FloatRange(from = 0.0, to = 1.0)
public static double calculateLuminance(@ColorInt int color) {
final double[] result = getTempDouble3Array();
colorToXYZ(color, result);
// Luminance is the Y component
return result[1] / 100;
}
/**
* Returns the contrast ratio between {@code foreground} and {@code background}. {@code
* background} must be opaque.
*
* <p>Formula defined <a
* href="http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef">here</a>.
*/
public static double calculateContrast(@ColorInt int foreground, @ColorInt int background) {
if (Color.alpha(background) != 255) {
Log.wtf(TAG, "background can not be translucent: #" + Integer.toHexString(background));
}
if (Color.alpha(foreground) < 255) {
// If the foreground is translucent, composite the foreground over the background
foreground = compositeColors(foreground, background);
}
final double luminance1 = calculateLuminance(foreground) + 0.05;
final double luminance2 = calculateLuminance(background) + 0.05;
// Now return the lighter luminance divided by the darker luminance
return Math.max(luminance1, luminance2) / Math.min(luminance1, luminance2);
}
/**
* Convert the ARGB color to its CIE Lab representative components.
*
* @param color the ARGB color to convert. The alpha component is ignored
* @param outLab 3-element array which holds the resulting LAB components
*/
public static void colorToLAB(@ColorInt int color, @NonNull double[] outLab) {
RGBToLAB(Color.red(color), Color.green(color), Color.blue(color), outLab);
}
/**
* Convert RGB components to its CIE Lab representative components.
*
* <ul>
* <li>outLab[0] is L [0 ...100)
* <li>outLab[1] is a [-128...127)
* <li>outLab[2] is b [-128...127)
* </ul>
*
* @param r red component value [0..255]
* @param g green component value [0..255]
* @param b blue component value [0..255]
* @param outLab 3-element array which holds the resulting LAB components
*/
public static void RGBToLAB(
@IntRange(from = 0x0, to = 0xFF) int r,
@IntRange(from = 0x0, to = 0xFF) int g,
@IntRange(from = 0x0, to = 0xFF) int b,
@NonNull double[] outLab) {
// First we convert RGB to XYZ
RGBToXYZ(r, g, b, outLab);
// outLab now contains XYZ
XYZToLAB(outLab[0], outLab[1], outLab[2], outLab);
// outLab now contains LAB representation
}
/**
* Convert the ARGB color to it's CIE XYZ representative components.
*
* <p>The resulting XYZ representation will use the D65 illuminant and the CIE 2° Standard
* Observer (1931).
*
* <ul>
* <li>outXyz[0] is X [0 ...95.047)
* <li>outXyz[1] is Y [0...100)
* <li>outXyz[2] is Z [0...108.883)
* </ul>
*
* @param color the ARGB color to convert. The alpha component is ignored
* @param outXyz 3-element array which holds the resulting LAB components
*/
public static void colorToXYZ(@ColorInt int color, @NonNull double[] outXyz) {
RGBToXYZ(Color.red(color), Color.green(color), Color.blue(color), outXyz);
}
/**
* Convert RGB components to it's CIE XYZ representative components.
*
* <p>The resulting XYZ representation will use the D65 illuminant and the CIE 2° Standard
* Observer (1931).
*
* <ul>
* <li>outXyz[0] is X [0 ...95.047)
* <li>outXyz[1] is Y [0...100)
* <li>outXyz[2] is Z [0...108.883)
* </ul>
*
* @param r red component value [0..255]
* @param g green component value [0..255]
* @param b blue component value [0..255]
* @param outXyz 3-element array which holds the resulting XYZ components
*/
public static void RGBToXYZ(
@IntRange(from = 0x0, to = 0xFF) int r,
@IntRange(from = 0x0, to = 0xFF) int g,
@IntRange(from = 0x0, to = 0xFF) int b,
@NonNull double[] outXyz) {
if (outXyz.length != 3) {
throw new IllegalArgumentException("outXyz must have a length of 3.");
}
double sr = r / 255.0;
sr = sr < 0.04045 ? sr / 12.92 : Math.pow((sr + 0.055) / 1.055, 2.4);
double sg = g / 255.0;
sg = sg < 0.04045 ? sg / 12.92 : Math.pow((sg + 0.055) / 1.055, 2.4);
double sb = b / 255.0;
sb = sb < 0.04045 ? sb / 12.92 : Math.pow((sb + 0.055) / 1.055, 2.4);
outXyz[0] = 100 * (sr * 0.4124 + sg * 0.3576 + sb * 0.1805);
outXyz[1] = 100 * (sr * 0.2126 + sg * 0.7152 + sb * 0.0722);
outXyz[2] = 100 * (sr * 0.0193 + sg * 0.1192 + sb * 0.9505);
}
/**
* Converts a color from CIE XYZ to CIE Lab representation.
*
* <p>This method expects the XYZ representation to use the D65 illuminant and the CIE 2°
* Standard Observer (1931).
*
* <ul>
* <li>outLab[0] is L [0 ...100)
* <li>outLab[1] is a [-128...127)
* <li>outLab[2] is b [-128...127)
* </ul>
*
* @param x X component value [0...95.047)
* @param y Y component value [0...100)
* @param z Z component value [0...108.883)
* @param outLab 3-element array which holds the resulting Lab components
*/
public static void XYZToLAB(
@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x,
@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y,
@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z,
@NonNull double[] outLab) {
if (outLab.length != 3) {
throw new IllegalArgumentException("outLab must have a length of 3.");
}
x = pivotXyzComponent(x / XYZ_WHITE_REFERENCE_X);
y = pivotXyzComponent(y / XYZ_WHITE_REFERENCE_Y);
z = pivotXyzComponent(z / XYZ_WHITE_REFERENCE_Z);
outLab[0] = Math.max(0, 116 * y - 16);
outLab[1] = 500 * (x - y);
outLab[2] = 200 * (y - z);
}
/**
* Converts a color from CIE Lab to CIE XYZ representation.
*
* <p>The resulting XYZ representation will use the D65 illuminant and the CIE 2° Standard
* Observer (1931).
*
* <ul>
* <li>outXyz[0] is X [0 ...95.047)
* <li>outXyz[1] is Y [0...100)
* <li>outXyz[2] is Z [0...108.883)
* </ul>
*
* @param l L component value [0...100)
* @param a A component value [-128...127)
* @param b B component value [-128...127)
* @param outXyz 3-element array which holds the resulting XYZ components
*/
public static void LABToXYZ(
@FloatRange(from = 0f, to = 100) final double l,
@FloatRange(from = -128, to = 127) final double a,
@FloatRange(from = -128, to = 127) final double b,
@NonNull double[] outXyz) {
final double fy = (l + 16) / 116;
final double fx = a / 500 + fy;
final double fz = fy - b / 200;
double tmp = Math.pow(fx, 3);
final double xr = tmp > XYZ_EPSILON ? tmp : (116 * fx - 16) / XYZ_KAPPA;
final double yr = l > XYZ_KAPPA * XYZ_EPSILON ? Math.pow(fy, 3) : l / XYZ_KAPPA;
tmp = Math.pow(fz, 3);
final double zr = tmp > XYZ_EPSILON ? tmp : (116 * fz - 16) / XYZ_KAPPA;
outXyz[0] = xr * XYZ_WHITE_REFERENCE_X;
outXyz[1] = yr * XYZ_WHITE_REFERENCE_Y;
outXyz[2] = zr * XYZ_WHITE_REFERENCE_Z;
}
/**
* Converts a color from CIE XYZ to its RGB representation.
*
* <p>This method expects the XYZ representation to use the D65 illuminant and the CIE 2°
* Standard Observer (1931).
*
* @param x X component value [0...95.047)
* @param y Y component value [0...100)
* @param z Z component value [0...108.883)
* @return int containing the RGB representation
*/
@ColorInt
public static int XYZToColor(
@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x,
@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y,
@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z) {
double r = (x * 3.2406 + y * -1.5372 + z * -0.4986) / 100;
double g = (x * -0.9689 + y * 1.8758 + z * 0.0415) / 100;
double b = (x * 0.0557 + y * -0.2040 + z * 1.0570) / 100;
r = r > 0.0031308 ? 1.055 * Math.pow(r, 1 / 2.4) - 0.055 : 12.92 * r;
g = g > 0.0031308 ? 1.055 * Math.pow(g, 1 / 2.4) - 0.055 : 12.92 * g;
b = b > 0.0031308 ? 1.055 * Math.pow(b, 1 / 2.4) - 0.055 : 12.92 * b;
return Color.rgb(
constrain((int) Math.round(r * 255), 0, 255),
constrain((int) Math.round(g * 255), 0, 255),
constrain((int) Math.round(b * 255), 0, 255));
}
/**
* Converts a color from CIE Lab to its RGB representation.
*
* @param l L component value [0...100]
* @param a A component value [-128...127]
* @param b B component value [-128...127]
* @return int containing the RGB representation
*/
@ColorInt
public static int LABToColor(
@FloatRange(from = 0f, to = 100) final double l,
@FloatRange(from = -128, to = 127) final double a,
@FloatRange(from = -128, to = 127) final double b) {
final double[] result = getTempDouble3Array();
LABToXYZ(l, a, b, result);
return XYZToColor(result[0], result[1], result[2]);
}
private static int constrain(int amount, int low, int high) {
return amount < low ? low : (Math.min(amount, high));
}
private static float constrain(float amount, float low, float high) {
return amount < low ? low : (Math.min(amount, high));
}
private static double pivotXyzComponent(double component) {
return component > XYZ_EPSILON
? Math.pow(component, 1 / 3.0)
: (XYZ_KAPPA * component + 16) / 116;
}
public static double[] getTempDouble3Array() {
double[] result = TEMP_ARRAY.get();
if (result == null) {
result = new double[3];
TEMP_ARRAY.set(result);
}
return result;
}
/**
* Convert HSL (hue-saturation-lightness) components to a RGB color.
*
* <ul>
* <li>hsl[0] is Hue [0 .. 360)
* <li>hsl[1] is Saturation [0...1]
* <li>hsl[2] is Lightness [0...1]
* </ul>
*
* If hsv values are out of range, they are pinned.
*
* @param hsl 3-element array which holds the input HSL components
* @return the resulting RGB color
*/
@ColorInt
public static int HSLToColor(@NonNull float[] hsl) {
final float h = hsl[0];
final float s = hsl[1];
final float l = hsl[2];
final float c = (1f - Math.abs(2 * l - 1f)) * s;
final float m = l - 0.5f * c;
final float x = c * (1f - Math.abs((h / 60f % 2f) - 1f));
final int hueSegment = (int) h / 60;
int r = 0, g = 0, b = 0;
switch (hueSegment) {
case 0:
r = Math.round(255 * (c + m));
g = Math.round(255 * (x + m));
b = Math.round(255 * m);
break;
case 1:
r = Math.round(255 * (x + m));
g = Math.round(255 * (c + m));
b = Math.round(255 * m);
break;
case 2:
r = Math.round(255 * m);
g = Math.round(255 * (c + m));
b = Math.round(255 * (x + m));
break;
case 3:
r = Math.round(255 * m);
g = Math.round(255 * (x + m));
b = Math.round(255 * (c + m));
break;
case 4:
r = Math.round(255 * (x + m));
g = Math.round(255 * m);
b = Math.round(255 * (c + m));
break;
case 5:
case 6:
r = Math.round(255 * (c + m));
g = Math.round(255 * m);
b = Math.round(255 * (x + m));
break;
}
r = constrain(r, 0, 255);
g = constrain(g, 0, 255);
b = constrain(b, 0, 255);
return Color.rgb(r, g, b);
}
/**
* Convert the ARGB color to its HSL (hue-saturation-lightness) components.
*
* <ul>
* <li>outHsl[0] is Hue [0 .. 360)
* <li>outHsl[1] is Saturation [0...1]
* <li>outHsl[2] is Lightness [0...1]
* </ul>
*
* @param color the ARGB color to convert. The alpha component is ignored
* @param outHsl 3-element array which holds the resulting HSL components
*/
public static void colorToHSL(@ColorInt int color, @NonNull float[] outHsl) {
RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), outHsl);
}
/**
* Convert RGB components to HSL (hue-saturation-lightness).
*
* <ul>
* <li>outHsl[0] is Hue [0 .. 360)
* <li>outHsl[1] is Saturation [0...1]
* <li>outHsl[2] is Lightness [0...1]
* </ul>
*
* @param r red component value [0..255]
* @param g green component value [0..255]
* @param b blue component value [0..255]
* @param outHsl 3-element array which holds the resulting HSL components
*/
public static void RGBToHSL(
@IntRange(from = 0x0, to = 0xFF) int r,
@IntRange(from = 0x0, to = 0xFF) int g,
@IntRange(from = 0x0, to = 0xFF) int b,
@NonNull float[] outHsl) {
final float rf = r / 255f;
final float gf = g / 255f;
final float bf = b / 255f;
final float max = Math.max(rf, Math.max(gf, bf));
final float min = Math.min(rf, Math.min(gf, bf));
final float deltaMaxMin = max - min;
float h, s;
float l = (max + min) / 2f;
if (max == min) {
// Monochromatic
h = s = 0f;
} else {
if (max == rf) {
h = ((gf - bf) / deltaMaxMin) % 6f;
} else if (max == gf) {
h = ((bf - rf) / deltaMaxMin) + 2f;
} else {
h = ((rf - gf) / deltaMaxMin) + 4f;
}
s = deltaMaxMin / (1f - Math.abs(2f * l - 1f));
}
h = (h * 60f) % 360f;
if (h < 0) {
h += 360f;
}
outHsl[0] = constrain(h, 0f, 360f);
outHsl[1] = constrain(s, 0f, 1f);
outHsl[2] = constrain(l, 0f, 1f);
}
}
}

View file

@ -0,0 +1,29 @@
package code.name.monkey.retromusic.util.theme
import android.content.Context
import androidx.annotation.StyleRes
import androidx.appcompat.app.AppCompatDelegate
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.extensions.generalThemeValue
import code.name.monkey.retromusic.util.PreferenceUtil
import code.name.monkey.retromusic.util.theme.ThemeMode.*
@StyleRes
fun Context.getThemeResValue(): Int =
if (PreferenceUtil.materialYou) {
if (generalThemeValue == BLACK) R.style.Theme_RetroMusic_MD3_Black
else R.style.Theme_RetroMusic_MD3
} else {
when (generalThemeValue) {
LIGHT -> R.style.Theme_RetroMusic_Light
DARK -> R.style.Theme_RetroMusic_Base
BLACK -> R.style.Theme_RetroMusic_Black
AUTO -> R.style.Theme_RetroMusic_FollowSystem
}
}
fun Context.getNightMode(): Int = when (generalThemeValue) {
LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
DARK -> AppCompatDelegate.MODE_NIGHT_YES
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}

View file

@ -0,0 +1,8 @@
package code.name.monkey.retromusic.util.theme
enum class ThemeMode {
LIGHT,
DARK,
BLACK,
AUTO
}