/*
* Copyright (C) 2014 Fastboot Mobile, LLC.
*
* This program 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 program 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.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, see <http://www.gnu.org/licenses>.
*/
package com.fastbootmobile.encore.utils;
import android.animation.Animator;
import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.v4.app.FragmentActivity;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Transformation;
import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toast;
import com.fastbootmobile.encore.app.AlbumActivity;
import com.fastbootmobile.encore.app.ArtistActivity;
import com.fastbootmobile.encore.app.R;
import com.fastbootmobile.encore.app.fragments.PlaylistChooserFragment;
import com.fastbootmobile.encore.framework.PlaybackProxy;
import com.fastbootmobile.encore.model.Album;
import com.fastbootmobile.encore.model.BoundEntity;
import com.fastbootmobile.encore.model.Playlist;
import com.fastbootmobile.encore.model.Song;
import com.fastbootmobile.encore.providers.ProviderAggregator;
import com.fastbootmobile.encore.providers.ProviderIdentifier;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* Utilities class used throughout the app
*/
public class Utils {
private static final String TAG = "Utils";
private static final Map<String, Bitmap> mBitmapQueue = new HashMap<>();
/**
* Format milliseconds into an human-readable track length.
* Examples:
* - 01:48:24 for 1 hour, 48 minutes, 24 seconds
* - 24:02 for 24 minutes, 2 seconds
* - 52s for 52 seconds
*
* @param timeMs The time to format, in milliseconds
* @return A formatted string
*/
public static String formatTrackLength(long timeMs) {
long hours = TimeUnit.MILLISECONDS.toHours(timeMs);
long minutes = TimeUnit.MILLISECONDS.toMinutes(timeMs) - TimeUnit.HOURS.toMinutes(hours);
long seconds = TimeUnit.MILLISECONDS.toSeconds(timeMs)
- TimeUnit.HOURS.toSeconds(hours)
- TimeUnit.MINUTES.toSeconds(minutes);
if (hours > 0) {
return String.format("%02d:%02d:%02d", hours, minutes, seconds);
} else if (minutes > 0) {
return String.format("%02d:%02d", minutes, seconds);
} else if (seconds > 0) {
return String.format("%02ds", seconds);
} else if (seconds == 0) {
return "-:--";
} else {
return "N/A";
}
}
/**
* Calculates the RMS audio level from the provided short sample extract
*
* @param audioData The audio samples
* @return The RMS level
*/
public static int calculateRMSLevel(short[] audioData, int numframes) {
long lSum = 0;
int numread = 0;
for (short s : audioData) {
lSum = lSum + s;
numread++;
if (numread == numframes) break;
}
double dAvg = lSum / numframes;
double sumMeanSquare = 0d;
numread = 0;
for (short anAudioData : audioData) {
sumMeanSquare = sumMeanSquare + Math.pow(anAudioData - dAvg, 2d);
numread++;
if (numread == numframes) break;
}
double averageMeanSquare = sumMeanSquare / numframes;
return (int) (Math.pow(averageMeanSquare, 0.5d) + 0.5);
}
/**
* Shows a short Toast style message
*
* @param context The application context
* @param res The String resource id
*/
public static void shortToast(Context context, int res) {
if (context != null) {
Toast.makeText(context, res, Toast.LENGTH_SHORT).show();
} else {
Log.e(TAG, "Cannot show toast for text ID " + res + " as context is null");
}
}
/**
* Calculates the optimal size of the text based on the text view width
*
* @param textView The text view in which the text should fit
* @param desiredWidth The desired final text width, or -1 for the TextView's getMeasuredWidth
*/
public static void adjustTextSize(TextView textView, int desiredWidth) {
if (desiredWidth <= 0) {
desiredWidth = textView.getMeasuredWidth();
if (desiredWidth <= 0) {
// Invalid width, don't do anything
Log.w("Utils", "adjustTextSize: Not doing anything (measured width invalid)");
return;
}
}
// Add some margin to width
desiredWidth *= 0.8f;
Paint paint = new Paint();
Rect bounds = new Rect();
paint.setTypeface(textView.getTypeface());
float textSize = textView.getTextSize() * 2.0f;
paint.setTextSize(textSize);
String text = textView.getText().toString();
paint.getTextBounds(text, 0, text.length(), bounds);
while (bounds.width() > desiredWidth) {
textSize--;
paint.setTextSize(textSize);
paint.getTextBounds(text, 0, text.length(), bounds);
}
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
}
/**
* Temporarily store a bitmap
*
* @param key The key
* @param bmp The bitmap
*/
public static void queueBitmap(String key, Bitmap bmp) {
mBitmapQueue.put(key, bmp);
}
/**
* Retrieve a bitmap from the store, and removes it
*
* @param key The key used in queueBitmap
* @return The bitmap associated with the key
*/
public static Bitmap dequeueBitmap(String key) {
Bitmap bmp = mBitmapQueue.get(key);
mBitmapQueue.remove(key);
return bmp;
}
/**
* Animate a view expansion (unwrapping)
*
* @param v The view to animate
* @param expand True to animate expanding, false to animate closing
* @return The animation object created
*/
public static Animation animateExpand(final View v, final boolean expand) {
try {
Method m = v.getClass().getDeclaredMethod("onMeasure", int.class, int.class);
m.setAccessible(true);
m.invoke(
v,
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(((View) v.getParent()).getMeasuredWidth(), View.MeasureSpec.UNSPECIFIED)
);
} catch (Exception e) {
e.printStackTrace();
}
final int initialHeight = v.getMeasuredHeight();
if (expand) {
v.getLayoutParams().height = 0;
} else {
v.getLayoutParams().height = initialHeight;
}
v.setVisibility(View.VISIBLE);
Animation a = new Animation() {
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
int newHeight;
if (expand) {
newHeight = (int) (initialHeight * interpolatedTime);
} else {
newHeight = (int) (initialHeight * (1 - interpolatedTime));
}
v.getLayoutParams().height = newHeight;
v.requestLayout();
if (interpolatedTime == 1 && !expand)
v.setVisibility(View.GONE);
}
@Override
public boolean willChangeBounds() {
return true;
}
};
a.setDuration(500);
return a;
}
/**
* Provides an API-independent way to set a Drawable background on a view
*/
public static void setViewBackground(View v, Drawable d) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
v.setBackground(d);
} else {
v.setBackgroundDrawable(d);
}
}
public static int dpToPx(Resources res, int dp) {
final DisplayMetrics displayMetrics = res.getDisplayMetrics();
return Math.round(dp * (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT));
}
/**
* Figures out the main artist of an album based on its songs
*
* @param a The album
* @return A reference to the main artist, or null if none
*/
public static String getMainArtist(Album a) {
if (a == null) {
return null;
}
HashMap<String, Integer> occurrences = new HashMap<>();
Iterator<String> it = a.songs();
final ProviderAggregator aggregator = ProviderAggregator.getDefault();
while (it.hasNext()) {
String songRef = it.next();
if (songRef == null) {
Log.e(TAG, "Album '" + a.getName() + "' contains null songs!");
continue;
}
Song song = aggregator.retrieveSong(songRef, a.getProvider());
if (song != null) {
String artistRef = song.getArtist();
Integer count = occurrences.get(artistRef);
if (count == null) {
count = 0;
}
count++;
occurrences.put(artistRef, count);
}
}
// Figure the max
Map.Entry<String, Integer> maxEntry = null;
for (Map.Entry<String, Integer> entry : occurrences.entrySet()) {
if (maxEntry == null || entry.getValue() > maxEntry.getValue()) {
maxEntry = entry;
}
}
// If there's more than 5 tracks in the album and the most occurrences is one, consider
// there's no major artist in the album and return null.
if (maxEntry != null &&
((a.getSongsCount() > 5 && maxEntry.getValue() > 1) || a.getSongsCount() <= 5)) {
return maxEntry.getKey();
} else {
return null;
}
}
/**
* Figures out the main artist of a playlist based on its songs
*
* @param p The playlist
* @return A reference to the main artist, or null if none
*/
public static String getMainArtist(Playlist p) {
HashMap<String, Integer> occurrences = new HashMap<>();
Iterator<String> it = p.songs();
final ProviderAggregator aggregator = ProviderAggregator.getDefault();
while (it.hasNext()) {
String songRef = it.next();
Song song = aggregator.retrieveSong(songRef, p.getProvider());
if (song != null) {
String artistRef = song.getArtist();
Integer count = occurrences.get(artistRef);
if (count == null) {
count = 0;
}
count++;
occurrences.put(artistRef, count);
}
}
// Figure the max
Map.Entry<String, Integer> maxEntry = null;
for (Map.Entry<String, Integer> entry : occurrences.entrySet()) {
if (maxEntry == null || entry.getValue() > maxEntry.getValue()) {
maxEntry = entry;
}
}
// If there's more than 5 tracks in the album and the most occurrences is one, consider
// there's no major artist in the album and return null.
if (maxEntry != null &&
((p.getSongsCount() > 5 && maxEntry.getValue() > 1) || p.getSongsCount() < 5)) {
return maxEntry.getKey();
} else {
return null;
}
}
public static void showSongOverflow(final FragmentActivity context, final View parent,
final Song song, final boolean hideArtist) {
if (song == null) {
return;
}
PopupMenu popupMenu = new PopupMenu(context, parent);
popupMenu.inflate(R.menu.track_overflow);
if (song.getArtist() == null || hideArtist) {
// This song has no artist information, hide the entry
Menu menu = popupMenu.getMenu();
menu.removeItem(R.id.menu_open_artist);
}
if (song.getAlbum() == null) {
// This song has no album information, hide the entry
Menu menu = popupMenu.getMenu();
menu.removeItem(R.id.menu_add_album_to_queue);
}
popupMenu.show();
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
final ProviderAggregator aggregator = ProviderAggregator.getDefault();
switch (menuItem.getItemId()) {
case R.id.menu_play_now:
PlaybackProxy.playSong(song);
break;
case R.id.menu_play_next:
PlaybackProxy.playNext(song);
break;
case R.id.menu_open_artist:
Intent intent = ArtistActivity.craftIntent(context, null, song.getArtist(),
song.getProvider(),
context.getResources().getColor(R.color.default_album_art_background));
context.startActivity(intent);
break;
case R.id.menu_add_to_queue:
PlaybackProxy.queueSong(song, false);
Toast.makeText(context, R.string.toast_song_added_to_queue, Toast.LENGTH_SHORT).show();
break;
case R.id.menu_add_album_to_queue:
PlaybackProxy.queueAlbum(aggregator.retrieveAlbum(song.getAlbum(),
song.getProvider()), false);
Toast.makeText(context, R.string.toast_album_added_to_queue, Toast.LENGTH_SHORT).show();
break;
case R.id.menu_add_to_playlist:
PlaylistChooserFragment fragment = PlaylistChooserFragment.newInstance(song);
fragment.show(context.getSupportFragmentManager(), song.getRef());
break;
default:
return false;
}
return true;
}
});
}
public static void showCurrentSongOverflow(final Context context, final View parent,
final Song song) {
showCurrentSongOverflow(context, parent, song, false);
}
public static void showCurrentSongOverflow(final Context context, final View parent,
final Song song, final boolean showArtist) {
PopupMenu popupMenu = new PopupMenu(context, parent);
popupMenu.inflate(R.menu.queue_overflow);
if (song.getAlbum() == null) {
Log.d(TAG, "No album information, removing album options");
// This song has no album information, hide the entries
Menu menu = popupMenu.getMenu();
menu.removeItem(R.id.menu_add_album_to_queue);
menu.removeItem(R.id.menu_open_album);
}
if (!showArtist) {
Menu menu = popupMenu.getMenu();
menu.removeItem(R.id.menu_open_artist);
}
popupMenu.show();
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
final ProviderAggregator aggregator = ProviderAggregator.getDefault();
switch (menuItem.getItemId()) {
case R.id.menu_open_album:
final Resources res = context.getResources();
Intent intent = AlbumActivity.craftIntent(context,
((BitmapDrawable) res.getDrawable(R.drawable.album_placeholder)).getBitmap(),
song.getAlbum(),
song.getProvider(),
res.getColor(R.color.default_album_art_background));
context.startActivity(intent);
break;
case R.id.menu_open_artist:
intent = ArtistActivity.craftIntent(context, null, song.getArtist(),
song.getProvider(),
context.getResources().getColor(R.color.default_album_art_background));
context.startActivity(intent);
break;
case R.id.menu_add_album_to_queue:
PlaybackProxy.queueAlbum(aggregator.retrieveAlbum(song.getAlbum(),
song.getProvider()), false);
Toast.makeText(context, R.string.toast_album_added_to_queue, Toast.LENGTH_SHORT).show();
break;
case R.id.menu_add_to_playlist:
PlaylistChooserFragment fragment = PlaylistChooserFragment.newInstance(song);
if (context instanceof FragmentActivity) {
FragmentActivity act = (FragmentActivity) context;
fragment.show(act.getSupportFragmentManager(), song.getRef());
} else {
throw new IllegalArgumentException("Context must be an instance of FragmentActivity");
}
break;
default:
return false;
}
return true;
}
});
}
/**
* Serializes a string array with the specified separator
*
* @param elements The elements to serialize
* @param separator The separator to use between the elements
* @return The elements in a single string, separated with the specified separator, an empty
* string if elements is empty, or null if elements is null.
*/
public static String implode(String[] elements, String separator) {
if (elements == null) {
return null;
}
if (elements.length == 0) {
return "";
}
StringBuilder builder = new StringBuilder();
builder.append(elements[0]);
boolean skippedFirst = false;
for (String s : elements) {
if (skippedFirst) {
builder.append(separator);
builder.append(s);
} else {
skippedFirst = true;
}
}
return builder.toString();
}
public static void animateScale(View v, boolean animate, boolean visible) {
v.setPivotX(v.getMeasuredWidth() / 2);
v.setPivotY(v.getMeasuredHeight() / 2);
if (visible) {
if (animate) {
v.animate().scaleX(1.0f).scaleY(1.0f)
.alpha(1.0f)
.translationY(0.0f)
.setDuration(400)
.setInterpolator(new DecelerateInterpolator()).start();
} else {
v.setScaleX(1.0f);
v.setScaleY(1.0f);
v.setAlpha(1.0f);
v.setTranslationY(0.0f);
}
} else {
if (animate) {
v.animate().scaleX(0.0f).scaleY(0.0f)
.alpha(0.0f)
.translationY(v.getHeight() / 4)
.setDuration(400)
.setInterpolator(new DecelerateInterpolator()).start();
} else {
v.setScaleX(0.0f);
v.setScaleY(0.0f);
v.setAlpha(0.0f);
v.setTranslationY(v.getHeight() / 4);
}
}
}
public static void setChildrenAlpha(ViewGroup root, final float alpha) {
final int childCount = root.getChildCount();
for (int i = 0; i < childCount; ++i) {
root.getChildAt(i).setAlpha(alpha);
}
}
/**
* Returns whether or not the song can be played. This takes into account the track's
* availability as reported by the provider, as well as the offline status and mode
*
* @param s The song to check
* @return True if the song can be played right now
*/
public static boolean canPlaySong(Song s) {
final boolean offlineMode = ProviderAggregator.getDefault().isOfflineMode();
return s != null
&& s.isAvailable()
&& (!offlineMode || s.getOfflineStatus() == BoundEntity.OFFLINE_STATUS_READY);
}
/**
* Returns whether or not the album is available offline. For that, the album must be loaded
* and have at least of track available offline
*
* @param a The album
* @return true if the album is available offline
*/
public static boolean isAlbumAvailableOffline(Album a) {
if (a == null) {
return false;
} else if (!a.songs().hasNext()) {
return false;
} else {
Iterator<String> songIt = a.songs();
while (songIt.hasNext()) {
Song song = ProviderAggregator.getDefault().retrieveSong(songIt.next(), a.getProvider());
if (canPlaySong(song)) {
return true;
}
}
}
return false;
}
public static boolean hasKitKat() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
}
public static boolean hasLollipop() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
}
public static boolean hasJellyBeanMR1() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1;
}
public static int distance(String a, String b) {
a = a.toLowerCase();
b = b.toLowerCase();
// i == 0
int[] costs = new int[b.length() + 1];
for (int j = 0; j < costs.length; j++) {
costs[j] = j;
}
for (int i = 1; i <= a.length(); i++) {
// j == 0; nw = lev(i - 1, j)
costs[0] = i;
int nw = i - 1;
for (int j = 1; j <= b.length(); j++) {
int cj = Math.min(1 + Math.min(costs[j], costs[j - 1]), a.charAt(i - 1) == b.charAt(j - 1) ? nw : nw + 1);
nw = costs[j];
costs[j] = cj;
}
}
return costs[b.length()];
}
public static float distancePercentage(String a, String b) {
float max = Math.max(a.length(), b.length());
return 1.0f - ((float) distance(a, b)) / max;
}
public static List<Song> refIteratorToSongList(Iterator<String> it, ProviderIdentifier id) {
ProviderAggregator aggr = ProviderAggregator.getDefault();
List<Song> output = new ArrayList<>();
while (it.hasNext()) {
output.add(aggr.retrieveSong(it.next(), id));
}
return output;
}
public static List<Song> refListToSongList(List<String> refList, ProviderIdentifier id) {
ProviderAggregator aggr = ProviderAggregator.getDefault();
List<Song> output = new ArrayList<>();
for (String ref : refList) {
output.add(aggr.retrieveSong(ref, id));
}
return output;
}
public static int getEnclosingCircleRadius(View v, int cx, int cy) {
int realCenterX = cx + v.getLeft();
int realCenterY = cy + v.getTop();
int distanceTopLeft = (int) Math.hypot(realCenterX - v.getLeft(), realCenterY - v.getTop());
int distanceTopRight = (int) Math.hypot(v.getRight() - realCenterX, realCenterY - v.getTop());
int distanceBottomLeft = (int) Math.hypot(realCenterX - v.getLeft(), v.getBottom() - realCenterY);
int distanceBotomRight = (int) Math.hypot(v.getRight() - realCenterX, v.getBottom() - realCenterY);
int[] distances = new int[]{distanceTopLeft, distanceTopRight, distanceBottomLeft, distanceBotomRight};
int radius = distances[0];
for (int i = 1; i < distances.length; i++) {
if (distances[i] > radius)
radius = distances[i];
}
return radius;
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static void animateHeadingReveal(final View view, final long duration) {
final int cx = view.getMeasuredWidth() / 5;
final int cy = view.getMeasuredHeight() / 2;
final int radius = Utils.getEnclosingCircleRadius(view, cx, cy);
if (cx == 0 && cy == 0) {
Log.w(TAG, "animateHidingReveal: Measured dimensions are zero");
}
animateCircleReveal(view, cx, cy, 0, radius, duration);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static void animateHeadingReveal(final View view, final int cx, final int cy,
final long duration) {
final int radius = Utils.getEnclosingCircleRadius(view, cx, cy);
if (cx == 0 && cy == 0) {
Log.w(TAG, "animateHidingReveal: Measured dimensions are zero");
}
animateCircleReveal(view, cx, cy, 0, radius, duration);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static void animateHeadingHiding(final View view, final long duration) {
final int cx = view.getMeasuredWidth() / 5;
final int cy = view.getMeasuredHeight() / 2;
animateHeadingHiding(view, cx, cy, duration);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static void animateHeadingHiding(final View view, final int cx, final int cy,
final long duration) {
final int radius = Utils.getEnclosingCircleRadius(view, cx, cy);
animateCircleReveal(view, cx, cy, radius, 0, duration);
}
public static Animator animateCircleReveal(final View view, final int cx, final int cy,
final int startRadius, final int endRadius,
final long duration) {
return animateCircleReveal(view, cx, cy, startRadius, endRadius, duration, 0);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static Animator animateCircleReveal(final View view, final int cx, final int cy,
final int startRadius, final int endRadius,
final long duration, final long startDelay) {
Animator animator = ViewAnimationUtils.createCircularReveal(view, cx, cy, startRadius,
endRadius)
.setDuration(duration);
animator.setStartDelay(startDelay);
animator.setInterpolator(new DecelerateInterpolator());
animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
view.setVisibility(View.VISIBLE);
}
@Override
public void onAnimationEnd(Animator animation) {
if (startRadius > endRadius) {
view.setVisibility(View.INVISIBLE);
}
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
animator.start();
return animator;
}
public static int getRandom(int maxExcluded) {
return new Random().nextInt(maxExcluded);
}
public static String getAppNameByPID(Context context, int pid) {
ActivityManager manager
= (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
for (ActivityManager.RunningAppProcessInfo processInfo : manager.getRunningAppProcesses()) {
if (processInfo.pid == pid) {
return processInfo.processName;
}
}
return "";
}
}