My initial commit
Removed Google play dependencies
This commit is contained in:
parent
fd582fff69
commit
301ac10570
430 changed files with 2210 additions and 3137 deletions
|
@ -1,84 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
package code.name.monkey.retromusic.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import code.name.monkey.retromusic.R
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
||||
object AppRater {
|
||||
private const val DO_NOT_SHOW_AGAIN = "do_not_show_again"// Package Name
|
||||
private const val APP_RATING = "app_rating"// Package Name
|
||||
private const val LAUNCH_COUNT = "launch_count"// Package Name
|
||||
private const val DATE_FIRST_LAUNCH = "date_first_launch"// Package Name
|
||||
|
||||
private const val DAYS_UNTIL_PROMPT = 3//Min number of days
|
||||
private const val LAUNCHES_UNTIL_PROMPT = 5//Min number of launches
|
||||
|
||||
@JvmStatic
|
||||
fun appLaunched(context: Context) {
|
||||
val prefs = context.getSharedPreferences(APP_RATING, 0)
|
||||
if (prefs.getBoolean(DO_NOT_SHOW_AGAIN, false)) {
|
||||
return
|
||||
}
|
||||
|
||||
val editor = prefs.edit()
|
||||
|
||||
// Increment launch counter
|
||||
val launchCount = prefs.getLong(LAUNCH_COUNT, 0) + 1
|
||||
editor.putLong(LAUNCH_COUNT, launchCount)
|
||||
|
||||
// Get date of first launch
|
||||
var dateFirstLaunch = prefs.getLong(DATE_FIRST_LAUNCH, 0)
|
||||
if (dateFirstLaunch == 0L) {
|
||||
dateFirstLaunch = System.currentTimeMillis()
|
||||
editor.putLong(DATE_FIRST_LAUNCH, dateFirstLaunch)
|
||||
}
|
||||
|
||||
// Wait at least n days before opening
|
||||
if (launchCount >= LAUNCHES_UNTIL_PROMPT) {
|
||||
if (System.currentTimeMillis() >= dateFirstLaunch + DAYS_UNTIL_PROMPT * 24 * 60 * 60 * 1000) {
|
||||
showRateDialog(context, editor)
|
||||
}
|
||||
}
|
||||
|
||||
editor.commit()
|
||||
}
|
||||
|
||||
private fun showRateDialog(context: Context, editor: SharedPreferences.Editor) {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle("Rate this App")
|
||||
.setMessage("If you enjoy using Retro Music, please take a moment to rate it. Thanks for your support!")
|
||||
.setPositiveButton(R.string.app_name) { _, _ ->
|
||||
context.startActivity(
|
||||
Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse("market://details?id=${context.packageName}")
|
||||
)
|
||||
)
|
||||
editor.putBoolean(DO_NOT_SHOW_AGAIN, true)
|
||||
editor.commit()
|
||||
}
|
||||
.setNeutralButton("Not now", null)
|
||||
.setNegativeButton("No thanks") { _, _ ->
|
||||
editor.putBoolean(DO_NOT_SHOW_AGAIN, true)
|
||||
editor.commit()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
package code.name.monkey.retromusic.util;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.bumptech.glide.signature.StringSignature;
|
||||
|
||||
/**
|
||||
* @author Karim Abou Zeid (kabouzeid)
|
||||
*/
|
||||
public class ArtistSignatureUtil {
|
||||
private static final String ARTIST_SIGNATURE_PREFS = "artist_signatures";
|
||||
|
||||
private static ArtistSignatureUtil sInstance;
|
||||
|
||||
private final SharedPreferences mPreferences;
|
||||
|
||||
private ArtistSignatureUtil(@NonNull final Context context) {
|
||||
mPreferences = context.getSharedPreferences(ARTIST_SIGNATURE_PREFS, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
public static ArtistSignatureUtil getInstance(@NonNull final Context context) {
|
||||
if (sInstance == null) {
|
||||
sInstance = new ArtistSignatureUtil(context.getApplicationContext());
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
@SuppressLint("CommitPrefEdits")
|
||||
public void updateArtistSignature(String artistName) {
|
||||
mPreferences.edit().putLong(artistName, System.currentTimeMillis()).commit();
|
||||
}
|
||||
|
||||
public long getArtistSignatureRaw(String artistName) {
|
||||
return mPreferences.getLong(artistName, 0);
|
||||
}
|
||||
|
||||
public StringSignature getArtistSignature(String artistName) {
|
||||
return new StringSignature(String.valueOf(getArtistSignatureRaw(artistName)));
|
||||
}
|
||||
}
|
|
@ -1,190 +0,0 @@
|
|||
package code.name.monkey.retromusic.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import code.name.monkey.retromusic.R;
|
||||
import code.name.monkey.retromusic.model.Song;
|
||||
|
||||
|
||||
public class AutoGeneratedPlaylistBitmap {
|
||||
private static final String TAG = "AutoGeneratedPB";
|
||||
|
||||
/*
|
||||
public static Bitmap getBitmapWithCollectionFrame(Context context, List<Song> songPlaylist, boolean round, boolean blur) {
|
||||
Bitmap bitmap = getBitmap(context,songPlaylist,round,blur);
|
||||
int w = bitmap.getWidth();
|
||||
Bitmap ret = Bitmap.createBitmap(w,w,Bitmap.Config.ARGB_8888);
|
||||
}
|
||||
*/
|
||||
public static Bitmap getBitmap(Context context, List<Song> songPlaylist, boolean round, boolean blur) {
|
||||
if (songPlaylist == null) return null;
|
||||
long start = System.currentTimeMillis();
|
||||
// lấy toàn bộ album id, loại bỏ trùng nhau
|
||||
ArrayList<Integer> albumID = new ArrayList<>();
|
||||
for (Song song : songPlaylist) {
|
||||
if (!albumID.contains(song.getAlbumId())) albumID.add(song.getAlbumId());
|
||||
}
|
||||
|
||||
long start2 = System.currentTimeMillis() - start;
|
||||
|
||||
// lấy toàn bộ art tồn tại
|
||||
ArrayList<Bitmap> art = new ArrayList<Bitmap>();
|
||||
for (Integer id : albumID) {
|
||||
Bitmap bitmap = getBitmapWithAlbumId(context, id);
|
||||
if (bitmap != null) art.add(bitmap);
|
||||
if (art.size() == 6) break;
|
||||
}
|
||||
return MergedImageUtils.INSTANCE.joinImages(art);
|
||||
/*
|
||||
|
||||
long start3 = System.currentTimeMillis() - start2 - start;
|
||||
Bitmap ret;
|
||||
switch (art.size()) {
|
||||
// lấy hình mặc định
|
||||
case 0:
|
||||
ret = getDefaultBitmap(context, round).copy(Bitmap.Config.ARGB_8888, false);
|
||||
break;
|
||||
// dùng hình duy nhất
|
||||
case 1:
|
||||
if (round)
|
||||
ret = BitmapEditor.getRoundedCornerBitmap(art.get(0), art.get(0).getWidth() / 40);
|
||||
else ret = art.get(0);
|
||||
break;
|
||||
// từ 2 trở lên ta cần vẽ canvas
|
||||
default:
|
||||
ret = getBitmapCollection(art, round);
|
||||
}
|
||||
int w = ret.getWidth();
|
||||
if (blur)
|
||||
return BitmapEditor.GetRoundedBitmapWithBlurShadow(context, ret, w / 24, w / 24, w / 24, w / 24, 0, 200, w / 40, 1);
|
||||
|
||||
Log.d(TAG, "getBitmap: time = " + (System.currentTimeMillis() - start) + ", start2 = " + start2 + ", start3 = " + start3);
|
||||
return ret;*/
|
||||
}
|
||||
|
||||
private static Bitmap getBitmapCollection(ArrayList<Bitmap> art, boolean round) {
|
||||
long start = System.currentTimeMillis();
|
||||
// lấy kích thước là kích thước của bitmap lớn nhất
|
||||
int max_width = art.get(0).getWidth();
|
||||
for (Bitmap b : art) if (max_width < b.getWidth()) max_width = b.getWidth();
|
||||
Bitmap bitmap = Bitmap.createBitmap(max_width, max_width, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
Paint paint = new Paint();
|
||||
paint.setAntiAlias(false);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setStrokeWidth(max_width / 100);
|
||||
paint.setColor(0xffffffff);
|
||||
switch (art.size()) {
|
||||
case 2:
|
||||
canvas.drawBitmap(art.get(1), null, new Rect(0, 0, max_width, max_width), null);
|
||||
canvas.drawBitmap(art.get(0), null, new Rect(-max_width / 2, 0, max_width / 2, max_width), null);
|
||||
canvas.drawLine(max_width / 2, 0, max_width / 2, max_width, paint);
|
||||
break;
|
||||
case 3:
|
||||
canvas.drawBitmap(art.get(0), null, new Rect(-max_width / 4, 0, 3 * max_width / 4, max_width), null);
|
||||
canvas.drawBitmap(art.get(1), null, new Rect(max_width / 2, 0, max_width, max_width / 2), null);
|
||||
canvas.drawBitmap(art.get(2), null, new Rect(max_width / 2, max_width / 2, max_width, max_width), null);
|
||||
canvas.drawLine(max_width / 2, 0, max_width / 2, max_width, paint);
|
||||
canvas.drawLine(max_width / 2, max_width / 2, max_width, max_width / 2, paint);
|
||||
break;
|
||||
case 4:
|
||||
canvas.drawBitmap(art.get(0), null, new Rect(0, 0, max_width / 2, max_width / 2), null);
|
||||
canvas.drawBitmap(art.get(1), null, new Rect(max_width / 2, 0, max_width, max_width / 2), null);
|
||||
canvas.drawBitmap(art.get(2), null, new Rect(0, max_width / 2, max_width / 2, max_width), null);
|
||||
canvas.drawBitmap(art.get(3), null, new Rect(max_width / 2, max_width / 2, max_width, max_width), null);
|
||||
canvas.drawLine(max_width / 2, 0, max_width / 2, max_width, paint);
|
||||
canvas.drawLine(0, max_width / 2, max_width, max_width / 2, paint);
|
||||
break;
|
||||
// default: canvas.drawBitmap(art.get(0),null,new Rect(0,0,max_width,max_width),null);
|
||||
default:
|
||||
|
||||
// độ rộng của des bitmap
|
||||
float w = (float) (Math.sqrt(2) / 2 * max_width);
|
||||
float b = (float) (max_width / Math.sqrt(5));
|
||||
// khoảng cách định nghĩa, dùng để tính vị trí tâm của 4 bức hình xung quanh
|
||||
float d = (float) (max_width * (0.5f - 1 / Math.sqrt(10)));
|
||||
float deg = 45;
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
canvas.save();
|
||||
switch (i) {
|
||||
case 0:
|
||||
canvas.translate(max_width / 2, max_width / 2);
|
||||
canvas.rotate(deg);
|
||||
// b = (float) (max_width*Math.sqrt(2/5f));
|
||||
canvas.drawBitmap(art.get(0), null, new RectF(-b / 2, -b / 2, b / 2, b / 2), null);
|
||||
break;
|
||||
case 1:
|
||||
canvas.translate(d, 0);
|
||||
canvas.rotate(deg);
|
||||
canvas.drawBitmap(art.get(i), null, new RectF(-w / 2, -w / 2, w / 2, w / 2), null);
|
||||
paint.setAntiAlias(true);
|
||||
canvas.drawLine(w / 2, -w / 2, w / 2, w / 2, paint);
|
||||
break;
|
||||
case 2:
|
||||
canvas.translate(max_width, d);
|
||||
canvas.rotate(deg);
|
||||
canvas.drawBitmap(art.get(i), null, new RectF(-w / 2, -w / 2, w / 2, w / 2), null);
|
||||
paint.setAntiAlias(true);
|
||||
canvas.drawLine(-w / 2, w / 2, w / 2, w / 2, paint);
|
||||
break;
|
||||
case 3:
|
||||
canvas.translate(max_width - d, max_width);
|
||||
canvas.rotate(deg);
|
||||
canvas.drawBitmap(art.get(i), null, new RectF(-w / 2, -w / 2, w / 2, w / 2), null);
|
||||
paint.setAntiAlias(true);
|
||||
canvas.drawLine(-w / 2, -w / 2, -w / 2, w / 2, paint);
|
||||
break;
|
||||
case 4:
|
||||
canvas.translate(0, max_width - d);
|
||||
canvas.rotate(deg);
|
||||
canvas.drawBitmap(art.get(i), null, new RectF(-w / 2, -w / 2, w / 2, w / 2), null);
|
||||
paint.setAntiAlias(true);
|
||||
canvas.drawLine(-w / 2, -w / 2, w / 2, -w / 2, paint);
|
||||
break;
|
||||
}
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
;
|
||||
Log.d(TAG, "getBitmapCollection: smalltime = " + (System.currentTimeMillis() - start));
|
||||
if (round)
|
||||
return BitmapEditor.getRoundedCornerBitmap(bitmap, bitmap.getWidth() / 40);
|
||||
else return bitmap;
|
||||
}
|
||||
|
||||
private static Bitmap getBitmapWithAlbumId(@NonNull Context context, Integer id) {
|
||||
try {
|
||||
return Glide.with(context)
|
||||
.load(MusicUtil.getMediaStoreAlbumCoverUri(id))
|
||||
.asBitmap()
|
||||
.into(200, 200)
|
||||
.get();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static Bitmap getDefaultBitmap(@NonNull Context context, boolean round) {
|
||||
if (round)
|
||||
return BitmapFactory.decodeResource(context.getResources(), R.drawable.default_album_art);
|
||||
return BitmapFactory.decodeResource(context.getResources(), R.drawable.default_album_art);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,969 +0,0 @@
|
|||
package code.name.monkey.retromusic.util;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.ColorMatrix;
|
||||
import android.graphics.ColorMatrixColorFilter;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.graphics.PorterDuffXfermode;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.renderscript.Allocation;
|
||||
import android.renderscript.Element;
|
||||
import android.renderscript.RenderScript;
|
||||
import android.renderscript.ScriptIntrinsicBlur;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
|
||||
/**
|
||||
* Created by trung on 7/11/2017.
|
||||
*/
|
||||
|
||||
public final class BitmapEditor {
|
||||
/**
|
||||
* Stack Blur v1.0 from
|
||||
* http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html
|
||||
* Java Author: Mario Klingemann <mario at quasimondo.com>
|
||||
* http://incubator.quasimondo.com
|
||||
* <p/>
|
||||
* created Feburary 29, 2004
|
||||
* Android port : Yahel Bouaziz <yahel at kayenko.com>
|
||||
* http://www.kayenko.com
|
||||
* ported april 5th, 2012
|
||||
* <p/>
|
||||
* This is A compromise between Gaussian Blur and Box blur
|
||||
* It creates much better looking blurs than Box Blur, but is
|
||||
* 7x faster than my Gaussian Blur implementation.
|
||||
* <p/>
|
||||
* I called it Stack Blur because this describes best how this
|
||||
* filter works internally: it creates A kind of moving stack
|
||||
* of colors whilst scanning through the image. Thereby it
|
||||
* just has to add one new block of color to the right side
|
||||
* of the stack and removeFromParent the leftmost color. The remaining
|
||||
* colors on the topmost layer of the stack are either added on
|
||||
* or reduced by one, depending on if they are on the right or
|
||||
* on the x side of the stack.
|
||||
* <p/>
|
||||
* If you are using this algorithm in your code please add
|
||||
* the following line:
|
||||
* Stack Blur Algorithm by Mario Klingemann <mario@quasimondo.com>
|
||||
*/
|
||||
|
||||
public static Bitmap FastBlurSupportAlpha(Bitmap sentBitmap, float scale, int radius) {
|
||||
|
||||
int width = Math.round(sentBitmap.getWidth() * scale);
|
||||
int height = Math.round(sentBitmap.getHeight() * scale);
|
||||
sentBitmap = Bitmap.createScaledBitmap(sentBitmap, width, height, false);
|
||||
|
||||
Bitmap bitmap = sentBitmap.copy(sentBitmap.getConfig(), true);
|
||||
|
||||
if (radius < 1) {
|
||||
return (null);
|
||||
}
|
||||
|
||||
int w = bitmap.getWidth();
|
||||
int h = bitmap.getHeight();
|
||||
|
||||
int[] pix = new int[w * h];
|
||||
// Log.e("pix", w + " " + h + " " + pix.length);
|
||||
bitmap.getPixels(pix, 0, w, 0, 0, w, h);
|
||||
|
||||
int wm = w - 1;
|
||||
int hm = h - 1;
|
||||
int wh = w * h;
|
||||
int div = radius + radius + 1;
|
||||
|
||||
int r[] = new int[wh];
|
||||
int g[] = new int[wh];
|
||||
int b[] = new int[wh];
|
||||
int a[] = new int[wh];
|
||||
int rsum, gsum, bsum, asum, x, y, i, p, yp, yi, yw;
|
||||
int vmin[] = new int[Math.max(w, h)];
|
||||
|
||||
int divsum = (div + 1) >> 1;
|
||||
divsum *= divsum;
|
||||
int dv[] = new int[256 * divsum];
|
||||
for (i = 0; i < 256 * divsum; i++) {
|
||||
dv[i] = (i / divsum);
|
||||
}
|
||||
|
||||
yw = yi = 0;
|
||||
|
||||
int[][] stack = new int[div][4];
|
||||
int stackpointer;
|
||||
int stackstart;
|
||||
int[] sir;
|
||||
int rbs;
|
||||
int r1 = radius + 1;
|
||||
int routsum, goutsum, boutsum, aoutsum;
|
||||
int rinsum, ginsum, binsum, ainsum;
|
||||
|
||||
for (y = 0; y < h; y++) {
|
||||
rinsum = ginsum = binsum = ainsum = routsum = goutsum = boutsum = aoutsum = rsum = gsum = bsum = asum = 0;
|
||||
for (i = -radius; i <= radius; i++) {
|
||||
p = pix[yi + Math.min(wm, Math.max(i, 0))];
|
||||
sir = stack[i + radius];
|
||||
sir[0] = (p & 0xff0000) >> 16;
|
||||
sir[1] = (p & 0x00ff00) >> 8;
|
||||
sir[2] = (p & 0x0000ff);
|
||||
sir[3] = 0xff & (p >> 24);
|
||||
|
||||
rbs = r1 - Math.abs(i);
|
||||
rsum += sir[0] * rbs;
|
||||
gsum += sir[1] * rbs;
|
||||
bsum += sir[2] * rbs;
|
||||
asum += sir[3] * rbs;
|
||||
if (i > 0) {
|
||||
rinsum += sir[0];
|
||||
ginsum += sir[1];
|
||||
binsum += sir[2];
|
||||
ainsum += sir[3];
|
||||
} else {
|
||||
routsum += sir[0];
|
||||
goutsum += sir[1];
|
||||
boutsum += sir[2];
|
||||
aoutsum += sir[3];
|
||||
}
|
||||
}
|
||||
stackpointer = radius;
|
||||
|
||||
for (x = 0; x < w; x++) {
|
||||
|
||||
r[yi] = dv[rsum];
|
||||
g[yi] = dv[gsum];
|
||||
b[yi] = dv[bsum];
|
||||
a[yi] = dv[asum];
|
||||
|
||||
rsum -= routsum;
|
||||
gsum -= goutsum;
|
||||
bsum -= boutsum;
|
||||
asum -= aoutsum;
|
||||
|
||||
stackstart = stackpointer - radius + div;
|
||||
sir = stack[stackstart % div];
|
||||
|
||||
routsum -= sir[0];
|
||||
goutsum -= sir[1];
|
||||
boutsum -= sir[2];
|
||||
aoutsum -= sir[3];
|
||||
|
||||
if (y == 0) {
|
||||
vmin[x] = Math.min(x + radius + 1, wm);
|
||||
}
|
||||
p = pix[yw + vmin[x]];
|
||||
|
||||
sir[0] = (p & 0xff0000) >> 16;
|
||||
sir[1] = (p & 0x00ff00) >> 8;
|
||||
sir[2] = (p & 0x0000ff);
|
||||
sir[3] = 0xff & (p >> 24);
|
||||
|
||||
rinsum += sir[0];
|
||||
ginsum += sir[1];
|
||||
binsum += sir[2];
|
||||
ainsum += sir[3];
|
||||
|
||||
rsum += rinsum;
|
||||
gsum += ginsum;
|
||||
bsum += binsum;
|
||||
asum += ainsum;
|
||||
|
||||
stackpointer = (stackpointer + 1) % div;
|
||||
sir = stack[(stackpointer) % div];
|
||||
|
||||
routsum += sir[0];
|
||||
goutsum += sir[1];
|
||||
boutsum += sir[2];
|
||||
aoutsum += sir[3];
|
||||
|
||||
rinsum -= sir[0];
|
||||
ginsum -= sir[1];
|
||||
binsum -= sir[2];
|
||||
ainsum -= sir[3];
|
||||
|
||||
yi++;
|
||||
}
|
||||
yw += w;
|
||||
}
|
||||
for (x = 0; x < w; x++) {
|
||||
rinsum = ginsum = binsum = ainsum = routsum = goutsum = boutsum = aoutsum = rsum = gsum = bsum = asum = 0;
|
||||
yp = -radius * w;
|
||||
for (i = -radius; i <= radius; i++) {
|
||||
yi = Math.max(0, yp) + x;
|
||||
|
||||
sir = stack[i + radius];
|
||||
|
||||
sir[0] = r[yi];
|
||||
sir[1] = g[yi];
|
||||
sir[2] = b[yi];
|
||||
sir[3] = a[yi];
|
||||
|
||||
rbs = r1 - Math.abs(i);
|
||||
|
||||
rsum += r[yi] * rbs;
|
||||
gsum += g[yi] * rbs;
|
||||
bsum += b[yi] * rbs;
|
||||
asum += a[yi] * rbs;
|
||||
|
||||
if (i > 0) {
|
||||
rinsum += sir[0];
|
||||
ginsum += sir[1];
|
||||
binsum += sir[2];
|
||||
ainsum += sir[3];
|
||||
} else {
|
||||
routsum += sir[0];
|
||||
goutsum += sir[1];
|
||||
boutsum += sir[2];
|
||||
aoutsum += sir[3];
|
||||
}
|
||||
|
||||
if (i < hm) {
|
||||
yp += w;
|
||||
}
|
||||
}
|
||||
yi = x;
|
||||
stackpointer = radius;
|
||||
for (y = 0; y < h; y++) {
|
||||
pix[yi] = (dv[asum] << 24) | (dv[rsum] << 16) | (dv[gsum] << 8) | dv[bsum];
|
||||
|
||||
rsum -= routsum;
|
||||
gsum -= goutsum;
|
||||
bsum -= boutsum;
|
||||
asum -= aoutsum;
|
||||
|
||||
stackstart = stackpointer - radius + div;
|
||||
sir = stack[stackstart % div];
|
||||
|
||||
routsum -= sir[0];
|
||||
goutsum -= sir[1];
|
||||
boutsum -= sir[2];
|
||||
aoutsum -= sir[3];
|
||||
|
||||
if (x == 0) {
|
||||
vmin[y] = Math.min(y + r1, hm) * w;
|
||||
}
|
||||
p = x + vmin[y];
|
||||
|
||||
|
||||
sir[0] = r[p];
|
||||
sir[1] = g[p];
|
||||
sir[2] = b[p];
|
||||
sir[3] = a[p];
|
||||
|
||||
rinsum += sir[0];
|
||||
ginsum += sir[1];
|
||||
binsum += sir[2];
|
||||
ainsum += sir[3];
|
||||
|
||||
rsum += rinsum;
|
||||
gsum += ginsum;
|
||||
bsum += binsum;
|
||||
asum += ainsum;
|
||||
|
||||
stackpointer = (stackpointer + 1) % div;
|
||||
sir = stack[stackpointer];
|
||||
|
||||
routsum += sir[0];
|
||||
goutsum += sir[1];
|
||||
boutsum += sir[2];
|
||||
aoutsum += sir[3];
|
||||
|
||||
rinsum -= sir[0];
|
||||
ginsum -= sir[1];
|
||||
binsum -= sir[2];
|
||||
ainsum -= sir[3];
|
||||
|
||||
yi += w;
|
||||
}
|
||||
}
|
||||
|
||||
// Log.e("pix", w + " " + h + " " + pix.length);
|
||||
bitmap.setPixels(pix, 0, w, 0, 0, w, h);
|
||||
|
||||
return (bitmap);
|
||||
}
|
||||
|
||||
public static boolean PerceivedBrightness(int will_White, int[] c) {
|
||||
double TBT = Math.sqrt(c[0] * c[0] * .241 + c[1] * c[1] * .691 + c[2] * c[2] * .068);
|
||||
// Log.d("themee",TBT+"");
|
||||
return (TBT > will_White) ? false : true;
|
||||
}
|
||||
|
||||
public static int[] getAverageColorRGB(Bitmap bitmap) {
|
||||
final int width = bitmap.getWidth();
|
||||
final int height = bitmap.getHeight();
|
||||
int size = width * height;
|
||||
int pixelColor;
|
||||
int r, g, b;
|
||||
r = g = b = 0;
|
||||
for (int x = 0; x < width; ++x) {
|
||||
for (int y = 0; y < height; ++y) {
|
||||
pixelColor = bitmap.getPixel(x, y);
|
||||
if (pixelColor == 0) {
|
||||
size--;
|
||||
continue;
|
||||
}
|
||||
r += Color.red(pixelColor);
|
||||
g += Color.green(pixelColor);
|
||||
b += Color.blue(pixelColor);
|
||||
}
|
||||
}
|
||||
r /= size;
|
||||
g /= size;
|
||||
b /= size;
|
||||
return new int[]{
|
||||
r, g, b
|
||||
};
|
||||
}
|
||||
|
||||
public static Bitmap updateSat(Bitmap src, float settingSat) {
|
||||
|
||||
int w = src.getWidth();
|
||||
int h = src.getHeight();
|
||||
|
||||
Bitmap bitmapResult =
|
||||
Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvasResult = new Canvas(bitmapResult);
|
||||
Paint paint = new Paint();
|
||||
ColorMatrix colorMatrix = new ColorMatrix();
|
||||
colorMatrix.setSaturation(settingSat);
|
||||
ColorMatrixColorFilter filter = new ColorMatrixColorFilter(colorMatrix);
|
||||
paint.setColorFilter(filter);
|
||||
canvasResult.drawBitmap(src, 0, 0, paint);
|
||||
canvasResult.setBitmap(null);
|
||||
canvasResult = null;
|
||||
return bitmapResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stack Blur v1.0 from
|
||||
* http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html
|
||||
* Java Author: Mario Klingemann <mario at quasimondo.com>
|
||||
* http://incubator.quasimondo.com
|
||||
* <p>
|
||||
* created Feburary 29, 2004
|
||||
* Android port : Yahel Bouaziz <yahel at kayenko.com>
|
||||
* http://www.kayenko.com
|
||||
* ported april 5th, 2012
|
||||
* <p>
|
||||
* This is A compromise between Gaussian Blur and Box blur
|
||||
* It creates much better looking blurs than Box Blur, but is
|
||||
* 7x faster than my Gaussian Blur implementation.
|
||||
* <p>
|
||||
* I called it Stack Blur because this describes best how this
|
||||
* filter works internally: it creates A kind of moving stack
|
||||
* of colors whilst scanning through the image. Thereby it
|
||||
* just has to add one new block of color to the right side
|
||||
* of the stack and removeFromParent the leftmost color. The remaining
|
||||
* colors on the topmost layer of the stack are either added on
|
||||
* or reduced by one, depending on if they are on the right or
|
||||
* on the x side of the stack.
|
||||
* <p>
|
||||
* If you are using this algorithm in your code please add
|
||||
* the following line:
|
||||
* Stack Blur Algorithm by Mario Klingemann <mario@quasimondo.com>
|
||||
*/
|
||||
|
||||
public static Bitmap fastblur(Bitmap sentBitmap, float scale, int radius) {
|
||||
|
||||
Bitmap afterscaleSentBitmap;
|
||||
Bitmap bitmap;
|
||||
if (scale != 1) {
|
||||
int width = Math.round(sentBitmap.getWidth() * scale); //lấy chiều rộng làm tròn
|
||||
int height = Math.round(sentBitmap.getHeight() * scale); // lấy chiều cao làm tròn
|
||||
afterscaleSentBitmap = Bitmap.createScaledBitmap(sentBitmap, width, height, false); // tạo bitmap scaled
|
||||
bitmap = afterscaleSentBitmap.copy(afterscaleSentBitmap.getConfig(), true);
|
||||
afterscaleSentBitmap.recycle();
|
||||
} else {
|
||||
bitmap = sentBitmap.copy(sentBitmap.getConfig(), true); // đơn giản chỉ copy
|
||||
}
|
||||
|
||||
|
||||
if (radius < 1) {
|
||||
return (sentBitmap.copy(sentBitmap.getConfig(), true));
|
||||
}
|
||||
|
||||
int w = bitmap.getWidth(); // w is the width of sample bitmap
|
||||
int h = bitmap.getHeight(); // h is the height of sample bitmap
|
||||
|
||||
int[] pix = new int[w * h]; // pix is the arrary of all bitmap pixel
|
||||
|
||||
bitmap.getPixels(pix, 0, w, 0, 0, w, h);
|
||||
|
||||
int wm = w - 1;
|
||||
int hm = h - 1;
|
||||
int wh = w * h;
|
||||
int div = radius + radius + 1;
|
||||
|
||||
int r[] = new int[wh];
|
||||
int g[] = new int[wh];
|
||||
int b[] = new int[wh];
|
||||
int rsum, gsum, bsum, x, y, i, p, yp, yi, yw;
|
||||
int vmin[] = new int[Math.max(w, h)];
|
||||
|
||||
int divsum = (div + 1) >> 1;
|
||||
divsum *= divsum;
|
||||
int dv[] = new int[256 * divsum];
|
||||
for (i = 0; i < 256 * divsum; i++) {
|
||||
dv[i] = (i / divsum);
|
||||
}
|
||||
|
||||
yw = yi = 0;
|
||||
|
||||
int[][] stack = new int[div][3];
|
||||
int stackpointer;
|
||||
int stackstart;
|
||||
int[] sir;
|
||||
int rbs;
|
||||
int r1 = radius + 1;
|
||||
int routsum, goutsum, boutsum;
|
||||
int rinsum, ginsum, binsum;
|
||||
|
||||
for (y = 0; y < h; y++) {
|
||||
rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
|
||||
for (i = -radius; i <= radius; i++) {
|
||||
p = pix[yi + Math.min(wm, Math.max(i, 0))];
|
||||
sir = stack[i + radius];
|
||||
sir[0] = (p & 0xff0000) >> 16;
|
||||
sir[1] = (p & 0x00ff00) >> 8;
|
||||
sir[2] = (p & 0x0000ff);
|
||||
rbs = r1 - Math.abs(i);
|
||||
rsum += sir[0] * rbs;
|
||||
gsum += sir[1] * rbs;
|
||||
bsum += sir[2] * rbs;
|
||||
if (i > 0) {
|
||||
rinsum += sir[0];
|
||||
ginsum += sir[1];
|
||||
binsum += sir[2];
|
||||
} else {
|
||||
routsum += sir[0];
|
||||
goutsum += sir[1];
|
||||
boutsum += sir[2];
|
||||
}
|
||||
}
|
||||
stackpointer = radius;
|
||||
|
||||
for (x = 0; x < w; x++) {
|
||||
|
||||
r[yi] = dv[rsum];
|
||||
g[yi] = dv[gsum];
|
||||
b[yi] = dv[bsum];
|
||||
|
||||
rsum -= routsum;
|
||||
gsum -= goutsum;
|
||||
bsum -= boutsum;
|
||||
|
||||
stackstart = stackpointer - radius + div;
|
||||
sir = stack[stackstart % div];
|
||||
|
||||
routsum -= sir[0];
|
||||
goutsum -= sir[1];
|
||||
boutsum -= sir[2];
|
||||
|
||||
if (y == 0) {
|
||||
vmin[x] = Math.min(x + radius + 1, wm);
|
||||
}
|
||||
p = pix[yw + vmin[x]];
|
||||
|
||||
sir[0] = (p & 0xff0000) >> 16;
|
||||
sir[1] = (p & 0x00ff00) >> 8;
|
||||
sir[2] = (p & 0x0000ff);
|
||||
|
||||
rinsum += sir[0];
|
||||
ginsum += sir[1];
|
||||
binsum += sir[2];
|
||||
|
||||
rsum += rinsum;
|
||||
gsum += ginsum;
|
||||
bsum += binsum;
|
||||
|
||||
stackpointer = (stackpointer + 1) % div;
|
||||
sir = stack[(stackpointer) % div];
|
||||
|
||||
routsum += sir[0];
|
||||
goutsum += sir[1];
|
||||
boutsum += sir[2];
|
||||
|
||||
rinsum -= sir[0];
|
||||
ginsum -= sir[1];
|
||||
binsum -= sir[2];
|
||||
|
||||
yi++;
|
||||
}
|
||||
yw += w;
|
||||
}
|
||||
for (x = 0; x < w; x++) {
|
||||
rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
|
||||
yp = -radius * w;
|
||||
for (i = -radius; i <= radius; i++) {
|
||||
yi = Math.max(0, yp) + x;
|
||||
|
||||
sir = stack[i + radius];
|
||||
|
||||
sir[0] = r[yi];
|
||||
sir[1] = g[yi];
|
||||
sir[2] = b[yi];
|
||||
|
||||
rbs = r1 - Math.abs(i);
|
||||
|
||||
rsum += r[yi] * rbs;
|
||||
gsum += g[yi] * rbs;
|
||||
bsum += b[yi] * rbs;
|
||||
|
||||
if (i > 0) {
|
||||
rinsum += sir[0];
|
||||
ginsum += sir[1];
|
||||
binsum += sir[2];
|
||||
} else {
|
||||
routsum += sir[0];
|
||||
goutsum += sir[1];
|
||||
boutsum += sir[2];
|
||||
}
|
||||
|
||||
if (i < hm) {
|
||||
yp += w;
|
||||
}
|
||||
}
|
||||
yi = x;
|
||||
stackpointer = radius;
|
||||
for (y = 0; y < h; y++) {
|
||||
// Preserve alpha channel: ( 0xff000000 & pix[yi] )
|
||||
pix[yi] = (0xff000000 & pix[yi]) | (dv[rsum] << 16) | (dv[gsum] << 8) | dv[bsum];
|
||||
|
||||
rsum -= routsum;
|
||||
gsum -= goutsum;
|
||||
bsum -= boutsum;
|
||||
|
||||
stackstart = stackpointer - radius + div;
|
||||
sir = stack[stackstart % div];
|
||||
|
||||
routsum -= sir[0];
|
||||
goutsum -= sir[1];
|
||||
boutsum -= sir[2];
|
||||
|
||||
if (x == 0) {
|
||||
vmin[y] = Math.min(y + r1, hm) * w;
|
||||
}
|
||||
p = x + vmin[y];
|
||||
|
||||
sir[0] = r[p];
|
||||
sir[1] = g[p];
|
||||
sir[2] = b[p];
|
||||
|
||||
rinsum += sir[0];
|
||||
ginsum += sir[1];
|
||||
binsum += sir[2];
|
||||
|
||||
rsum += rinsum;
|
||||
gsum += ginsum;
|
||||
bsum += binsum;
|
||||
|
||||
stackpointer = (stackpointer + 1) % div;
|
||||
sir = stack[stackpointer];
|
||||
|
||||
routsum += sir[0];
|
||||
goutsum += sir[1];
|
||||
boutsum += sir[2];
|
||||
|
||||
rinsum -= sir[0];
|
||||
ginsum -= sir[1];
|
||||
binsum -= sir[2];
|
||||
|
||||
yi += w;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bitmap.setPixels(pix, 0, w, 0, 0, w, h);
|
||||
|
||||
return (bitmap);
|
||||
}
|
||||
|
||||
public static Bitmap getRoundedCornerBitmap(Bitmap bitmap, int pixels) {
|
||||
Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap
|
||||
.getHeight(), Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(output);
|
||||
|
||||
final int color = 0xff424242;
|
||||
final Paint paint = new Paint();
|
||||
final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
|
||||
// final ScreenSize rectF = new ScreenSize(rect);
|
||||
final float roundPx = pixels;
|
||||
|
||||
paint.setAntiAlias(true);
|
||||
canvas.drawARGB(0, 0, 0, 0);
|
||||
paint.setColor(color);
|
||||
// canvas.drawRoundRect(rectF, roundPx, roundPx, paint);
|
||||
canvas.drawPath(BitmapEditor.RoundedRect(0, 0, bitmap.getWidth(), bitmap.getHeight(), roundPx, roundPx, false), paint);
|
||||
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
|
||||
canvas.drawBitmap(bitmap, rect, rect, paint);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* getResizedBitmap method is used to Resized the Image according to custom width and height
|
||||
*
|
||||
* @param image
|
||||
* @param newHeight (new desired height)
|
||||
* @param newWidth (new desired Width)
|
||||
* @return image (new resized image)
|
||||
*/
|
||||
public static Bitmap getResizedBitmap(Bitmap image, int newHeight, int newWidth) {
|
||||
int width = image.getWidth();
|
||||
int height = image.getHeight();
|
||||
float scaleWidth = ((float) newWidth) / width;
|
||||
float scaleHeight = ((float) newHeight) / height;
|
||||
// create A matrix for the manipulation
|
||||
Matrix matrix = new Matrix();
|
||||
// onTap the bit map
|
||||
matrix.postScale(scaleWidth, scaleHeight);
|
||||
// recreate the new Bitmap
|
||||
Bitmap resizedBitmap = Bitmap.createBitmap(image, 0, 0, width, height,
|
||||
matrix, false);
|
||||
return resizedBitmap;
|
||||
}
|
||||
|
||||
public static boolean TrueIfBitmapBigger(Bitmap bitmap, int size) {
|
||||
int sizeBitmap = (bitmap.getHeight() > bitmap.getWidth()) ? bitmap.getHeight() : bitmap.getWidth();
|
||||
if (sizeBitmap > size) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Bitmap GetRoundedBitmapWithBlurShadow(Bitmap original, int paddingTop, int paddingBottom, int paddingLeft, int paddingRight) {
|
||||
int original_width = original.getWidth();
|
||||
int orginal_height = original.getHeight();
|
||||
int bitmap_width = original_width + paddingLeft + paddingRight;
|
||||
int bitmap_height = orginal_height + paddingTop + paddingBottom;
|
||||
Bitmap bitmap = Bitmap.createBitmap(bitmap_width, bitmap_height, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
Paint paint = new Paint();
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
//paint.setAlpha(60);
|
||||
// canvas.drawRect(0,0,bitmap_width,bitmap_height,paint);
|
||||
paint.setAntiAlias(true);
|
||||
canvas.drawBitmap(original, paddingLeft, paddingTop, paint);
|
||||
Bitmap blurred_bitmap = getBlurredWithGoodPerformance(bitmap, 1, 6, 4);
|
||||
canvas.setBitmap(null);
|
||||
bitmap.recycle();
|
||||
return blurred_bitmap;
|
||||
}
|
||||
|
||||
// Activity.
|
||||
// | Original bitmap.
|
||||
// | | To make the blur background, the original must to padding.
|
||||
// | | | | | |
|
||||
// V V V V V V
|
||||
public static Bitmap GetRoundedBitmapWithBlurShadow(Context context, Bitmap original, int paddingTop, int paddingBottom, int paddingLeft, int paddingRight,
|
||||
int TopBack // this value makes the overview bitmap is higher or belower the background.
|
||||
, int alphaBlurBackground // this is the alpha of the background Bitmap, you need A number between 0 -> 255, the value recommend is 180.
|
||||
, int valueBlurBackground // this is the value used to blur the background Bitmap, the recommended one is 12.
|
||||
, int valueSaturationBlurBackground // this is the value used to background Bitmap more colorful, if valueBlur is 12, the valudeSaturation should be 2.
|
||||
) {
|
||||
int original_width = original.getWidth();
|
||||
int orginal_height = original.getHeight();
|
||||
int bitmap_width = original_width + paddingLeft + paddingRight;
|
||||
int bitmap_height = orginal_height + paddingTop + paddingBottom;
|
||||
Bitmap bitmap = Bitmap.createBitmap(bitmap_width, bitmap_height, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
Paint paint = new Paint();
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
paint.setAntiAlias(true);
|
||||
canvas.drawBitmap(original, paddingLeft, paddingTop, paint);
|
||||
Bitmap blurred_bitmap = getBlurredWithGoodPerformance(context, bitmap, 1, valueBlurBackground, valueSaturationBlurBackground);
|
||||
// Bitmap blurred_bitmap= getBlurredWithGoodPerformance(context, bitmap,1,15,3);
|
||||
Bitmap end_bitmap = Bitmap.createBitmap(bitmap_width, bitmap_height, Bitmap.Config.ARGB_8888);
|
||||
canvas.setBitmap(end_bitmap);
|
||||
paint.setAlpha(alphaBlurBackground);
|
||||
|
||||
canvas.drawBitmap(blurred_bitmap, new Rect(0, 0, blurred_bitmap.getWidth(), blurred_bitmap.getHeight()), new Rect(0, 0, bitmap_width, bitmap_height), paint);
|
||||
paint.setAlpha(255);
|
||||
|
||||
canvas.drawBitmap(bitmap, 0, TopBack, paint); // drawVisualWave cái lớn
|
||||
canvas.setBitmap(null);
|
||||
blurred_bitmap.recycle();
|
||||
bitmap.recycle();
|
||||
return end_bitmap;
|
||||
}
|
||||
|
||||
public static void setBitmapforImageView(ImageView imv, Bitmap apply) {
|
||||
Bitmap old = ((BitmapDrawable) imv.getDrawable()).getBitmap();
|
||||
imv.setImageBitmap(apply);
|
||||
if (old != null)
|
||||
old.recycle();
|
||||
}
|
||||
|
||||
public static Bitmap getBlurredWithGoodPerformance(Bitmap bitmap, int scale, int radius, int saturation) {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
Bitmap bitmap1 = getResizedBitmap(bitmap, 50, 50);
|
||||
Bitmap updateSatBitmap = updateSat(bitmap1, saturation);
|
||||
Bitmap blurredBitmap = FastBlurSupportAlpha(updateSatBitmap, scale, radius);
|
||||
|
||||
updateSatBitmap.recycle();
|
||||
bitmap1.recycle();
|
||||
return blurredBitmap;
|
||||
}
|
||||
|
||||
static public Path RoundedRect(float left, float top, float right, float bottom, float rx, float ry, boolean conformToOriginalPost) {
|
||||
Path path = new Path();
|
||||
if (rx < 0) rx = 0;
|
||||
if (ry < 0) ry = 0;
|
||||
float width = right - left;
|
||||
float height = bottom - top;
|
||||
if (rx > width / 2) rx = width / 2;
|
||||
if (ry > height / 2) ry = height / 2;
|
||||
float widthMinusCorners = (width - (2 * rx)); // do dai phan "thang" cua chieu rong
|
||||
float heightMinusCorners = (height - (2 * ry)); // do dai phan "thang" cua chieu dai
|
||||
|
||||
path.moveTo(right, top + ry); // bat dau tu day
|
||||
path.rQuadTo(0, -ry, -rx, -ry);//y-right corner
|
||||
path.rLineTo(-widthMinusCorners, 0);
|
||||
path.rQuadTo(-rx, 0, -rx, ry); //y-x corner
|
||||
path.rLineTo(0, heightMinusCorners);
|
||||
|
||||
if (conformToOriginalPost) {
|
||||
path.rLineTo(0, ry);
|
||||
path.rLineTo(width, 0);
|
||||
path.rLineTo(0, -ry);
|
||||
} else {
|
||||
|
||||
path.rQuadTo(0, ry, rx, ry);//bottom-x corner
|
||||
path.rLineTo(widthMinusCorners, 0);
|
||||
path.rQuadTo(rx, 0, rx, -ry); //bottom-right corner
|
||||
}
|
||||
|
||||
path.rLineTo(0, -heightMinusCorners);
|
||||
|
||||
path.close();//Given close, last lineto can be removed.
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
public static int mixTwoColors(int color1, int color2, float amount) {
|
||||
final byte ALPHA_CHANNEL = 24;
|
||||
final byte RED_CHANNEL = 16;
|
||||
final byte GREEN_CHANNEL = 8;
|
||||
final byte BLUE_CHANNEL = 0;
|
||||
|
||||
final float inverseAmount = 1.0f - amount;
|
||||
|
||||
int a = ((int) (((float) (color1 >> ALPHA_CHANNEL & 0xff) * amount) +
|
||||
((float) (color2 >> ALPHA_CHANNEL & 0xff) * inverseAmount))) & 0xff;
|
||||
int r = ((int) (((float) (color1 >> RED_CHANNEL & 0xff) * amount) +
|
||||
((float) (color2 >> RED_CHANNEL & 0xff) * inverseAmount))) & 0xff;
|
||||
int g = ((int) (((float) (color1 >> GREEN_CHANNEL & 0xff) * amount) +
|
||||
((float) (color2 >> GREEN_CHANNEL & 0xff) * inverseAmount))) & 0xff;
|
||||
int b = ((int) (((float) (color1 & 0xff) * amount) +
|
||||
((float) (color2 & 0xff) * inverseAmount))) & 0xff;
|
||||
|
||||
return a << ALPHA_CHANNEL | r << RED_CHANNEL | g << GREEN_CHANNEL | b << BLUE_CHANNEL;
|
||||
}
|
||||
|
||||
public static Bitmap getBlurredWithGoodPerformance(Context context, Bitmap bitmap, int scale, int radius, float saturation) {
|
||||
Bitmap bitmap1 = getResizedBitmap(bitmap, 150, 150);
|
||||
Bitmap updateSatBimap = updateSat(bitmap1, saturation);
|
||||
Bitmap blurredBitmap = BlurBitmapWithRenderScript(context, updateSatBimap, radius);
|
||||
updateSatBimap.recycle();
|
||||
bitmap1.recycle();
|
||||
return blurredBitmap;
|
||||
}
|
||||
|
||||
public static Bitmap getBlurredBimapWithRenderScript(Context context, Bitmap bitmapOriginal, float radius) {
|
||||
//define this only once if blurring multiple times
|
||||
RenderScript rs = RenderScript.create(context);
|
||||
|
||||
//this will blur the bitmapOriginal with A radius of 8 and save it in bitmapOriginal
|
||||
final Allocation input = Allocation.createFromBitmap(rs, bitmapOriginal); //use this constructor for best performance, because it uses USAGE_SHARED mode which reuses memory
|
||||
final Allocation output = Allocation.createTyped(rs, input.getType());
|
||||
final ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
|
||||
script.setRadius(radius);
|
||||
script.setInput(input);
|
||||
script.forEach(output);
|
||||
output.copyTo(bitmapOriginal);
|
||||
return bitmapOriginal;
|
||||
}
|
||||
|
||||
public static Bitmap BlurBitmapWithRenderScript(Context context, Bitmap bitmap, float radius) {
|
||||
//Let's create an empty bitmap with the same size of the bitmap we want to blur
|
||||
Bitmap outBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
|
||||
|
||||
//Instantiate A new Renderscript
|
||||
RenderScript rs = RenderScript.create(context);
|
||||
|
||||
//Create an Intrinsic Blur Script using the Renderscript
|
||||
ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
|
||||
|
||||
//Create the Allocations (in/out) with the Renderscript and the in/out bitmaps
|
||||
Allocation allIn = Allocation.createFromBitmap(rs, bitmap);
|
||||
Allocation allOut = Allocation.createFromBitmap(rs, outBitmap);
|
||||
//Set the radius of the blur
|
||||
blurScript.setRadius(radius);
|
||||
|
||||
//Perform the Renderscript
|
||||
blurScript.setInput(allIn);
|
||||
blurScript.forEach(allOut);
|
||||
|
||||
//Copy the final bitmap created by the out Allocation to the outBitmap
|
||||
allOut.copyTo(outBitmap);
|
||||
|
||||
//recycle the original bitmap
|
||||
|
||||
//After finishing everything, we destroy the Renderscript.
|
||||
rs.destroy();
|
||||
|
||||
return outBitmap;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
public static Drawable covertBitmapToDrawable(Context context, Bitmap bitmap) {
|
||||
Drawable d = new BitmapDrawable(context.getResources(), bitmap);
|
||||
return d;
|
||||
}
|
||||
|
||||
public static Bitmap convertDrawableToBitmap(Drawable drawable) {
|
||||
if (drawable instanceof BitmapDrawable) {
|
||||
return ((BitmapDrawable) drawable).getBitmap();
|
||||
}
|
||||
Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
|
||||
drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||
drawable.draw(canvas);
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
public static Bitmap changeBitmapColor(Bitmap sourceBitmap, int color) {
|
||||
Bitmap resultBitmap = sourceBitmap.copy(sourceBitmap.getConfig(), true);
|
||||
Paint paint = new Paint();
|
||||
ColorFilter filter = new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN);
|
||||
paint.setColorFilter(filter);
|
||||
Canvas canvas = new Canvas(resultBitmap);
|
||||
canvas.drawBitmap(resultBitmap, 0, 0, paint);
|
||||
return resultBitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mode
|
||||
* @return 0 : CLEAR
|
||||
* <br> 1 : SRC
|
||||
* <br> 2 : DST
|
||||
* <br> 3 : SRC_OVER
|
||||
* <br> 4 : DST_OVER
|
||||
* <br> 5 : SRC_IN
|
||||
* <br> 6 : DST_IN
|
||||
* <br> 7 : SRC_OUT
|
||||
* <br> 8 : DST_OUT
|
||||
* <br> 9 : SRC_ATOP
|
||||
* <br>10 : DST_ATOP
|
||||
* <br>11 : XOR
|
||||
* <br>12 : ADD
|
||||
* <br>13 : MULTIPLY
|
||||
* <br>14 : SCREEN
|
||||
* <br>15 : OVERLAY
|
||||
* <br>16 : DARKEN
|
||||
* <br>17 : LIGHTEN
|
||||
*/
|
||||
public static PorterDuff.Mode getPorterMode(int mode) {
|
||||
switch (mode) {
|
||||
default:
|
||||
case 0:
|
||||
return PorterDuff.Mode.CLEAR;
|
||||
case 1:
|
||||
return PorterDuff.Mode.SRC;
|
||||
case 2:
|
||||
return PorterDuff.Mode.DST;
|
||||
case 3:
|
||||
return PorterDuff.Mode.SRC_OVER;
|
||||
case 4:
|
||||
return PorterDuff.Mode.DST_OVER;
|
||||
case 5:
|
||||
return PorterDuff.Mode.SRC_IN;
|
||||
case 6:
|
||||
return PorterDuff.Mode.DST_IN;
|
||||
case 7:
|
||||
return PorterDuff.Mode.SRC_OUT;
|
||||
case 8:
|
||||
return PorterDuff.Mode.DST_OUT;
|
||||
case 9:
|
||||
return PorterDuff.Mode.SRC_ATOP;
|
||||
case 10:
|
||||
return PorterDuff.Mode.DST_ATOP;
|
||||
case 11:
|
||||
return PorterDuff.Mode.XOR;
|
||||
case 16:
|
||||
return PorterDuff.Mode.DARKEN;
|
||||
case 17:
|
||||
return PorterDuff.Mode.LIGHTEN;
|
||||
case 13:
|
||||
return PorterDuff.Mode.MULTIPLY;
|
||||
case 14:
|
||||
return PorterDuff.Mode.SCREEN;
|
||||
case 12:
|
||||
return PorterDuff.Mode.ADD;
|
||||
case 15:
|
||||
return PorterDuff.Mode.OVERLAY;
|
||||
}
|
||||
}
|
||||
|
||||
public static void applyNewColor4Bitmap(Context context, int[] idBitmaps, ImageView[] imageViews, int color, float alpha) {
|
||||
android.content.res.Resources resource = context.getResources();
|
||||
int size = idBitmaps.length;
|
||||
Bitmap usingBitmap, resultBitmap;
|
||||
for (int i = 0; i < size; i++) {
|
||||
usingBitmap = BitmapFactory.decodeResource(resource, idBitmaps[i]);
|
||||
resultBitmap = changeBitmapColor(usingBitmap, color);
|
||||
imageViews[i].setImageBitmap(resultBitmap);
|
||||
imageViews[i].setAlpha(alpha);
|
||||
}
|
||||
}
|
||||
|
||||
public static void applyNewColor4Bitmap(Context context, int idBitmap, ImageView applyView, int color, float alpha) {
|
||||
|
||||
android.content.res.Resources resource = context.getResources();
|
||||
Bitmap usingBitmap = BitmapFactory.decodeResource(resource, idBitmap);
|
||||
Bitmap resultBitmap = changeBitmapColor(usingBitmap, color);
|
||||
applyView.setImageBitmap(resultBitmap);
|
||||
applyView.setAlpha(alpha);
|
||||
|
||||
}
|
||||
|
||||
public static Bitmap getBitmapFromView(View view) {
|
||||
Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
|
||||
Canvas c = new Canvas(bitmap);
|
||||
view.layout(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
|
||||
view.draw(c);
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
public static Bitmap getBitmapFromView(View view, int left, int top, int right, int bottom) {
|
||||
Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
|
||||
Canvas c = new Canvas(bitmap);
|
||||
view.layout(left, top, right, bottom);
|
||||
view.draw(c);
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
public static Bitmap getBackgroundBitmapAViewWithParent(View childView, View parentView) {
|
||||
int[] pos_child = new int[2];
|
||||
childView.getLocationOnScreen(pos_child);
|
||||
return getBitmapFromView(parentView, pos_child[0], pos_child[1], parentView.getRight(), parentView.getBottom());
|
||||
}
|
||||
|
||||
public static Bitmap getBackgroundBlurAViewWithParent(Activity activity, View childView, View parentView) {
|
||||
Bitmap b1 = getBackgroundBitmapAViewWithParent(childView, parentView);
|
||||
Bitmap b2 = getBlurredWithGoodPerformance(activity, b1, 1, 8, 2);
|
||||
b1.recycle();
|
||||
return b2;
|
||||
}
|
||||
}
|
|
@ -1,130 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
package code.name.monkey.retromusic.util;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
|
||||
/**
|
||||
* @author Eugene Cheung (arkon)
|
||||
*/
|
||||
public class CalendarUtil {
|
||||
private static final long MS_PER_MINUTE = 60 * 1000;
|
||||
private static final long MS_PER_DAY = 24 * 60 * MS_PER_MINUTE;
|
||||
|
||||
private Calendar calendar;
|
||||
|
||||
public CalendarUtil() {
|
||||
this.calendar = Calendar.getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the time elapsed so far today in milliseconds.
|
||||
*
|
||||
* @return Time elapsed today in milliseconds.
|
||||
*/
|
||||
public long getElapsedToday() {
|
||||
// Time elapsed so far today
|
||||
return (calendar.get(Calendar.HOUR_OF_DAY) * 60 + calendar.get(Calendar.MINUTE)) * MS_PER_MINUTE
|
||||
+ calendar.get(Calendar.SECOND) * 1000
|
||||
+ calendar.get(Calendar.MILLISECOND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the time elapsed so far this week in milliseconds.
|
||||
*
|
||||
* @return Time elapsed this week in milliseconds.
|
||||
*/
|
||||
public long getElapsedWeek() {
|
||||
// Today + days passed this week
|
||||
long elapsed = getElapsedToday();
|
||||
|
||||
final int passedWeekdays = calendar.get(Calendar.DAY_OF_WEEK) - 1 - calendar.getFirstDayOfWeek();
|
||||
if (passedWeekdays > 0) {
|
||||
elapsed += passedWeekdays * MS_PER_DAY;
|
||||
}
|
||||
|
||||
return elapsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the time elapsed so far this month in milliseconds.
|
||||
*
|
||||
* @return Time elapsed this month in milliseconds.
|
||||
*/
|
||||
public long getElapsedMonth() {
|
||||
// Today + rest of this month
|
||||
return getElapsedToday() +
|
||||
((calendar.get(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.
|
||||
*/
|
||||
public long getElapsedMonths(int numMonths) {
|
||||
// Today + rest of this month
|
||||
long elapsed = getElapsedMonth();
|
||||
|
||||
// Previous numMonths months
|
||||
int month = calendar.get(Calendar.MONTH);
|
||||
int year = calendar.get(Calendar.YEAR);
|
||||
for (int i = 0; i < numMonths; i++) {
|
||||
month--;
|
||||
|
||||
if (month < Calendar.JANUARY) {
|
||||
month = Calendar.DECEMBER;
|
||||
year--;
|
||||
}
|
||||
|
||||
elapsed += getDaysInMonth(month) * MS_PER_DAY;
|
||||
}
|
||||
|
||||
return elapsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the time elapsed so far this year in milliseconds.
|
||||
*
|
||||
* @return Time elapsed this year in milliseconds.
|
||||
*/
|
||||
public long getElapsedYear() {
|
||||
// Today + rest of this month + previous months until January
|
||||
long elapsed = getElapsedMonth();
|
||||
|
||||
int month = calendar.get(Calendar.MONTH) - 1;
|
||||
int year = calendar.get(Calendar.YEAR);
|
||||
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 int getDaysInMonth(int month) {
|
||||
final Calendar monthCal = new GregorianCalendar(calendar.get(Calendar.YEAR), month, 1);
|
||||
return monthCal.getActualMaximum(Calendar.DAY_OF_MONTH);
|
||||
}
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
package code.name.monkey.retromusic.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Created on : June 18, 2016
|
||||
* Author : zetbaitsu
|
||||
* Name : Zetra
|
||||
* GitHub : https://github.com/zetbaitsu
|
||||
*/
|
||||
public class Compressor {
|
||||
//max width and height values of the compressed image is taken as 612x816
|
||||
private int maxWidth = 612;
|
||||
private int maxHeight = 816;
|
||||
private Bitmap.CompressFormat compressFormat = Bitmap.CompressFormat.JPEG;
|
||||
private int quality = 80;
|
||||
private String destinationDirectoryPath;
|
||||
|
||||
public Compressor(Context context) {
|
||||
destinationDirectoryPath = context.getCacheDir().getPath() + File.separator + "images";
|
||||
}
|
||||
|
||||
public Compressor setMaxWidth(int maxWidth) {
|
||||
this.maxWidth = maxWidth;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Compressor setMaxHeight(int maxHeight) {
|
||||
this.maxHeight = maxHeight;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Compressor setCompressFormat(Bitmap.CompressFormat compressFormat) {
|
||||
this.compressFormat = compressFormat;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Compressor setQuality(int quality) {
|
||||
this.quality = quality;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Compressor setDestinationDirectoryPath(String destinationDirectoryPath) {
|
||||
this.destinationDirectoryPath = destinationDirectoryPath;
|
||||
return this;
|
||||
}
|
||||
|
||||
public File compressToFile(File imageFile) throws IOException {
|
||||
return compressToFile(imageFile, imageFile.getName());
|
||||
}
|
||||
|
||||
public File compressToFile(File imageFile, String compressedFileName) throws IOException {
|
||||
return ImageUtil.compressImage(imageFile, maxWidth, maxHeight, compressFormat, quality,
|
||||
destinationDirectoryPath + File.separator + compressedFileName);
|
||||
}
|
||||
|
||||
public Bitmap compressToBitmap(File imageFile) throws IOException {
|
||||
return ImageUtil.decodeSampledBitmapFromFile(imageFile, maxWidth, maxHeight);
|
||||
}
|
||||
}
|
|
@ -1,158 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
package code.name.monkey.retromusic.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.AsyncTask
|
||||
import android.provider.MediaStore
|
||||
import android.widget.Toast
|
||||
import code.name.monkey.retromusic.App
|
||||
import code.name.monkey.retromusic.model.Artist
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.request.animation.GlideAnimation
|
||||
import com.bumptech.glide.request.target.SimpleTarget
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
|
||||
class CustomArtistImageUtil private constructor(context: Context) {
|
||||
|
||||
private val mPreferences: SharedPreferences
|
||||
|
||||
init {
|
||||
mPreferences = context.applicationContext.getSharedPreferences(
|
||||
CUSTOM_ARTIST_IMAGE_PREFS,
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
}
|
||||
|
||||
fun setCustomArtistImage(artist: Artist, uri: Uri) {
|
||||
Glide.with(App.getContext())
|
||||
.load(uri)
|
||||
.asBitmap()
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.into(object : SimpleTarget<Bitmap>() {
|
||||
override fun onLoadFailed(e: Exception?, errorDrawable: Drawable?) {
|
||||
super.onLoadFailed(e, errorDrawable)
|
||||
e!!.printStackTrace()
|
||||
Toast.makeText(App.getContext(), e.toString(), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun onResourceReady(
|
||||
resource: Bitmap,
|
||||
glideAnimation: GlideAnimation<in Bitmap>
|
||||
) {
|
||||
object : AsyncTask<Void, Void, Void>() {
|
||||
@SuppressLint("ApplySharedPref")
|
||||
override fun doInBackground(vararg params: Void): Void? {
|
||||
val dir = File(App.getContext().filesDir, FOLDER_NAME)
|
||||
if (!dir.exists()) {
|
||||
if (!dir.mkdirs()) { // create the folder
|
||||
return null
|
||||
}
|
||||
}
|
||||
val file = File(dir, getFileName(artist))
|
||||
|
||||
var succesful = false
|
||||
try {
|
||||
val os = BufferedOutputStream(FileOutputStream(file))
|
||||
succesful = ImageUtil.resizeBitmap(resource, 2048)
|
||||
.compress(Bitmap.CompressFormat.JPEG, 100, os)
|
||||
os.close()
|
||||
} catch (e: IOException) {
|
||||
Toast.makeText(App.getContext(), e.toString(), Toast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
|
||||
if (succesful) {
|
||||
mPreferences.edit().putBoolean(getFileName(artist), true).commit()
|
||||
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
|
||||
}
|
||||
return null
|
||||
}
|
||||
}.execute()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun resetCustomArtistImage(artist: Artist) {
|
||||
object : AsyncTask<Void, Void, Void>() {
|
||||
@SuppressLint("ApplySharedPref")
|
||||
override fun doInBackground(vararg params: Void): Void? {
|
||||
mPreferences.edit().putBoolean(getFileName(artist), false).commit()
|
||||
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()) {
|
||||
return null
|
||||
} else {
|
||||
file.delete()
|
||||
}
|
||||
return null
|
||||
}
|
||||
}.execute()
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
package code.name.monkey.retromusic.util;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.util.DisplayMetrics;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* Created by hefuyi on 16/7/30.
|
||||
*/
|
||||
public class DensityUtil {
|
||||
|
||||
public static int getScreenHeight(@NonNull Context context) {
|
||||
DisplayMetrics displayMetrics = new DisplayMetrics();
|
||||
((Activity) context).getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
|
||||
return displayMetrics.heightPixels;
|
||||
}
|
||||
|
||||
public static int dip2px(@NonNull Context context, float dpVale) {
|
||||
final float scale = context.getResources().getDisplayMetrics().density;
|
||||
return (int) (dpVale * scale + 0.5f);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,268 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
package code.name.monkey.retromusic.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.os.Environment;
|
||||
import android.provider.MediaStore;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileFilter;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import code.name.monkey.retromusic.loaders.SongLoader;
|
||||
import code.name.monkey.retromusic.loaders.SortedCursor;
|
||||
import code.name.monkey.retromusic.model.Song;
|
||||
|
||||
|
||||
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 ArrayList<Song> matchFilesWithMediaStore(@NonNull Context context,
|
||||
@Nullable List<File> files) {
|
||||
return SongLoader.INSTANCE.getSongs(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 =
|
||||
MediaStore.Audio.AudioColumns.DATA + " IN (" + makePlaceholders(files.size()) + ")";
|
||||
}
|
||||
}
|
||||
|
||||
Cursor songCursor = SongLoader.INSTANCE.makeSongCursor(context, selection, selection == null ? null : paths);
|
||||
|
||||
return songCursor == null ? null
|
||||
: new SortedCursor(songCursor, paths, MediaStore.Audio.AudioColumns.DATA);
|
||||
}
|
||||
|
||||
private static String makePlaceholders(int len) {
|
||||
StringBuilder sb = new StringBuilder(len * 2 - 1);
|
||||
sb.append("?");
|
||||
for (int i = 1; i < len; i++) {
|
||||
sb.append(",?");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
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++) {
|
||||
/*try {
|
||||
paths[i] = files.get(i).getCanonicalPath(); // canonical path is important here because we want to compare the path with the media store entry later
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
paths[i] = files.get(i).getPath();
|
||||
}*/
|
||||
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();
|
||||
|
||||
if (isSDSupportedDevice && isSDPresent) {
|
||||
// yes SD-card is present
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
// Sorry
|
||||
}
|
||||
}
|
||||
|
||||
public static File safeGetCanonicalFile(File file) {
|
||||
try {
|
||||
return file.getCanonicalFile();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return file.getAbsoluteFile();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,287 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
package code.name.monkey.retromusic.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.media.ExifInterface;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import code.name.monkey.appthemehelper.util.TintHelper;
|
||||
|
||||
/**
|
||||
* Created on : June 18, 2016 Author : zetbaitsu Name : Zetra GitHub :
|
||||
* https://github.com/zetbaitsu
|
||||
*/
|
||||
public class ImageUtil {
|
||||
|
||||
private static final int TOLERANCE = 20;
|
||||
// Alpha amount for which values below are considered transparent.
|
||||
private static final int ALPHA_TOLERANCE = 50;
|
||||
private static int[] mTempBuffer;
|
||||
|
||||
private ImageUtil() {
|
||||
|
||||
}
|
||||
|
||||
public static boolean isGrayscale(Bitmap bitmap) {
|
||||
final int height = bitmap.getHeight();
|
||||
final int width = bitmap.getWidth();
|
||||
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;
|
||||
}
|
||||
|
||||
public static Bitmap createBitmap(Drawable drawable) {
|
||||
return createBitmap(drawable, 1f);
|
||||
}
|
||||
|
||||
public static Bitmap createBitmap(Drawable drawable, float sizeMultiplier) {
|
||||
Bitmap bitmap = Bitmap.createBitmap((int) (drawable.getIntrinsicWidth() * sizeMultiplier), (int) (drawable.getIntrinsicHeight() * sizeMultiplier), Bitmap.Config.ARGB_8888);
|
||||
Canvas c = new Canvas(bitmap);
|
||||
drawable.setBounds(0, 0, c.getWidth(), c.getHeight());
|
||||
drawable.draw(c);
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
public static Drawable getTintedVectorDrawable(@NonNull Resources res, @DrawableRes int resId, @Nullable Resources.Theme theme, @ColorInt int color) {
|
||||
return TintHelper.createTintedDrawable(getVectorDrawable(res, resId, theme), color);
|
||||
}
|
||||
|
||||
public static Drawable getTintedVectorDrawable(@NonNull Context context, @DrawableRes int id, @ColorInt int color) {
|
||||
return TintHelper.createTintedDrawable(getVectorDrawable(context.getResources(), id, context.getTheme()), color);
|
||||
}
|
||||
|
||||
public static Drawable getVectorDrawable(@NonNull Context context, @DrawableRes int id) {
|
||||
return getVectorDrawable(context.getResources(), id, context.getTheme());
|
||||
}
|
||||
|
||||
public static Drawable getVectorDrawable(@NonNull Resources res, @DrawableRes int resId, @Nullable Resources.Theme theme) {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
return res.getDrawable(resId, theme);
|
||||
}
|
||||
return VectorDrawableCompat.create(res, resId, theme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure that {@code mTempBuffer} has at least length {@code size}.
|
||||
*/
|
||||
private static void ensureBufferSize(int size) {
|
||||
if (mTempBuffer == null || mTempBuffer.length < size) {
|
||||
mTempBuffer = new int[size];
|
||||
}
|
||||
}
|
||||
|
||||
public static Bitmap setBitmapColor(Bitmap bitmap, int color) {
|
||||
Bitmap result = Bitmap
|
||||
.createBitmap(bitmap, 0, 0, bitmap.getWidth() - 1, bitmap.getHeight() - 1);
|
||||
Paint paint = new Paint();
|
||||
paint.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP));
|
||||
|
||||
Canvas canvas = new Canvas(result);
|
||||
canvas.drawBitmap(result, 0, 0, paint);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
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;
|
||||
} // Amount (max is 255) that two channels can differ before the color is no longer "gray".
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static File compressImage(File imageFile, int reqWidth, int reqHeight,
|
||||
Bitmap.CompressFormat compressFormat, int quality, String destinationPath)
|
||||
throws IOException {
|
||||
FileOutputStream fileOutputStream = null;
|
||||
File file = new File(destinationPath).getParentFile();
|
||||
if (!file.exists()) {
|
||||
file.mkdirs();
|
||||
}
|
||||
try {
|
||||
fileOutputStream = new FileOutputStream(destinationPath);
|
||||
// write the compressed bitmap at the destination specified by destinationPath.
|
||||
decodeSampledBitmapFromFile(imageFile, reqWidth, reqHeight)
|
||||
.compress(compressFormat, quality, fileOutputStream);
|
||||
} finally {
|
||||
if (fileOutputStream != null) {
|
||||
fileOutputStream.flush();
|
||||
fileOutputStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
return new File(destinationPath);
|
||||
}
|
||||
|
||||
static Bitmap decodeSampledBitmapFromFile(File imageFile, int reqWidth, int reqHeight)
|
||||
throws IOException {
|
||||
// First decode with inJustDecodeBounds=true to check dimensions
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeFile(imageFile.getAbsolutePath(), options);
|
||||
|
||||
// Calculate inSampleSize
|
||||
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
|
||||
|
||||
// Decode bitmap with inSampleSize set
|
||||
options.inJustDecodeBounds = false;
|
||||
|
||||
Bitmap scaledBitmap = BitmapFactory.decodeFile(imageFile.getAbsolutePath(), options);
|
||||
|
||||
//check the rotation of the image and display it properly
|
||||
ExifInterface exif;
|
||||
exif = new ExifInterface(imageFile.getAbsolutePath());
|
||||
int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0);
|
||||
Matrix matrix = new Matrix();
|
||||
if (orientation == 6) {
|
||||
matrix.postRotate(90);
|
||||
} else if (orientation == 3) {
|
||||
matrix.postRotate(180);
|
||||
} else if (orientation == 8) {
|
||||
matrix.postRotate(270);
|
||||
}
|
||||
scaledBitmap = Bitmap
|
||||
.createBitmap(scaledBitmap, 0, 0, scaledBitmap.getWidth(), scaledBitmap.getHeight(), matrix,
|
||||
true);
|
||||
return scaledBitmap;
|
||||
}
|
||||
|
||||
private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth,
|
||||
int reqHeight) {
|
||||
// Raw height and width of image
|
||||
final int height = options.outHeight;
|
||||
final int width = options.outWidth;
|
||||
int inSampleSize = 1;
|
||||
|
||||
if (height > reqHeight || 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) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
|
||||
inSampleSize *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
return inSampleSize;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Bitmap getResizedBitmap(@NonNull Bitmap image, int maxSize) {
|
||||
int width = image.getWidth();
|
||||
int height = image.getHeight();
|
||||
|
||||
float bitmapRatio = (float) width / (float) height;
|
||||
if (bitmapRatio > 1) {
|
||||
width = maxSize;
|
||||
height = (int) (width / bitmapRatio);
|
||||
} else {
|
||||
height = maxSize;
|
||||
width = (int) (height * bitmapRatio);
|
||||
}
|
||||
return Bitmap.createScaledBitmap(image, width, height, true);
|
||||
}
|
||||
|
||||
public static Bitmap resize(InputStream stream, int scaledWidth, int scaledHeight) {
|
||||
final Bitmap bitmap = BitmapFactory.decodeStream(stream);
|
||||
return Bitmap.createScaledBitmap(bitmap, scaledWidth, scaledHeight, true);
|
||||
|
||||
}
|
||||
}
|
|
@ -1,125 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
package code.name.monkey.retromusic.util;
|
||||
|
||||
import android.util.Base64;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
/**
|
||||
* Created by hefuyi on 2016/11/8.
|
||||
*/
|
||||
|
||||
public class LyricUtil {
|
||||
|
||||
private static final String lrcRootPath = android.os.Environment
|
||||
.getExternalStorageDirectory().toString() + "/RetroMusic/lyrics/";
|
||||
private static final String TAG = "LyricUtil";
|
||||
|
||||
@Nullable
|
||||
public static File writeLrcToLoc(@NonNull String title, @NonNull String artist, @NonNull String lrcContext) {
|
||||
FileWriter writer = null;
|
||||
try {
|
||||
File file = new File(getLrcPath(title, artist));
|
||||
if (!file.getParentFile().exists()) {
|
||||
file.getParentFile().mkdirs();
|
||||
}
|
||||
writer = new FileWriter(getLrcPath(title, artist));
|
||||
writer.write(lrcContext);
|
||||
return file;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
} finally {
|
||||
try {
|
||||
if (writer != null)
|
||||
writer.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean deleteLrcFile(@NonNull String title, @NonNull String artist) {
|
||||
File file = new File(getLrcPath(title, artist));
|
||||
return file.delete();
|
||||
}
|
||||
|
||||
public static boolean isLrcFileExist(@NonNull String title, @NonNull String artist) {
|
||||
File file = new File(getLrcPath(title, artist));
|
||||
return file.exists();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static File getLocalLyricFile(@NonNull String title, @NonNull String artist) {
|
||||
File file = new File(getLrcPath(title, artist));
|
||||
if (file.exists()) {
|
||||
return file;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String getLrcPath(String title, String artist) {
|
||||
return lrcRootPath + title + " - " + artist + ".lrc";
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String decryptBASE64(@NonNull String str) {
|
||||
if (str == null || str.length() == 0) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
byte[] encode = str.getBytes("UTF-8");
|
||||
// base64 解密
|
||||
return new String(Base64.decode(encode, 0, encode.length, Base64.DEFAULT), "UTF-8");
|
||||
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String getStringFromFile(@NonNull String title, @NonNull String artist) throws Exception {
|
||||
File file = new File(getLrcPath(title, artist));
|
||||
FileInputStream fin = new FileInputStream(file);
|
||||
String ret = convertStreamToString(fin);
|
||||
fin.close();
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static String convertStreamToString(InputStream is) throws Exception {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line = null;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
sb.append(line).append("\n");
|
||||
}
|
||||
reader.close();
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
|
@ -1,121 +0,0 @@
|
|||
package code.name.monkey.retromusic.util
|
||||
|
||||
import android.graphics.*
|
||||
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.shuffled())
|
||||
|
||||
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.createScaledBitmap(bitmap, onePartSize, onePartSize, true)
|
||||
canvas.drawBitmap(
|
||||
bit,
|
||||
(onePartSize * (i % parts)).toFloat(),
|
||||
(onePartSize * (i / parts)).toFloat(),
|
||||
paint
|
||||
)
|
||||
bit.recycle()
|
||||
}
|
||||
|
||||
paint.color = Color.WHITE
|
||||
paint.strokeWidth = 10f
|
||||
|
||||
val oneThirdSize = (IMAGE_SIZE / 3).toFloat()
|
||||
val twoThirdSize = (IMAGE_SIZE / 3 * 2).toFloat()
|
||||
// vertical lines
|
||||
canvas.drawLine(oneThirdSize, 0f, oneThirdSize, imageSize.toFloat(), paint)
|
||||
canvas.drawLine(twoThirdSize, 0f, twoThirdSize, imageSize.toFloat(), paint)
|
||||
// horizontal lines
|
||||
canvas.drawLine(0f, oneThirdSize, imageSize.toFloat(), oneThirdSize, paint)
|
||||
canvas.drawLine(0f, twoThirdSize, imageSize.toFloat(), twoThirdSize, paint)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,428 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
package code.name.monkey.retromusic.util;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.provider.BaseColumns;
|
||||
import android.provider.MediaStore;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.FileProvider;
|
||||
|
||||
import org.jaudiotagger.audio.AudioFileIO;
|
||||
import org.jaudiotagger.tag.FieldKey;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import code.name.monkey.retromusic.R;
|
||||
import code.name.monkey.retromusic.helper.MusicPlayerRemote;
|
||||
import code.name.monkey.retromusic.loaders.PlaylistLoader;
|
||||
import code.name.monkey.retromusic.loaders.SongLoader;
|
||||
import code.name.monkey.retromusic.model.Artist;
|
||||
import code.name.monkey.retromusic.model.Playlist;
|
||||
import code.name.monkey.retromusic.model.Song;
|
||||
import code.name.monkey.retromusic.model.lyrics.AbsSynchronizedLyrics;
|
||||
import code.name.monkey.retromusic.service.MusicService;
|
||||
|
||||
|
||||
public class MusicUtil {
|
||||
|
||||
public static final String TAG = MusicUtil.class.getSimpleName();
|
||||
|
||||
private static Playlist playlist;
|
||||
|
||||
/**
|
||||
* Build a concatenated string from the provided arguments
|
||||
* The intended purpose is to show extra annotations
|
||||
* to a music library item.
|
||||
* Ex: for a given album --> buildInfoString(album.artist, album.songCount)
|
||||
*/
|
||||
@NonNull
|
||||
public static String buildInfoString(@Nullable final String string1, @Nullable final String string2) {
|
||||
// Skip empty strings
|
||||
if (TextUtils.isEmpty(string1)) {
|
||||
//noinspection ConstantConditions
|
||||
return TextUtils.isEmpty(string2) ? "" : string2;
|
||||
}
|
||||
if (TextUtils.isEmpty(string2)) {
|
||||
//noinspection ConstantConditions
|
||||
return TextUtils.isEmpty(string1) ? "" : string1;
|
||||
}
|
||||
|
||||
return string1 + " • " + string2;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static File createAlbumArtFile() {
|
||||
return new File(createAlbumArtDir(), String.valueOf(System.currentTimeMillis()));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Intent createShareSongFileIntent(@NonNull final Song song, @NonNull Context context) {
|
||||
try {
|
||||
return new Intent()
|
||||
.setAction(Intent.ACTION_SEND)
|
||||
.putExtra(Intent.EXTRA_STREAM, FileProvider
|
||||
.getUriForFile(context, context.getApplicationContext().getPackageName(),
|
||||
new File(song.getData())))
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
.setType("audio/*");
|
||||
} catch (IllegalArgumentException e) {
|
||||
// TODO the path is most likely not like /storage/emulated/0/... but something like /storage/28C7-75B0/...
|
||||
e.printStackTrace();
|
||||
Toast.makeText(context, "Could not share this file, I'm aware of the issue.", Toast.LENGTH_SHORT).show();
|
||||
return new Intent();
|
||||
}
|
||||
}
|
||||
|
||||
public static void deleteAlbumArt(@NonNull Context context, int albumId) {
|
||||
ContentResolver contentResolver = context.getContentResolver();
|
||||
Uri localUri = Uri.parse("content://media/external/audio/albumart");
|
||||
contentResolver.delete(ContentUris.withAppendedId(localUri, albumId), null, null);
|
||||
contentResolver.notifyChange(localUri, null);
|
||||
}
|
||||
|
||||
public static void deleteTracks(
|
||||
@NonNull final Activity activity,
|
||||
@NonNull final List<Song> songs,
|
||||
@Nullable final List<Uri> safUris,
|
||||
@Nullable final Runnable callback) {
|
||||
final String[] projection = new String[]{
|
||||
BaseColumns._ID, MediaStore.MediaColumns.DATA
|
||||
};
|
||||
|
||||
// Split the query into multiple batches, and merge the resulting cursors
|
||||
int batchStart = 0;
|
||||
int batchEnd = 0;
|
||||
final int batchSize = 1000000
|
||||
/ 10; // 10^6 being the SQLite limite on the query lenth in bytes, 10 being the max number of digits in an int, used to store the track ID
|
||||
final int songCount = songs.size();
|
||||
|
||||
while (batchEnd < songCount) {
|
||||
batchStart = batchEnd;
|
||||
|
||||
final StringBuilder selection = new StringBuilder();
|
||||
selection.append(BaseColumns._ID + " IN (");
|
||||
|
||||
for (int i = 0; (i < batchSize - 1) && (batchEnd < songCount - 1); i++, batchEnd++) {
|
||||
selection.append(songs.get(batchEnd).getId());
|
||||
selection.append(",");
|
||||
}
|
||||
// The last element of a batch
|
||||
selection.append(songs.get(batchEnd).getId());
|
||||
batchEnd++;
|
||||
selection.append(")");
|
||||
|
||||
try {
|
||||
final Cursor cursor = activity.getContentResolver().query(
|
||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection.toString(),
|
||||
null, null);
|
||||
// TODO: At this point, there is no guarantee that the size of the cursor is the same as the size of the selection string.
|
||||
// Despite that, the Step 3 assumes that the safUris elements are tracking closely the content of the cursor.
|
||||
|
||||
if (cursor != null) {
|
||||
// Step 1: Remove selected tracks from the current playlist, as well
|
||||
// as from the album art cache
|
||||
cursor.moveToFirst();
|
||||
while (!cursor.isAfterLast()) {
|
||||
final int id = cursor.getInt(0);
|
||||
final Song song = SongLoader.getSong(activity, id);
|
||||
MusicPlayerRemote.removeFromQueue(song);
|
||||
cursor.moveToNext();
|
||||
}
|
||||
|
||||
// Step 2: Remove selected tracks from the database
|
||||
activity.getContentResolver().delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||
selection.toString(), null);
|
||||
|
||||
// Step 3: Remove files from card
|
||||
cursor.moveToFirst();
|
||||
int i = batchStart;
|
||||
while (!cursor.isAfterLast()) {
|
||||
final String name = cursor.getString(1);
|
||||
final Uri safUri = safUris == null || safUris.size() <= i ? null : safUris.get(i);
|
||||
SAFUtil.delete(activity, name, safUri);
|
||||
i++;
|
||||
cursor.moveToNext();
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
} catch (SecurityException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
activity.getContentResolver().notifyChange(Uri.parse("content://media"), null);
|
||||
|
||||
activity.runOnUiThread(() -> {
|
||||
Toast.makeText(activity, activity.getString(R.string.deleted_x_songs, songCount), Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
if (callback != null) {
|
||||
callback.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String getArtistInfoString(@NonNull final Context context,
|
||||
@NonNull final Artist artist) {
|
||||
int albumCount = artist.getAlbumCount();
|
||||
int songCount = artist.getSongCount();
|
||||
String albumString = albumCount == 1 ? context.getResources().getString(R.string.album)
|
||||
: context.getResources().getString(R.string.albums);
|
||||
String songString = songCount == 1 ? context.getResources().getString(R.string.song)
|
||||
: context.getResources().getString(R.string.songs);
|
||||
return albumCount + " " + albumString + " • " + songCount + " " + songString;
|
||||
}
|
||||
|
||||
//iTunes uses for example 1002 for track 2 CD1 or 3011 for track 11 CD3.
|
||||
//this method converts those values to normal tracknumbers
|
||||
public static int getFixedTrackNumber(int trackNumberToFix) {
|
||||
return trackNumberToFix % 1000;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getLyrics(@NonNull Song song) {
|
||||
String lyrics = null;
|
||||
|
||||
File file = new File(song.getData());
|
||||
|
||||
try {
|
||||
lyrics = AudioFileIO.read(file).getTagOrCreateDefault().getFirst(FieldKey.LYRICS);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
if (lyrics == null || lyrics.trim().isEmpty() || !AbsSynchronizedLyrics
|
||||
.isSynchronized(lyrics)) {
|
||||
File dir = file.getAbsoluteFile().getParentFile();
|
||||
|
||||
if (dir != null && dir.exists() && dir.isDirectory()) {
|
||||
String format = ".*%s.*\\.(lrc|txt)";
|
||||
String filename = Pattern.quote(FileUtil.stripExtension(file.getName()));
|
||||
String songtitle = Pattern.quote(song.getTitle());
|
||||
|
||||
final ArrayList<Pattern> patterns = new ArrayList<>();
|
||||
patterns.add(Pattern.compile(String.format(format, filename),
|
||||
Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE));
|
||||
patterns.add(Pattern.compile(String.format(format, songtitle),
|
||||
Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE));
|
||||
|
||||
File[] files = dir.listFiles(f -> {
|
||||
for (Pattern pattern : patterns) {
|
||||
if (pattern.matcher(f.getName()).matches()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (files != null && files.length > 0) {
|
||||
for (File f : files) {
|
||||
try {
|
||||
String newLyrics = FileUtil.read(f);
|
||||
if (newLyrics != null && !newLyrics.trim().isEmpty()) {
|
||||
if (AbsSynchronizedLyrics.isSynchronized(newLyrics)) {
|
||||
return newLyrics;
|
||||
}
|
||||
lyrics = newLyrics;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lyrics;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Uri getMediaStoreAlbumCoverUri(int albumId) {
|
||||
final Uri sArtworkUri = Uri.parse("content://media/external/audio/albumart");
|
||||
return ContentUris.withAppendedId(sArtworkUri, albumId);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Playlist getPlaylist() {
|
||||
return playlist;
|
||||
}
|
||||
|
||||
public static void setPlaylist(@NonNull Playlist playlist) {
|
||||
MusicUtil.playlist = playlist;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String getPlaylistInfoString(@NonNull final Context context, @NonNull List<Song> songs) {
|
||||
final long duration = getTotalDuration(songs);
|
||||
|
||||
return MusicUtil.buildInfoString(
|
||||
MusicUtil.getSongCountString(context, songs.size()),
|
||||
MusicUtil.getReadableDurationString(duration)
|
||||
);
|
||||
}
|
||||
|
||||
public static String getReadableDurationString(long songDurationMillis) {
|
||||
long minutes = (songDurationMillis / 1000) / 60;
|
||||
long seconds = (songDurationMillis / 1000) % 60;
|
||||
if (minutes < 60) {
|
||||
return String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds);
|
||||
} else {
|
||||
long hours = minutes / 60;
|
||||
minutes = minutes % 60;
|
||||
return String.format(Locale.getDefault(), "%02d:%02d:%02d", hours, minutes, seconds);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String getSectionName(@Nullable String musicMediaTitle) {
|
||||
try {
|
||||
if (TextUtils.isEmpty(musicMediaTitle)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
musicMediaTitle = musicMediaTitle.trim().toLowerCase();
|
||||
if (musicMediaTitle.startsWith("the ")) {
|
||||
musicMediaTitle = musicMediaTitle.substring(4);
|
||||
} else if (musicMediaTitle.startsWith("a ")) {
|
||||
musicMediaTitle = musicMediaTitle.substring(2);
|
||||
}
|
||||
if (musicMediaTitle.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return musicMediaTitle.substring(0, 1).toUpperCase();
|
||||
} catch (Exception e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String getSongCountString(@NonNull final Context context, int songCount) {
|
||||
final String songString = songCount == 1 ? context.getResources().getString(R.string.song)
|
||||
: context.getResources().getString(R.string.songs);
|
||||
return songCount + " " + songString;
|
||||
}
|
||||
|
||||
public static Uri getSongFileUri(int songId) {
|
||||
return ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, songId);
|
||||
}
|
||||
|
||||
public static long getTotalDuration(@NonNull List<Song> songs) {
|
||||
long duration = 0;
|
||||
for (int i = 0; i < songs.size(); i++) {
|
||||
duration += songs.get(i).getDuration();
|
||||
}
|
||||
return duration;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String getYearString(int year) {
|
||||
return year > 0 ? String.valueOf(year) : "-";
|
||||
}
|
||||
|
||||
public static int indexOfSongInList(@NonNull List<Song> songs, int songId) {
|
||||
for (int i = 0; i < songs.size(); i++) {
|
||||
if (songs.get(i).getId() == songId) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static void insertAlbumArt(@NonNull Context context, int albumId, String path) {
|
||||
ContentResolver contentResolver = context.getContentResolver();
|
||||
|
||||
Uri artworkUri = Uri.parse("content://media/external/audio/albumart");
|
||||
contentResolver.delete(ContentUris.withAppendedId(artworkUri, albumId), null, null);
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("album_id", albumId);
|
||||
values.put("_data", path);
|
||||
|
||||
contentResolver.insert(artworkUri, values);
|
||||
contentResolver.notifyChange(artworkUri, null);
|
||||
}
|
||||
|
||||
public static boolean isArtistNameUnknown(@Nullable String artistName) {
|
||||
if (TextUtils.isEmpty(artistName)) {
|
||||
return false;
|
||||
}
|
||||
if (artistName.equals(Artist.UNKNOWN_ARTIST_DISPLAY_NAME)) {
|
||||
return true;
|
||||
}
|
||||
String tempName = artistName.trim().toLowerCase();
|
||||
return tempName.equals("unknown") || tempName.equals("<unknown>");
|
||||
}
|
||||
|
||||
public static boolean isFavorite(@NonNull final Context context, @NonNull final Song song) {
|
||||
return PlaylistsUtil
|
||||
.doPlaylistContains(context, getFavoritesPlaylist(context).id, song.getId());
|
||||
}
|
||||
|
||||
public static boolean isFavoritePlaylist(@NonNull final Context context,
|
||||
@NonNull final Playlist playlist) {
|
||||
return playlist.name != null && playlist.name.equals(context.getString(R.string.favorites));
|
||||
}
|
||||
|
||||
public static void toggleFavorite(@NonNull final Context context, @NonNull final Song song) {
|
||||
if (isFavorite(context, song)) {
|
||||
PlaylistsUtil.removeFromPlaylist(context, song, getFavoritesPlaylist(context).id);
|
||||
} else {
|
||||
PlaylistsUtil.addToPlaylist(context, song, getOrCreateFavoritesPlaylist(context).id,
|
||||
false);
|
||||
}
|
||||
context.sendBroadcast(new Intent(MusicService.FAVORITE_STATE_CHANGED));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
private static File createAlbumArtDir() {
|
||||
File albumArtDir = new File(Environment.getExternalStorageDirectory(), "/albumthumbs/");
|
||||
if (!albumArtDir.exists()) {
|
||||
albumArtDir.mkdirs();
|
||||
try {
|
||||
new File(albumArtDir, ".nomedia").createNewFile();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return albumArtDir;
|
||||
}
|
||||
|
||||
private static Playlist getFavoritesPlaylist(@NonNull final Context context) {
|
||||
return PlaylistLoader.INSTANCE.getPlaylist(context, context.getString(R.string.favorites));
|
||||
}
|
||||
|
||||
private static Playlist getOrCreateFavoritesPlaylist(@NonNull final Context context) {
|
||||
return PlaylistLoader.INSTANCE.getPlaylist(context,
|
||||
PlaylistsUtil.createPlaylist(context, context.getString(R.string.favorites)));
|
||||
}
|
||||
}
|
|
@ -1,191 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
package code.name.monkey.retromusic.util;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.ActivityOptions;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.media.audiofx.AudioEffect;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import code.name.monkey.retromusic.R;
|
||||
import code.name.monkey.retromusic.activities.AboutActivity;
|
||||
import code.name.monkey.retromusic.activities.albums.AlbumDetailsActivity;
|
||||
import code.name.monkey.retromusic.activities.artists.ArtistDetailActivity;
|
||||
import code.name.monkey.retromusic.activities.DriveModeActivity;
|
||||
import code.name.monkey.retromusic.activities.GenreDetailsActivity;
|
||||
import code.name.monkey.retromusic.activities.LicenseActivity;
|
||||
import code.name.monkey.retromusic.activities.LyricsActivity;
|
||||
import code.name.monkey.retromusic.activities.PlayingQueueActivity;
|
||||
import code.name.monkey.retromusic.activities.PlaylistDetailActivity;
|
||||
import code.name.monkey.retromusic.activities.PurchaseActivity;
|
||||
import code.name.monkey.retromusic.activities.SearchActivity;
|
||||
import code.name.monkey.retromusic.activities.SettingsActivity;
|
||||
import code.name.monkey.retromusic.activities.SupportDevelopmentActivity;
|
||||
import code.name.monkey.retromusic.activities.UserInfoActivity;
|
||||
import code.name.monkey.retromusic.activities.WhatsNewActivity;
|
||||
import code.name.monkey.retromusic.activities.bugreport.BugReportActivity;
|
||||
import code.name.monkey.retromusic.helper.MusicPlayerRemote;
|
||||
import code.name.monkey.retromusic.model.Genre;
|
||||
import code.name.monkey.retromusic.model.Playlist;
|
||||
|
||||
import static code.name.monkey.retromusic.Constants.RATE_ON_GOOGLE_PLAY;
|
||||
import static code.name.monkey.retromusic.util.RetroUtil.openUrl;
|
||||
|
||||
|
||||
public class NavigationUtil {
|
||||
|
||||
public static void bugReport(@NonNull Activity activity) {
|
||||
ActivityCompat.startActivity(activity, new Intent(activity, BugReportActivity.class), null);
|
||||
}
|
||||
|
||||
public static void goToAbout(@NonNull Activity activity) {
|
||||
ActivityCompat.startActivity(activity, new Intent(activity, AboutActivity.class), null);
|
||||
}
|
||||
|
||||
public static void goToAlbum(@NonNull Activity activity, int albumId) {
|
||||
Intent intent = new Intent(activity, AlbumDetailsActivity.class);
|
||||
intent.putExtra(AlbumDetailsActivity.EXTRA_ALBUM_ID, albumId);
|
||||
ActivityCompat.startActivity(activity, intent, null);
|
||||
}
|
||||
|
||||
public static void goToAlbumOptions(@NonNull Activity activity,
|
||||
int albumId,
|
||||
@NonNull ActivityOptions options) {
|
||||
Intent intent = new Intent(activity, AlbumDetailsActivity.class);
|
||||
intent.putExtra(AlbumDetailsActivity.EXTRA_ALBUM_ID, albumId);
|
||||
ActivityCompat.startActivity(activity, intent, options.toBundle());
|
||||
}
|
||||
|
||||
public static void goToArtist(@NonNull Activity activity, int i) {
|
||||
Intent intent = new Intent(activity, ArtistDetailActivity.class);
|
||||
intent.putExtra(ArtistDetailActivity.EXTRA_ARTIST_ID, i);
|
||||
ActivityCompat.startActivity(activity, intent, null);
|
||||
}
|
||||
|
||||
public static void goToArtistOptions(@NotNull Activity activity,
|
||||
int artistId,
|
||||
@NonNull ActivityOptions options) {
|
||||
|
||||
Intent intent = new Intent(activity, ArtistDetailActivity.class);
|
||||
intent.putExtra(ArtistDetailActivity.EXTRA_ARTIST_ID, artistId);
|
||||
ActivityCompat.startActivity(activity, intent, options.toBundle());
|
||||
}
|
||||
|
||||
public static void goToGenre(@NonNull Activity activity, @NonNull Genre genre) {
|
||||
Intent intent = new Intent(activity, GenreDetailsActivity.class);
|
||||
intent.putExtra(GenreDetailsActivity.EXTRA_GENRE_ID, genre);
|
||||
ActivityCompat.startActivity(activity, intent, null);
|
||||
}
|
||||
|
||||
public static void goToLyrics(@NonNull Activity activity) {
|
||||
Intent intent = new Intent(activity, LyricsActivity.class);
|
||||
ActivityCompat.startActivity(activity, intent, null);
|
||||
}
|
||||
|
||||
public static void goToOpenSource(@NonNull Activity activity) {
|
||||
ActivityCompat.startActivity(activity, new Intent(activity, LicenseActivity.class), null);
|
||||
}
|
||||
|
||||
public static void goToPlayStore(@NonNull Activity activity) {
|
||||
openUrl(activity, RATE_ON_GOOGLE_PLAY);
|
||||
}
|
||||
|
||||
public static void goToPlayingQueue(@NonNull Activity activity) {
|
||||
Intent intent = new Intent(activity, PlayingQueueActivity.class);
|
||||
ActivityCompat.startActivity(activity, intent, null);
|
||||
}
|
||||
|
||||
public static void goToPlaylistNew(@NonNull Activity activity, @NonNull Playlist playlist) {
|
||||
Intent intent = new Intent(activity, PlaylistDetailActivity.class);
|
||||
intent.putExtra(PlaylistDetailActivity.Companion.getEXTRA_PLAYLIST(), playlist);
|
||||
ActivityCompat.startActivity(activity, intent, null);
|
||||
}
|
||||
|
||||
public static void goToProVersion(@NonNull Context context) {
|
||||
ActivityCompat.startActivity(context, new Intent(context, PurchaseActivity.class), null);
|
||||
}
|
||||
|
||||
public static void goToSearch(@NonNull Activity activity,
|
||||
@NonNull ActivityOptions activityOptions) {
|
||||
ActivityCompat.startActivity(activity, new Intent(activity, SearchActivity.class),
|
||||
activityOptions.toBundle());
|
||||
}
|
||||
|
||||
public static void goToSearch(@NonNull Activity activity) {
|
||||
ActivityCompat.startActivity(activity, new Intent(activity, SearchActivity.class),
|
||||
null);
|
||||
}
|
||||
|
||||
public static void goToSearch(@NonNull Activity activity, boolean isMicOpen,
|
||||
@NonNull ActivityOptions activityOptions) {
|
||||
ActivityCompat.startActivity(activity, new Intent(activity, SearchActivity.class)
|
||||
.putExtra(SearchActivity.EXTRA_SHOW_MIC, isMicOpen),
|
||||
activityOptions.toBundle());
|
||||
}
|
||||
|
||||
public static void goToSettings(@NonNull Activity activity) {
|
||||
ActivityCompat.startActivity(activity, new Intent(activity, SettingsActivity.class), null);
|
||||
}
|
||||
|
||||
public static void goToSupportDevelopment(@NonNull Activity activity) {
|
||||
ActivityCompat.startActivity(activity, new Intent(activity, SupportDevelopmentActivity.class), null);
|
||||
}
|
||||
|
||||
public static void goToUserInfo(@NonNull Activity activity,
|
||||
@NonNull ActivityOptions activityOptions) {
|
||||
ActivityCompat.startActivity(activity, new Intent(activity, UserInfoActivity.class),
|
||||
activityOptions.toBundle());
|
||||
}
|
||||
|
||||
public static void gotoDriveMode(@NotNull final Activity activity) {
|
||||
ActivityCompat.startActivity(activity, new Intent(activity, DriveModeActivity.class), null);
|
||||
}
|
||||
|
||||
public static void gotoWhatNews(@NonNull Activity activity) {
|
||||
ActivityCompat.startActivity(activity, new Intent(activity, WhatsNewActivity.class), null);
|
||||
}
|
||||
|
||||
public static void openEqualizer(@NonNull final Activity activity) {
|
||||
stockEqalizer(activity);
|
||||
}
|
||||
|
||||
private static void stockEqalizer(@NonNull Activity activity) {
|
||||
final int sessionId = MusicPlayerRemote.INSTANCE.getAudioSessionId();
|
||||
if (sessionId == AudioEffect.ERROR_BAD_VALUE) {
|
||||
Toast.makeText(activity, activity.getResources().getString(R.string.no_audio_ID),
|
||||
Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
try {
|
||||
final Intent effects = new 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 (@NonNull final ActivityNotFoundException notFound) {
|
||||
Toast.makeText(activity, activity.getResources().getString(R.string.no_equalizer),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,276 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
package code.name.monkey.retromusic.util;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.provider.BaseColumns;
|
||||
import android.provider.MediaStore;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import code.name.monkey.retromusic.R;
|
||||
import code.name.monkey.retromusic.helper.M3UWriter;
|
||||
import code.name.monkey.retromusic.model.Playlist;
|
||||
import code.name.monkey.retromusic.model.PlaylistSong;
|
||||
import code.name.monkey.retromusic.model.Song;
|
||||
|
||||
import static android.provider.MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI;
|
||||
|
||||
public class PlaylistsUtil {
|
||||
|
||||
public static int createPlaylist(@NonNull final Context context, @Nullable final String name) {
|
||||
int id = -1;
|
||||
if (name != null && name.length() > 0) {
|
||||
try {
|
||||
Cursor cursor = context.getContentResolver().query(EXTERNAL_CONTENT_URI,
|
||||
new String[]{MediaStore.Audio.Playlists._ID},
|
||||
MediaStore.Audio.PlaylistsColumns.NAME + "=?", new String[]{name},
|
||||
null);
|
||||
if (cursor == null || cursor.getCount() < 1) {
|
||||
final ContentValues values = new ContentValues(1);
|
||||
values.put(MediaStore.Audio.PlaylistsColumns.NAME, name);
|
||||
final Uri uri = context.getContentResolver().insert(
|
||||
EXTERNAL_CONTENT_URI,
|
||||
values);
|
||||
if (uri != null) {
|
||||
// Necessary because somehow the MediaStoreObserver is not notified when adding a playlist
|
||||
context.getContentResolver().notifyChange(Uri.parse("content://media"), null);
|
||||
Toast.makeText(context, context.getResources().getString(
|
||||
R.string.created_playlist_x, name), Toast.LENGTH_SHORT).show();
|
||||
id = Integer.parseInt(uri.getLastPathSegment());
|
||||
}
|
||||
} else {
|
||||
// Playlist exists
|
||||
if (cursor.moveToFirst()) {
|
||||
id = cursor.getInt(cursor.getColumnIndex(MediaStore.Audio.Playlists._ID));
|
||||
}
|
||||
}
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
if (id == -1) {
|
||||
Toast.makeText(context, context.getResources().getString(
|
||||
R.string.could_not_create_playlist), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
public static void deletePlaylists(@NonNull final Context context, @NonNull final List<Playlist> playlists) {
|
||||
final StringBuilder selection = new StringBuilder();
|
||||
selection.append(MediaStore.Audio.Playlists._ID + " IN (");
|
||||
for (int i = 0; i < playlists.size(); i++) {
|
||||
selection.append(playlists.get(i).id);
|
||||
if (i < playlists.size() - 1) {
|
||||
selection.append(",");
|
||||
}
|
||||
}
|
||||
selection.append(")");
|
||||
try {
|
||||
context.getContentResolver().delete(EXTERNAL_CONTENT_URI, selection.toString(), null);
|
||||
context.getContentResolver().notifyChange(Uri.parse("content://media"), null);
|
||||
} catch (SecurityException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public static void addToPlaylist(@NonNull final Context context, final Song song, final int playlistId, final boolean showToastOnFinish) {
|
||||
List<Song> helperList = new ArrayList<>();
|
||||
helperList.add(song);
|
||||
addToPlaylist(context, helperList, playlistId, showToastOnFinish);
|
||||
}
|
||||
|
||||
public static void addToPlaylist(@NonNull final Context context, @NonNull final List<Song> songs, final int playlistId, final boolean showToastOnFinish) {
|
||||
final int size = songs.size();
|
||||
final ContentResolver resolver = context.getContentResolver();
|
||||
final String[] projection = new String[]{
|
||||
"max(" + MediaStore.Audio.Playlists.Members.PLAY_ORDER + ")",
|
||||
};
|
||||
final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
|
||||
Cursor cursor = null;
|
||||
int base = 0;
|
||||
|
||||
try {
|
||||
try {
|
||||
cursor = resolver.query(uri, projection, null, null, null);
|
||||
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
base = cursor.getInt(0) + 1;
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
int numInserted = 0;
|
||||
for (int offSet = 0; offSet < size; offSet += 1000)
|
||||
numInserted += resolver.bulkInsert(uri, makeInsertItems(songs, offSet, 1000, base));
|
||||
|
||||
if (showToastOnFinish) {
|
||||
Toast.makeText(context, context.getResources().getString(
|
||||
R.string.inserted_x_songs_into_playlist_x, numInserted, getNameForPlaylist(context, playlistId)), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} catch (SecurityException ignored) {
|
||||
ignored.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static ContentValues[] makeInsertItems(@NonNull final List<Song> songs, final int offset, int len, final int base) {
|
||||
if (offset + len > songs.size()) {
|
||||
len = songs.size() - offset;
|
||||
}
|
||||
|
||||
ContentValues[] contentValues = new ContentValues[len];
|
||||
|
||||
for (int i = 0; i < len; i++) {
|
||||
contentValues[i] = new ContentValues();
|
||||
contentValues[i].put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, base + offset + i);
|
||||
contentValues[i].put(MediaStore.Audio.Playlists.Members.AUDIO_ID, songs.get(offset + i).getId());
|
||||
}
|
||||
return contentValues;
|
||||
}
|
||||
|
||||
public static String getNameForPlaylist(@NonNull final Context context, final long id) {
|
||||
try {
|
||||
Cursor cursor = context.getContentResolver().query(EXTERNAL_CONTENT_URI,
|
||||
new String[]{MediaStore.Audio.PlaylistsColumns.NAME},
|
||||
BaseColumns._ID + "=?",
|
||||
new String[]{String.valueOf(id)},
|
||||
null);
|
||||
if (cursor != null) {
|
||||
try {
|
||||
if (cursor.moveToFirst()) {
|
||||
return cursor.getString(0);
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
} catch (SecurityException ignored) {
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public static void removeFromPlaylist(@NonNull final Context context, @NonNull final Song song, int playlistId) {
|
||||
Uri uri = MediaStore.Audio.Playlists.Members.getContentUri(
|
||||
"external", playlistId);
|
||||
String selection = MediaStore.Audio.Playlists.Members.AUDIO_ID + " =?";
|
||||
String[] selectionArgs = new String[]{String.valueOf(song.getId())};
|
||||
|
||||
try {
|
||||
context.getContentResolver().delete(uri, selection, selectionArgs);
|
||||
} catch (SecurityException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeFromPlaylist(@NonNull final Context context, @NonNull final List<PlaylistSong> songs) {
|
||||
final int playlistId = songs.get(0).getPlaylistId();
|
||||
Uri uri = MediaStore.Audio.Playlists.Members.getContentUri(
|
||||
"external", playlistId);
|
||||
String selectionArgs[] = new String[songs.size()];
|
||||
for (int i = 0; i < selectionArgs.length; i++) {
|
||||
selectionArgs[i] = String.valueOf(songs.get(i).getIdInPlayList());
|
||||
}
|
||||
String selection = MediaStore.Audio.Playlists.Members._ID + " in (";
|
||||
//noinspection unused
|
||||
for (String selectionArg : selectionArgs) selection += "?, ";
|
||||
selection = selection.substring(0, selection.length() - 2) + ")";
|
||||
|
||||
try {
|
||||
context.getContentResolver().delete(uri, selection, selectionArgs);
|
||||
} catch (SecurityException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean doPlaylistContains(@NonNull final Context context, final long playlistId, final int songId) {
|
||||
if (playlistId != -1) {
|
||||
try {
|
||||
Cursor c = context.getContentResolver().query(
|
||||
MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId),
|
||||
new String[]{MediaStore.Audio.Playlists.Members.AUDIO_ID}, MediaStore.Audio.Playlists.Members.AUDIO_ID + "=?", new String[]{String.valueOf(songId)}, null);
|
||||
int count = 0;
|
||||
if (c != null) {
|
||||
count = c.getCount();
|
||||
c.close();
|
||||
}
|
||||
return count > 0;
|
||||
} catch (SecurityException ignored) {
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean moveItem(@NonNull final Context context, int playlistId, int from, int to) {
|
||||
return MediaStore.Audio.Playlists.Members.moveItem(context.getContentResolver(),
|
||||
playlistId, from, to);
|
||||
}
|
||||
|
||||
public static void renamePlaylist(@NonNull final Context context, final long id, final String newName) {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(MediaStore.Audio.PlaylistsColumns.NAME, newName);
|
||||
try {
|
||||
context.getContentResolver().update(EXTERNAL_CONTENT_URI,
|
||||
contentValues,
|
||||
MediaStore.Audio.Playlists._ID + "=?",
|
||||
new String[]{String.valueOf(id)});
|
||||
context.getContentResolver().notifyChange(Uri.parse("content://media"), null);
|
||||
} catch (SecurityException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public static File savePlaylist(Context context, Playlist playlist) throws IOException {
|
||||
return M3UWriter.write(context, new File(Environment.getExternalStorageDirectory(), "Playlists"), playlist);
|
||||
}
|
||||
|
||||
public static boolean doesPlaylistExist(@NonNull final Context context, final int playlistId) {
|
||||
return playlistId != -1 && doesPlaylistExist(context,
|
||||
MediaStore.Audio.Playlists._ID + "=?",
|
||||
new String[]{String.valueOf(playlistId)});
|
||||
}
|
||||
|
||||
public static boolean doesPlaylistExist(@NonNull final Context context, final String name) {
|
||||
return doesPlaylistExist(context,
|
||||
MediaStore.Audio.PlaylistsColumns.NAME + "=?",
|
||||
new String[]{name});
|
||||
}
|
||||
|
||||
private static boolean doesPlaylistExist(@NonNull Context context, @NonNull final String selection, @NonNull final String[] values) {
|
||||
Cursor cursor = context.getContentResolver().query(EXTERNAL_CONTENT_URI,
|
||||
new String[]{}, selection, values, null);
|
||||
|
||||
boolean exists = false;
|
||||
if (cursor != null) {
|
||||
exists = cursor.getCount() != 0;
|
||||
cursor.close();
|
||||
}
|
||||
return exists;
|
||||
}
|
||||
}
|
|
@ -1,552 +0,0 @@
|
|||
package code.name.monkey.retromusic.util
|
||||
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkInfo
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.viewpager.widget.ViewPager
|
||||
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.NowPlayingScreen
|
||||
import code.name.monkey.retromusic.fragments.mainactivity.FoldersFragment
|
||||
import code.name.monkey.retromusic.helper.SortOrder.*
|
||||
import code.name.monkey.retromusic.model.CategoryInfo
|
||||
import code.name.monkey.retromusic.transform.*
|
||||
import code.name.monkey.retromusic.util.theme.ThemeMode
|
||||
import com.google.android.material.bottomnavigation.LabelVisibilityMode
|
||||
import com.google.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.Queue, false),
|
||||
CategoryInfo(CategoryInfo.Category.Folder, 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(
|
||||
changeListener: OnSharedPreferenceChangeListener
|
||||
) {
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(changeListener)
|
||||
}
|
||||
|
||||
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.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 get() = sharedPreferences.getString(LANGUAGE_NAME, "auto")
|
||||
|
||||
var userName
|
||||
get() = sharedPreferences.getString(USER_NAME, "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)
|
||||
}
|
||||
|
||||
|
||||
val selectedEqualizer
|
||||
get() = sharedPreferences.getStringOrDefault(
|
||||
CHOOSE_EQUALIZER,
|
||||
"system"
|
||||
)
|
||||
|
||||
val autoDownloadImagesPolicy
|
||||
get() = sharedPreferences.getStringOrDefault(
|
||||
AUTO_DOWNLOAD_IMAGES_POLICY,
|
||||
"only_wifi"
|
||||
)
|
||||
|
||||
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 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,
|
||||
AlbumSortOrder.ALBUM_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
|
||||
)
|
||||
|
||||
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_LOCKSCREEN, 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 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(
|
||||
GAPLESS_PLAYBACK, false
|
||||
)
|
||||
|
||||
val isAdaptiveColor
|
||||
get() = sharedPreferences.getBoolean(
|
||||
ADAPTIVE_COLOR_APP, false
|
||||
)
|
||||
|
||||
val isFullScreenMode
|
||||
get() = sharedPreferences.getBoolean(
|
||||
TOGGLE_FULL_SCREEN, false
|
||||
)
|
||||
|
||||
val isLockScreen get() = sharedPreferences.getBoolean(LOCK_SCREEN, false)
|
||||
|
||||
fun isAllowedToDownloadMetadata(): Boolean {
|
||||
return when (autoDownloadImagesPolicy) {
|
||||
"always" -> true
|
||||
"only_wifi" -> {
|
||||
val connectivityManager = ContextCompat.getSystemService(
|
||||
App.getContext(),
|
||||
ConnectivityManager::class.java
|
||||
)
|
||||
var netInfo: NetworkInfo? = null
|
||||
if (connectivityManager != null) {
|
||||
netInfo = connectivityManager.activeNetworkInfo
|
||||
}
|
||||
netInfo != null && netInfo.type == ConnectivityManager.TYPE_WIFI && netInfo.isConnectedOrConnecting
|
||||
}
|
||||
"never" -> false
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var lyricsOption
|
||||
get() = sharedPreferences.getInt(LYRICS_OPTIONS, 1)
|
||||
set(value) = sharedPreferences.edit {
|
||||
putInt(LYRICS_OPTIONS, value)
|
||||
}
|
||||
|
||||
var songGridStyle
|
||||
get() = sharedPreferences.getInt(SONG_GRID_STYLE, R.layout.item_grid)
|
||||
set(value) = sharedPreferences.edit {
|
||||
putInt(SONG_GRID_STYLE, value)
|
||||
}
|
||||
|
||||
var albumGridStyle
|
||||
get() = sharedPreferences.getInt(ALBUM_GRID_STYLE, R.layout.item_grid)
|
||||
set(value) = sharedPreferences.edit {
|
||||
putInt(ALBUM_GRID_STYLE, value)
|
||||
}
|
||||
|
||||
var artistGridStyle
|
||||
get() = sharedPreferences.getInt(ARTIST_GRID_STYLE, R.layout.item_grid_circle)
|
||||
set(value) = sharedPreferences.edit {
|
||||
putInt(ARTIST_GRID_STYLE, value)
|
||||
}
|
||||
|
||||
val filterLength get() = sharedPreferences.getInt(FILTER_SONG, 20)
|
||||
|
||||
var lastVersion
|
||||
get() = sharedPreferences.getInt(LAST_CHANGELOG_VERSION, 0)
|
||||
set(value) = sharedPreferences.edit {
|
||||
putInt(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 lastPage
|
||||
get() = sharedPreferences.getInt(LAST_PAGE, R.id.action_song)
|
||||
set(value) = sharedPreferences.edit {
|
||||
putInt(LAST_PAGE, 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 homeGridStyle: Int
|
||||
get() {
|
||||
val position =
|
||||
sharedPreferences.getStringOrDefault(
|
||||
HOME_ARTIST_GRID_STYLE, "0"
|
||||
).toInt()
|
||||
val typedArray =
|
||||
App.getContext().resources.obtainTypedArray(R.array.pref_home_grid_style_layout)
|
||||
val layoutRes = typedArray.getResourceId(position, 0)
|
||||
typedArray.recycle()
|
||||
return if (layoutRes == 0) {
|
||||
R.layout.item_artist
|
||||
} else layoutRes
|
||||
}
|
||||
|
||||
val tabTitleMode: Int
|
||||
get() {
|
||||
return when (sharedPreferences.getStringOrDefault(
|
||||
TAB_TEXT_MODE, "1"
|
||||
).toInt()) {
|
||||
1 -> LabelVisibilityMode.LABEL_VISIBILITY_LABELED
|
||||
0 -> LabelVisibilityMode.LABEL_VISIBILITY_AUTO
|
||||
2 -> LabelVisibilityMode.LABEL_VISIBILITY_SELECTED
|
||||
3 -> LabelVisibilityMode.LABEL_VISIBILITY_UNLABELED
|
||||
else -> LabelVisibilityMode.LABEL_VISIBILITY_LABELED
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var songGridSize
|
||||
get() = sharedPreferences.getInt(
|
||||
SONG_GRID_SIZE,
|
||||
App.getContext().getIntRes(R.integer.default_grid_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(
|
||||
ALBUM_GRID_SIZE_LAND,
|
||||
App.getContext().getIntRes(R.integer.default_grid_columns_land)
|
||||
)
|
||||
set(value) = sharedPreferences.edit {
|
||||
putInt(ALBUM_GRID_SIZE_LAND, 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)
|
||||
}
|
||||
|
||||
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 -> NormalPageTransformer()
|
||||
}
|
||||
}
|
||||
|
||||
var startDirectory: File
|
||||
get() {
|
||||
val folderPath = FoldersFragment.getDefaultStartDirectory().path
|
||||
val filePath: String = sharedPreferences.getStringOrDefault(START_DIRECTORY, folderPath)
|
||||
return File(filePath) ?: File(FoldersFragment.getDefaultStartDirectory().path)
|
||||
}
|
||||
set(value) = sharedPreferences.edit {
|
||||
putString(
|
||||
START_DIRECTORY,
|
||||
FileUtil.safeGetCanonicalPath(value)
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
package code.name.monkey.retromusic.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.app.ActivityCompat
|
||||
import code.name.monkey.retromusic.App
|
||||
import code.name.monkey.retromusic.activities.PurchaseActivity
|
||||
|
||||
object PremiumShow {
|
||||
private const val PREF_NAME = "premium_show"
|
||||
private const val LAUNCH_COUNT = "launch_count"
|
||||
private const val DATE_FIRST_LAUNCH = "date_first_launch"
|
||||
|
||||
@JvmStatic
|
||||
fun launch(context: Context) {
|
||||
val pref = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
||||
if (App.isProVersion()) {
|
||||
return
|
||||
}
|
||||
val prefEditor = pref.edit()
|
||||
val launchCount = pref.getLong(LAUNCH_COUNT, 0) + 1
|
||||
prefEditor.putLong(LAUNCH_COUNT, launchCount)
|
||||
|
||||
var dateLaunched = pref.getLong(DATE_FIRST_LAUNCH, 0)
|
||||
if (dateLaunched == 0L) {
|
||||
dateLaunched = System.currentTimeMillis()
|
||||
prefEditor.putLong(DATE_FIRST_LAUNCH, dateLaunched)
|
||||
}
|
||||
if (System.currentTimeMillis() >= dateLaunched + 2 * 24 * 60 * 60 * 1000) {
|
||||
ActivityCompat.startActivity(
|
||||
context,
|
||||
Intent(context, PurchaseActivity::class.java),
|
||||
null
|
||||
)
|
||||
}
|
||||
prefEditor.apply()
|
||||
}
|
||||
}
|
|
@ -1,220 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
package code.name.monkey.retromusic.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.palette.graphics.Palette;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import code.name.monkey.appthemehelper.util.ColorUtil;
|
||||
|
||||
public class RetroColorUtil {
|
||||
public static int desaturateColor(int color, float ratio) {
|
||||
float[] hsv = new float[3];
|
||||
Color.colorToHSV(color, hsv);
|
||||
|
||||
hsv[1] = (hsv[1] / 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<Palette.Swatch>(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;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,199 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
package code.name.monkey.retromusic.util;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.Display;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
import code.name.monkey.appthemehelper.util.TintHelper;
|
||||
import code.name.monkey.retromusic.App;
|
||||
|
||||
public class RetroUtil {
|
||||
|
||||
private static final int[] TEMP_ARRAY = new int[1];
|
||||
|
||||
private static final String SHOW_NAV_BAR_RES_NAME = "config_showNavigationBar";
|
||||
|
||||
public static int calculateNoOfColumns(@NonNull Context context) {
|
||||
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
|
||||
float dpWidth = displayMetrics.widthPixels / displayMetrics.density;
|
||||
return (int) (dpWidth / 180);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Bitmap createBitmap(@NonNull Drawable drawable, float sizeMultiplier) {
|
||||
Bitmap bitmap = Bitmap.createBitmap((int) (drawable.getIntrinsicWidth() * sizeMultiplier),
|
||||
(int) (drawable.getIntrinsicHeight() * sizeMultiplier), Bitmap.Config.ARGB_8888);
|
||||
Canvas c = new Canvas(bitmap);
|
||||
drawable.setBounds(0, 0, c.getWidth(), c.getHeight());
|
||||
drawable.draw(c);
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
public static String formatValue(float value) {
|
||||
String arr[] = {"", "K", "M", "B", "T", "P", "E"};
|
||||
int index = 0;
|
||||
while ((value / 1000) >= 1) {
|
||||
value = value / 1000;
|
||||
index++;
|
||||
}
|
||||
DecimalFormat decimalFormat = new DecimalFormat("#.##");
|
||||
return String.format("%s %s", decimalFormat.format(value), arr[index]);
|
||||
}
|
||||
|
||||
public static float frequencyCount(int frequency) {
|
||||
return (float) (frequency / 1000.0);
|
||||
}
|
||||
|
||||
public static Point getScreenSize(@NonNull Context c) {
|
||||
Display display = null;
|
||||
if (c.getSystemService(Context.WINDOW_SERVICE) != null) {
|
||||
display = ((WindowManager) c.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
|
||||
}
|
||||
Point size = new Point();
|
||||
if (display != null) {
|
||||
display.getSize(size);
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
public static int getStatusBarHeight() {
|
||||
int result = 0;
|
||||
int resourceId = App.Companion.getContext().getResources()
|
||||
.getIdentifier("status_bar_height", "dimen", "android");
|
||||
if (resourceId > 0) {
|
||||
result = App.Companion.getContext().getResources().getDimensionPixelSize(resourceId);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Drawable getTintedVectorDrawable(@NonNull Context context, @DrawableRes int id,
|
||||
@ColorInt int color) {
|
||||
return TintHelper.createTintedDrawable(
|
||||
getVectorDrawable(context.getResources(), id, context.getTheme()), color);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Drawable getTintedVectorDrawable(@NonNull Resources res, @DrawableRes int resId,
|
||||
@Nullable Resources.Theme theme, @ColorInt int color) {
|
||||
return TintHelper.createTintedDrawable(getVectorDrawable(res, resId, theme), color);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Drawable getVectorDrawable(@NonNull Resources res, @DrawableRes int resId,
|
||||
@Nullable Resources.Theme theme) {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
return res.getDrawable(resId, theme);
|
||||
}
|
||||
return VectorDrawableCompat.create(res, resId, theme);
|
||||
}
|
||||
|
||||
public static void hideSoftKeyboard(@Nullable Activity activity) {
|
||||
if (activity != null) {
|
||||
View currentFocus = activity.getCurrentFocus();
|
||||
if (currentFocus != null) {
|
||||
InputMethodManager inputMethodManager = (InputMethodManager) activity
|
||||
.getSystemService(Activity.INPUT_METHOD_SERVICE);
|
||||
if (inputMethodManager != null) {
|
||||
inputMethodManager.hideSoftInputFromWindow(currentFocus.getWindowToken(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isAllowedToDownloadMetadata(final @NonNull Context context) {
|
||||
switch (PreferenceUtil.INSTANCE.getAutoDownloadImagesPolicy()) {
|
||||
case "always":
|
||||
return true;
|
||||
case "only_wifi":
|
||||
final ConnectivityManager connectivityManager = (ConnectivityManager) context
|
||||
.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
NetworkInfo netInfo = connectivityManager.getActiveNetworkInfo();
|
||||
return netInfo != null && netInfo.getType() == ConnectivityManager.TYPE_WIFI && netInfo
|
||||
.isConnectedOrConnecting();
|
||||
case "never":
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isLandscape() {
|
||||
return App.Companion.getContext().getResources().getConfiguration().orientation
|
||||
== Configuration.ORIENTATION_LANDSCAPE;
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
|
||||
public static boolean isRTL(@NonNull Context context) {
|
||||
Configuration config = context.getResources().getConfiguration();
|
||||
return config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
|
||||
}
|
||||
|
||||
public static boolean isTablet() {
|
||||
return App.Companion.getContext().getResources().getConfiguration().smallestScreenWidthDp >= 600;
|
||||
}
|
||||
|
||||
public static void openUrl(@NonNull Activity context, @NonNull String str) {
|
||||
Intent intent = new Intent("android.intent.action.VIEW");
|
||||
intent.setData(Uri.parse(str));
|
||||
intent.setFlags(268435456);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
public static void setAllowDrawUnderNavigationBar(Window window) {
|
||||
window.setNavigationBarColor(Color.TRANSPARENT);
|
||||
window.getDecorView().setSystemUiVisibility(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ?
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION :
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
);
|
||||
}
|
||||
|
||||
public static void setAllowDrawUnderStatusBar(@NonNull Window window) {
|
||||
window.getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
|
||||
}
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
package code.name.monkey.retromusic.util
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.BaseColumns
|
||||
import android.provider.MediaStore
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import code.name.monkey.retromusic.R
|
||||
import code.name.monkey.retromusic.model.Song
|
||||
import code.name.monkey.retromusic.util.MusicUtil.getSongFileUri
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
||||
class RingtoneManager(val context: Context) {
|
||||
fun setRingtone(song: Song) {
|
||||
val resolver = context.contentResolver
|
||||
val uri = getSongFileUri(song.id)
|
||||
try {
|
||||
val values = ContentValues(2)
|
||||
values.put(MediaStore.Audio.AudioColumns.IS_RINGTONE, "1")
|
||||
values.put(MediaStore.Audio.AudioColumns.IS_ALARM, "1")
|
||||
resolver.update(uri, values, null, null)
|
||||
} catch (ignored: UnsupportedOperationException) {
|
||||
return
|
||||
}
|
||||
|
||||
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))
|
||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
} catch (ignored: SecurityException) {
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun requiresDialog(context: Context): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (!Settings.System.canWrite(context)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun getDialog(context: Context): AlertDialog {
|
||||
return MaterialAlertDialogBuilder(context)
|
||||
.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 = Uri.parse("package:" + context.applicationContext.packageName)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,146 +0,0 @@
|
|||
package code.name.monkey.retromusic.util;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.util.StateSet;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.graphics.ColorUtils;
|
||||
|
||||
public class RippleUtils {
|
||||
public static final boolean USE_FRAMEWORK_RIPPLE = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
|
||||
private static final int[] PRESSED_STATE_SET = {
|
||||
android.R.attr.state_pressed,
|
||||
};
|
||||
private static final int[] HOVERED_FOCUSED_STATE_SET = {
|
||||
android.R.attr.state_hovered, android.R.attr.state_focused,
|
||||
};
|
||||
private static final int[] FOCUSED_STATE_SET = {
|
||||
android.R.attr.state_focused,
|
||||
};
|
||||
private static final int[] HOVERED_STATE_SET = {
|
||||
android.R.attr.state_hovered,
|
||||
};
|
||||
|
||||
private static final int[] SELECTED_PRESSED_STATE_SET = {
|
||||
android.R.attr.state_selected, android.R.attr.state_pressed,
|
||||
};
|
||||
private static final int[] SELECTED_HOVERED_FOCUSED_STATE_SET = {
|
||||
android.R.attr.state_selected, android.R.attr.state_hovered, android.R.attr.state_focused,
|
||||
};
|
||||
private static final int[] SELECTED_FOCUSED_STATE_SET = {
|
||||
android.R.attr.state_selected, android.R.attr.state_focused,
|
||||
};
|
||||
private static final int[] SELECTED_HOVERED_STATE_SET = {
|
||||
android.R.attr.state_selected, android.R.attr.state_hovered,
|
||||
};
|
||||
private static final int[] SELECTED_STATE_SET = {
|
||||
android.R.attr.state_selected,
|
||||
};
|
||||
|
||||
private static final int[] ENABLED_PRESSED_STATE_SET = {
|
||||
android.R.attr.state_enabled, android.R.attr.state_pressed
|
||||
};
|
||||
|
||||
public static ColorStateList convertToRippleDrawableColor(@Nullable ColorStateList rippleColor) {
|
||||
if (USE_FRAMEWORK_RIPPLE) {
|
||||
int size = 2;
|
||||
|
||||
final int[][] states = new int[size][];
|
||||
final int[] colors = new int[size];
|
||||
int i = 0;
|
||||
|
||||
// Ideally we would define a different composite color for each state, but that causes the
|
||||
// ripple animation to abort prematurely.
|
||||
// So we only allow two base states: selected, and non-selected. For each base state, we only
|
||||
// base the ripple composite on its pressed state.
|
||||
|
||||
// Selected base state.
|
||||
states[i] = SELECTED_STATE_SET;
|
||||
colors[i] = getColorForState(rippleColor, SELECTED_PRESSED_STATE_SET);
|
||||
i++;
|
||||
|
||||
// Non-selected base state.
|
||||
states[i] = StateSet.NOTHING;
|
||||
colors[i] = getColorForState(rippleColor, PRESSED_STATE_SET);
|
||||
i++;
|
||||
|
||||
return new ColorStateList(states, colors);
|
||||
} else {
|
||||
int size = 10;
|
||||
|
||||
final int[][] states = new int[size][];
|
||||
final int[] colors = new int[size];
|
||||
int i = 0;
|
||||
|
||||
states[i] = SELECTED_PRESSED_STATE_SET;
|
||||
colors[i] = getColorForState(rippleColor, SELECTED_PRESSED_STATE_SET);
|
||||
i++;
|
||||
|
||||
states[i] = SELECTED_HOVERED_FOCUSED_STATE_SET;
|
||||
colors[i] = getColorForState(rippleColor, SELECTED_HOVERED_FOCUSED_STATE_SET);
|
||||
i++;
|
||||
|
||||
states[i] = SELECTED_FOCUSED_STATE_SET;
|
||||
colors[i] = getColorForState(rippleColor, SELECTED_FOCUSED_STATE_SET);
|
||||
i++;
|
||||
|
||||
states[i] = SELECTED_HOVERED_STATE_SET;
|
||||
colors[i] = getColorForState(rippleColor, SELECTED_HOVERED_STATE_SET);
|
||||
i++;
|
||||
|
||||
// Checked state.
|
||||
states[i] = SELECTED_STATE_SET;
|
||||
colors[i] = Color.TRANSPARENT;
|
||||
i++;
|
||||
|
||||
states[i] = PRESSED_STATE_SET;
|
||||
colors[i] = getColorForState(rippleColor, PRESSED_STATE_SET);
|
||||
i++;
|
||||
|
||||
states[i] = HOVERED_FOCUSED_STATE_SET;
|
||||
colors[i] = getColorForState(rippleColor, HOVERED_FOCUSED_STATE_SET);
|
||||
i++;
|
||||
|
||||
states[i] = FOCUSED_STATE_SET;
|
||||
colors[i] = getColorForState(rippleColor, FOCUSED_STATE_SET);
|
||||
i++;
|
||||
|
||||
states[i] = HOVERED_STATE_SET;
|
||||
colors[i] = getColorForState(rippleColor, HOVERED_STATE_SET);
|
||||
i++;
|
||||
|
||||
// Default state.
|
||||
states[i] = StateSet.NOTHING;
|
||||
colors[i] = Color.TRANSPARENT;
|
||||
i++;
|
||||
|
||||
return new ColorStateList(states, colors);
|
||||
}
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
private static int getColorForState(@Nullable ColorStateList rippleColor, int[] state) {
|
||||
int color;
|
||||
if (rippleColor != null) {
|
||||
color = rippleColor.getColorForState(state, rippleColor.getDefaultColor());
|
||||
} else {
|
||||
color = Color.TRANSPARENT;
|
||||
}
|
||||
return USE_FRAMEWORK_RIPPLE ? doubleAlpha(color) : color;
|
||||
}
|
||||
|
||||
/**
|
||||
* On API 21+, the framework composites a ripple color onto the display at about 50% opacity.
|
||||
* Since we are providing precise ripple colors, cancel that out by doubling the opacity here.
|
||||
*/
|
||||
@ColorInt
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
private static int doubleAlpha(@ColorInt int color) {
|
||||
int alpha = Math.min(2 * Color.alpha(color), 255);
|
||||
return ColorUtils.setAlphaComponent(color, alpha);
|
||||
}
|
||||
}
|
|
@ -1,303 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
package code.name.monkey.retromusic.util;
|
||||
|
||||
import android.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 code.name.monkey.retromusic.R;
|
||||
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 Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && !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();
|
||||
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(Context context) {
|
||||
return !TextUtils.isEmpty(PreferenceUtil.INSTANCE.getSafSdCardUri());
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
public static boolean isSDCardAccessGranted(Context context) {
|
||||
if (!isTreeUriSaved(context)) 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) {
|
||||
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(context)) {
|
||||
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 void delete(Context context, String path, Uri safUri) {
|
||||
if (isSAFRequired(path)) {
|
||||
deleteSAF(context, path, safUri);
|
||||
} else {
|
||||
try {
|
||||
deleteFile(path);
|
||||
} catch (NullPointerException e) {
|
||||
Log.e("MusicUtils", "Failed to find file " + path);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void deleteFile(String path) {
|
||||
new File(path).delete();
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||
public static void deleteSAF(Context context, String path, Uri safUri) {
|
||||
Uri uri = null;
|
||||
|
||||
if (context == null) {
|
||||
Log.e(TAG, "deleteSAF: context == null");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTreeUriSaved(context)) {
|
||||
List<String> pathSegments = new ArrayList<>(Arrays.asList(path.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, "deleteSAF: Can't get SAF URI");
|
||||
toast(context, context.getString(R.string.saf_error_uri));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
DocumentsContract.deleteDocument(context.getContentResolver(), uri);
|
||||
} 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()));
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* 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.app.ActivityCompat
|
||||
|
||||
/**
|
||||
* 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)
|
||||
ActivityCompat.startActivity(context, feedIntent, null)
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
package code.name.monkey.retromusic.util
|
||||
|
||||
import com.google.android.material.slider.Slider
|
||||
import java.util.*
|
||||
|
||||
class SliderReadTimeLabelFormatter : Slider.LabelFormatter {
|
||||
|
||||
override fun getFormattedValue(value: Float): String {
|
||||
var minutes: Long = value.toLong() / 1000 / 60
|
||||
val seconds: Long = value.toLong() / 1000 % 60
|
||||
return if (minutes < 60) {
|
||||
String.format(
|
||||
Locale.getDefault(),
|
||||
"%01d:%02d",
|
||||
minutes,
|
||||
seconds
|
||||
)
|
||||
} else {
|
||||
val hours = minutes / 60
|
||||
minutes %= 60
|
||||
String.format(
|
||||
Locale.getDefault(),
|
||||
"%d:%02d:%02d",
|
||||
hours,
|
||||
minutes,
|
||||
seconds
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
package code.name.monkey.retromusic.util;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class SwipeAndDragHelper extends ItemTouchHelper.Callback {
|
||||
|
||||
private ActionCompletionContract contract;
|
||||
|
||||
public SwipeAndDragHelper(@NonNull ActionCompletionContract contract) {
|
||||
this.contract = contract;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
|
||||
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
|
||||
return makeMovementFlags(dragFlags, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
|
||||
contract.onViewMoved(viewHolder.getLayoutPosition(), target.getLayoutPosition());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLongPressDragEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildDraw(Canvas c,
|
||||
RecyclerView recyclerView,
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
package code.name.monkey.retromusic.util;
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
/*
|
||||
* 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)
|
||||
popupText.setTextColor(textColor)
|
||||
}
|
||||
|
||||
fastScrollerBuilder.setThumbDrawable(
|
||||
TintHelper.createTintedDrawable(
|
||||
context,
|
||||
R.drawable.afs_md2_thumb,
|
||||
color
|
||||
)
|
||||
)
|
||||
return fastScrollerBuilder.build()
|
||||
}
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
package code.name.monkey.retromusic.util
|
||||
|
||||
import android.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 androidx.core.view.ViewCompat
|
||||
import code.name.monkey.appthemehelper.util.ATHUtil
|
||||
import code.name.monkey.appthemehelper.util.ColorUtil
|
||||
import code.name.monkey.appthemehelper.util.MaterialValueHelper
|
||||
import com.google.android.material.slider.Slider
|
||||
|
||||
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: Slider, color: Int, thumbTint: Boolean = false) {
|
||||
if (thumbTint) {
|
||||
progressSlider.thumbColor = ColorStateList.valueOf(color)
|
||||
}
|
||||
val colorWithAlpha = ColorUtil.withAlpha(color, 0.25f)
|
||||
progressSlider.haloColor = ColorStateList.valueOf(colorWithAlpha)
|
||||
progressSlider.haloRadius = 0
|
||||
progressSlider.trackColorActive = ColorStateList.valueOf(color)
|
||||
progressSlider.trackColorInactive = ColorStateList.valueOf(colorWithAlpha)
|
||||
}
|
||||
|
||||
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 = (ViewCompat.getTranslationX(v) + 0.5f).toInt()
|
||||
val ty = (ViewCompat.getTranslationY(v) + 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
|
||||
}
|
||||
}
|
|
@ -1,147 +0,0 @@
|
|||
/*
|
||||
* 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 int[] mTempBuffer;
|
||||
private Bitmap mTempCompactBitmap;
|
||||
private Canvas mTempCompactBitmapCanvas;
|
||||
private Paint mTempCompactBitmapPaint;
|
||||
private final Matrix mTempMatrix = new Matrix();
|
||||
|
||||
/**
|
||||
* 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];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
|
@ -1,507 +0,0 @@
|
|||
/*
|
||||
* 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.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 code.name.monkey.retromusic.R;
|
||||
|
||||
import static androidx.core.graphics.ColorUtils.RGBToXYZ;
|
||||
|
||||
/**
|
||||
* 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 static final String TAG = "ColorPicking";
|
||||
private float[] mFilteredBackgroundHsl = null;
|
||||
private Palette.Filter mBlackWhiteFilter = new Palette.Filter() {
|
||||
@Override
|
||||
public boolean isAllowed(int rgb, @NonNull float[] hsl) {
|
||||
return !isWhiteOrBlack(hsl);
|
||||
}
|
||||
};
|
||||
private boolean mIsLowPriority;
|
||||
private int backgroundColor;
|
||||
private int secondaryTextColor;
|
||||
private int primaryTextColor;
|
||||
private int actionBarColor;
|
||||
private Drawable drawable;
|
||||
private 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}.</p>
|
||||
*/
|
||||
@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(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
getMediaPalette();
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
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 = paletteBuilder.generate();
|
||||
backgroundColor = findBackgroundColorAndFilter(drawable);
|
||||
// we want most of the full region again, slightly shifted to the right
|
||||
float textColorStartWidthFraction = 0.4f;
|
||||
paletteBuilder.setRegion((int) (bitmap.getWidth() * textColorStartWidthFraction), 0,
|
||||
bitmap.getWidth(),
|
||||
bitmap.getHeight());
|
||||
if (mFilteredBackgroundHsl != null) {
|
||||
paletteBuilder.addFilter(new Palette.Filter() {
|
||||
@Override
|
||||
public boolean isAllowed(int rgb, @NonNull float[] hsl) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
public void setIsLowPriority(boolean isLowPriority) {
|
||||
mIsLowPriority = isLowPriority;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -1,989 +0,0 @@
|
|||
/*
|
||||
* 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 code.name.monkey.retromusic.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<Bitmap, Pair<Boolean, Integer>>();
|
||||
|
||||
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);
|
||||
if (useDark) {
|
||||
return ContextCompat.getColor(context, android.R.color.primary_text_light);
|
||||
} else {
|
||||
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}.</p>
|
||||
*/
|
||||
@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>
|
||||
* <li>outLab[1] is a [-128...127)</li>
|
||||
* <li>outLab[2] is b [-128...127)</li>
|
||||
* </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).</p>
|
||||
*
|
||||
* <ul>
|
||||
* <li>outXyz[0] is X [0 ...95.047)</li>
|
||||
* <li>outXyz[1] is Y [0...100)</li>
|
||||
* <li>outXyz[2] is Z [0...108.883)</li>
|
||||
* </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).</p>
|
||||
*
|
||||
* <ul>
|
||||
* <li>outXyz[0] is X [0 ...95.047)</li>
|
||||
* <li>outXyz[1] is Y [0...100)</li>
|
||||
* <li>outXyz[2] is Z [0...108.883)</li>
|
||||
* </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).</p>
|
||||
*
|
||||
* <ul>
|
||||
* <li>outLab[0] is L [0 ...100)</li>
|
||||
* <li>outLab[1] is a [-128...127)</li>
|
||||
* <li>outLab[2] is b [-128...127)</li>
|
||||
* </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).</p>
|
||||
*
|
||||
* <ul>
|
||||
* <li>outXyz[0] is X [0 ...95.047)</li>
|
||||
* <li>outXyz[1] is Y [0...100)</li>
|
||||
* <li>outXyz[2] is Z [0...108.883)</li>
|
||||
* </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).</p>
|
||||
*
|
||||
* @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 : (amount > high ? high : amount);
|
||||
}
|
||||
|
||||
private static float constrain(float amount, float low, float high) {
|
||||
return amount < low ? low : (amount > high ? high : amount);
|
||||
}
|
||||
|
||||
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>
|
||||
* <li>hsl[1] is Saturation [0...1]</li>
|
||||
* <li>hsl[2] is Lightness [0...1]</li>
|
||||
* </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>
|
||||
* <li>outHsl[1] is Saturation [0...1]</li>
|
||||
* <li>outHsl[2] is Lightness [0...1]</li>
|
||||
* </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>
|
||||
* <li>outHsl[1] is Saturation [0...1]</li>
|
||||
* <li>outHsl[2] is Lightness [0...1]</li>
|
||||
* </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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
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.theme.ThemeMode.*
|
||||
|
||||
object ThemeManager {
|
||||
|
||||
@StyleRes
|
||||
fun getThemeResValue(
|
||||
context: Context
|
||||
): Int = when (context.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 getNightMode(
|
||||
context: Context
|
||||
): Int = when (context.generalThemeValue) {
|
||||
LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
DARK,
|
||||
BLACK -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
AUTO -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
package code.name.monkey.retromusic.util.theme
|
||||
|
||||
enum class ThemeMode {
|
||||
LIGHT,
|
||||
DARK,
|
||||
BLACK,
|
||||
AUTO
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue