blob: 7cb057bdf3fbb518aec228e941280a4e8b0d9a8e [file] [log] [blame]
/*
* Copyright (C) 2008 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 com.android.music;
import android.app.Activity;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.provider.MediaStore;
import android.provider.Settings;
import android.text.TextUtils;
import android.text.format.Time;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.view.Window;
import android.widget.TabWidget;
import android.widget.TextView;
import android.widget.Toast;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Formatter;
import java.util.HashMap;
import java.util.Locale;
public class MusicUtils {
private static final String TAG = "MusicUtils";
public interface Defs {
public final static int OPEN_URL = 0;
public final static int ADD_TO_PLAYLIST = 1;
public final static int USE_AS_RINGTONE = 2;
public final static int PLAYLIST_SELECTED = 3;
public final static int NEW_PLAYLIST = 4;
public final static int PLAY_SELECTION = 5;
public final static int GOTO_START = 6;
public final static int GOTO_PLAYBACK = 7;
public final static int PARTY_SHUFFLE = 8;
public final static int SHUFFLE_ALL = 9;
public final static int DELETE_ITEM = 10;
public final static int SCAN_DONE = 11;
public final static int QUEUE = 12;
public final static int EFFECTS_PANEL = 13;
public final static int CHILD_MENU_BASE = 14; // this should be the last item
}
public static String makeAlbumsLabel(
Context context, int numalbums, int numsongs, boolean isUnknown) {
// There are two formats for the albums/songs information:
// "N Song(s)" - used for unknown artist/album
// "N Album(s)" - used for known albums
StringBuilder songs_albums = new StringBuilder();
Resources r = context.getResources();
if (isUnknown) {
if (numsongs == 1) {
songs_albums.append(context.getString(R.string.onesong));
} else {
String f = r.getQuantityText(R.plurals.Nsongs, numsongs).toString();
sFormatBuilder.setLength(0);
sFormatter.format(f, Integer.valueOf(numsongs));
songs_albums.append(sFormatBuilder);
}
} else {
String f = r.getQuantityText(R.plurals.Nalbums, numalbums).toString();
sFormatBuilder.setLength(0);
sFormatter.format(f, Integer.valueOf(numalbums));
songs_albums.append(sFormatBuilder);
songs_albums.append(context.getString(R.string.albumsongseparator));
}
return songs_albums.toString();
}
/**
* This is now only used for the query screen
*/
public static String makeAlbumsSongsLabel(
Context context, int numalbums, int numsongs, boolean isUnknown) {
// There are several formats for the albums/songs information:
// "1 Song" - used if there is only 1 song
// "N Songs" - used for the "unknown artist" item
// "1 Album"/"N Songs"
// "N Album"/"M Songs"
// Depending on locale, these may need to be further subdivided
StringBuilder songs_albums = new StringBuilder();
if (numsongs == 1) {
songs_albums.append(context.getString(R.string.onesong));
} else {
Resources r = context.getResources();
if (!isUnknown) {
String f = r.getQuantityText(R.plurals.Nalbums, numalbums).toString();
sFormatBuilder.setLength(0);
sFormatter.format(f, Integer.valueOf(numalbums));
songs_albums.append(sFormatBuilder);
songs_albums.append(context.getString(R.string.albumsongseparator));
}
String f = r.getQuantityText(R.plurals.Nsongs, numsongs).toString();
sFormatBuilder.setLength(0);
sFormatter.format(f, Integer.valueOf(numsongs));
songs_albums.append(sFormatBuilder);
}
return songs_albums.toString();
}
public static IMediaPlaybackService sService = null;
private static HashMap<Context, ServiceBinder> sConnectionMap =
new HashMap<Context, ServiceBinder>();
public static class ServiceToken {
ContextWrapper mWrappedContext;
ServiceToken(ContextWrapper context) {
mWrappedContext = context;
}
}
public static ServiceToken bindToService(Activity context) {
return bindToService(context, null);
}
public static ServiceToken bindToService(Activity context, ServiceConnection callback) {
Activity realActivity = context.getParent();
if (realActivity == null) {
realActivity = context;
}
ContextWrapper cw = new ContextWrapper(realActivity);
cw.startService(new Intent(cw, MediaPlaybackService.class));
ServiceBinder sb = new ServiceBinder(callback);
if (cw.bindService((new Intent()).setClass(cw, MediaPlaybackService.class), sb, 0)) {
sConnectionMap.put(cw, sb);
return new ServiceToken(cw);
}
Log.e("Music", "Failed to bind to service");
return null;
}
public static void unbindFromService(ServiceToken token) {
if (token == null) {
Log.e("MusicUtils", "Trying to unbind with null token");
return;
}
ContextWrapper cw = token.mWrappedContext;
ServiceBinder sb = sConnectionMap.remove(cw);
if (sb == null) {
Log.e("MusicUtils", "Trying to unbind for unknown Context");
return;
}
cw.unbindService(sb);
if (sConnectionMap.isEmpty()) {
// presumably there is nobody interested in the service at this point,
// so don't hang on to the ServiceConnection
sService = null;
}
}
private static class ServiceBinder implements ServiceConnection {
ServiceConnection mCallback;
ServiceBinder(ServiceConnection callback) {
mCallback = callback;
}
public void onServiceConnected(ComponentName className, android.os.IBinder service) {
sService = IMediaPlaybackService.Stub.asInterface(service);
initAlbumArtCache();
if (mCallback != null) {
mCallback.onServiceConnected(className, service);
}
}
public void onServiceDisconnected(ComponentName className) {
if (mCallback != null) {
mCallback.onServiceDisconnected(className);
}
sService = null;
}
}
public static long getCurrentAlbumId() {
if (sService != null) {
try {
return sService.getAlbumId();
} catch (RemoteException ex) {
}
}
return -1;
}
public static long getCurrentArtistId() {
if (MusicUtils.sService != null) {
try {
return sService.getArtistId();
} catch (RemoteException ex) {
}
}
return -1;
}
public static long getCurrentAudioId() {
if (MusicUtils.sService != null) {
try {
return sService.getAudioId();
} catch (RemoteException ex) {
}
}
return -1;
}
public static int getCurrentShuffleMode() {
int mode = MediaPlaybackService.SHUFFLE_NONE;
if (sService != null) {
try {
mode = sService.getShuffleMode();
} catch (RemoteException ex) {
}
}
return mode;
}
public static void togglePartyShuffle() {
if (sService != null) {
int shuffle = getCurrentShuffleMode();
try {
if (shuffle == MediaPlaybackService.SHUFFLE_AUTO) {
sService.setShuffleMode(MediaPlaybackService.SHUFFLE_NONE);
} else {
sService.setShuffleMode(MediaPlaybackService.SHUFFLE_AUTO);
}
} catch (RemoteException ex) {
}
}
}
public static void setPartyShuffleMenuIcon(Menu menu) {
MenuItem item = menu.findItem(Defs.PARTY_SHUFFLE);
if (item != null) {
int shuffle = MusicUtils.getCurrentShuffleMode();
if (shuffle == MediaPlaybackService.SHUFFLE_AUTO) {
item.setIcon(R.drawable.ic_menu_party_shuffle);
item.setTitle(R.string.party_shuffle_off);
} else {
item.setIcon(R.drawable.ic_menu_party_shuffle);
item.setTitle(R.string.party_shuffle);
}
}
}
/*
* Returns true if a file is currently opened for playback (regardless
* of whether it's playing or paused).
*/
public static boolean isMusicLoaded() {
if (MusicUtils.sService != null) {
try {
return sService.getPath() != null;
} catch (RemoteException ex) {
}
}
return false;
}
private final static long[] sEmptyList = new long[0];
public static long[] getSongListForCursor(Cursor cursor) {
if (cursor == null) {
return sEmptyList;
}
int len = cursor.getCount();
long[] list = new long[len];
cursor.moveToFirst();
int colidx = -1;
try {
colidx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.Members.AUDIO_ID);
} catch (IllegalArgumentException ex) {
colidx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID);
}
for (int i = 0; i < len; i++) {
list[i] = cursor.getLong(colidx);
cursor.moveToNext();
}
return list;
}
public static long[] getSongListForArtist(Context context, long id) {
final String[] ccols = new String[] {MediaStore.Audio.Media._ID};
String where = MediaStore.Audio.Media.ARTIST_ID + "=" + id + " AND "
+ MediaStore.Audio.Media.IS_MUSIC + "=1";
Cursor cursor = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, ccols, where,
null, MediaStore.Audio.Media.ALBUM_KEY + "," + MediaStore.Audio.Media.TRACK);
if (cursor != null) {
long[] list = getSongListForCursor(cursor);
cursor.close();
return list;
}
return sEmptyList;
}
public static long[] getSongListForAlbum(Context context, long id) {
final String[] ccols = new String[] {MediaStore.Audio.Media._ID};
String where = MediaStore.Audio.Media.ALBUM_ID + "=" + id + " AND "
+ MediaStore.Audio.Media.IS_MUSIC + "=1";
Cursor cursor = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, ccols, where,
null, MediaStore.Audio.Media.TRACK);
if (cursor != null) {
long[] list = getSongListForCursor(cursor);
cursor.close();
return list;
}
return sEmptyList;
}
public static long[] getSongListForPlaylist(Context context, long plid) {
final String[] ccols = new String[] {MediaStore.Audio.Playlists.Members.AUDIO_ID};
Cursor cursor =
query(context, MediaStore.Audio.Playlists.Members.getContentUri("external", plid),
ccols, null, null, MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER);
if (cursor != null) {
long[] list = getSongListForCursor(cursor);
cursor.close();
return list;
}
return sEmptyList;
}
public static void playPlaylist(Context context, long plid) {
long[] list = getSongListForPlaylist(context, plid);
if (list != null) {
playAll(context, list, -1, false);
}
}
public static long[] getAllSongs(Context context) {
Cursor c = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
new String[] {MediaStore.Audio.Media._ID}, MediaStore.Audio.Media.IS_MUSIC + "=1",
null, null);
try {
if (c == null || c.getCount() == 0) {
return null;
}
int len = c.getCount();
long[] list = new long[len];
for (int i = 0; i < len; i++) {
c.moveToNext();
list[i] = c.getLong(0);
}
return list;
} finally {
if (c != null) {
c.close();
}
}
}
/**
* Fills out the given submenu with items for "new playlist" and
* any existing playlists. When the user selects an item, the
* application will receive PLAYLIST_SELECTED with the Uri of
* the selected playlist, NEW_PLAYLIST if a new playlist
* should be created, and QUEUE if the "current playlist" was
* selected.
* @param context The context to use for creating the menu items
* @param sub The submenu to add the items to.
*/
public static void makePlaylistMenu(Context context, SubMenu sub) {
String[] cols =
new String[] {MediaStore.Audio.Playlists._ID, MediaStore.Audio.Playlists.NAME};
ContentResolver resolver = context.getContentResolver();
if (resolver == null) {
System.out.println("resolver = null");
} else {
String whereclause = MediaStore.Audio.Playlists.NAME + " != ''";
Cursor cur = resolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, cols,
whereclause, null, MediaStore.Audio.Playlists.NAME);
sub.clear();
sub.add(1, Defs.QUEUE, 0, R.string.queue);
sub.add(1, Defs.NEW_PLAYLIST, 0, R.string.new_playlist);
if (cur != null && cur.getCount() > 0) {
// sub.addSeparator(1, 0);
cur.moveToFirst();
while (!cur.isAfterLast()) {
Intent intent = new Intent();
intent.putExtra("playlist", cur.getLong(0));
// if (cur.getInt(0) == mLastPlaylistSelected) {
// sub.add(0, MusicBaseActivity.PLAYLIST_SELECTED,
// cur.getString(1)).setIntent(intent);
// } else {
sub.add(1, Defs.PLAYLIST_SELECTED, 0, cur.getString(1)).setIntent(intent);
// }
cur.moveToNext();
}
}
if (cur != null) {
cur.close();
}
}
}
public static void clearPlaylist(Context context, int plid) {
Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", plid);
context.getContentResolver().delete(uri, null, null);
return;
}
public static void deleteTracks(Context context, long[] list) {
String[] cols = new String[] {MediaStore.Audio.Media._ID, MediaStore.Audio.Media.DATA,
MediaStore.Audio.Media.ALBUM_ID};
StringBuilder where = new StringBuilder();
where.append(MediaStore.Audio.Media._ID + " IN (");
for (int i = 0; i < list.length; i++) {
where.append(list[i]);
if (i < list.length - 1) {
where.append(",");
}
}
where.append(")");
Cursor c = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, cols,
where.toString(), null, null);
if (c != null) {
// step 1: remove selected tracks from the current playlist, as well
// as from the album art cache
try {
c.moveToFirst();
while (!c.isAfterLast()) {
// remove from current playlist
long id = c.getLong(0);
sService.removeTrack(id);
// remove from album art cache
long artIndex = c.getLong(2);
synchronized (sArtCache) {
sArtCache.remove(artIndex);
}
c.moveToNext();
}
} catch (RemoteException ex) {
}
// step 2: remove selected tracks from the database
context.getContentResolver().delete(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, where.toString(), null);
// step 3: remove files from card
c.moveToFirst();
while (!c.isAfterLast()) {
String name = c.getString(1);
File f = new File(name);
try { // File.delete can throw a security exception
if (!f.delete()) {
// I'm not sure if we'd ever get here (deletion would
// have to fail, but no exception thrown)
Log.e("MusicUtils", "Failed to delete file " + name);
}
c.moveToNext();
} catch (SecurityException ex) {
c.moveToNext();
}
}
c.close();
}
String message = context.getResources().getQuantityString(
R.plurals.NNNtracksdeleted, list.length, Integer.valueOf(list.length));
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
// We deleted a number of tracks, which could affect any number of things
// in the media content domain, so update everything.
context.getContentResolver().notifyChange(Uri.parse("content://media"), null);
}
public static void addToCurrentPlaylist(Context context, long[] list) {
if (sService == null) {
return;
}
try {
sService.enqueue(list, MediaPlaybackService.LAST);
String message = context.getResources().getQuantityString(
R.plurals.NNNtrackstoplaylist, list.length, Integer.valueOf(list.length));
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
} catch (RemoteException ex) {
}
}
private static ContentValues[] sContentValuesCache = null;
/**
* @param ids The source array containing all the ids to be added to the playlist
* @param offset Where in the 'ids' array we start reading
* @param len How many items to copy during this pass
* @param base The play order offset to use for this pass
*/
private static void makeInsertItems(long[] ids, int offset, int len, int base) {
// adjust 'len' if would extend beyond the end of the source array
if (offset + len > ids.length) {
len = ids.length - offset;
}
// allocate the ContentValues array, or reallocate if it is the wrong size
if (sContentValuesCache == null || sContentValuesCache.length != len) {
sContentValuesCache = new ContentValues[len];
}
// fill in the ContentValues array with the right values for this pass
for (int i = 0; i < len; i++) {
if (sContentValuesCache[i] == null) {
sContentValuesCache[i] = new ContentValues();
}
sContentValuesCache[i].put(
MediaStore.Audio.Playlists.Members.PLAY_ORDER, base + offset + i);
sContentValuesCache[i].put(
MediaStore.Audio.Playlists.Members.AUDIO_ID, ids[offset + i]);
}
}
public static void addToPlaylist(Context context, long[] ids, long playlistid) {
if (ids == null) {
// this shouldn't happen (the menuitems shouldn't be visible
// unless the selected item represents something playable
Log.e("MusicBase", "ListSelection null");
} else {
int size = ids.length;
ContentResolver resolver = context.getContentResolver();
// need to determine the number of items currently in the playlist,
// so the play_order field can be maintained.
String[] cols = new String[] {"count(*)"};
Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistid);
Cursor cur = resolver.query(uri, cols, null, null, null);
cur.moveToFirst();
int base = cur.getInt(0);
cur.close();
int numinserted = 0;
for (int i = 0; i < size; i += 1000) {
makeInsertItems(ids, i, 1000, base);
numinserted += resolver.bulkInsert(uri, sContentValuesCache);
}
String message = context.getResources().getQuantityString(
R.plurals.NNNtrackstoplaylist, numinserted, numinserted);
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
// mLastPlaylistSelected = playlistid;
}
}
public static Cursor query(Context context, Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder, int limit) {
try {
ContentResolver resolver = context.getContentResolver();
if (resolver == null) {
return null;
}
if (limit > 0) {
uri = uri.buildUpon().appendQueryParameter("limit", "" + limit).build();
}
return resolver.query(uri, projection, selection, selectionArgs, sortOrder);
} catch (UnsupportedOperationException ex) {
return null;
}
}
public static Cursor query(Context context, Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
return query(context, uri, projection, selection, selectionArgs, sortOrder, 0);
}
public static boolean isMediaScannerScanning(Context context) {
boolean result = false;
Cursor cursor = query(context, MediaStore.getMediaScannerUri(),
new String[] {MediaStore.MEDIA_SCANNER_VOLUME}, null, null, null);
if (cursor != null) {
if (cursor.getCount() == 1) {
cursor.moveToFirst();
result = "external".equals(cursor.getString(0));
}
cursor.close();
}
return result;
}
public static void setSpinnerState(Activity a) {
if (isMediaScannerScanning(a)) {
// start the progress spinner
a.getWindow().setFeatureInt(
Window.FEATURE_INDETERMINATE_PROGRESS, Window.PROGRESS_INDETERMINATE_ON);
a.getWindow().setFeatureInt(
Window.FEATURE_INDETERMINATE_PROGRESS, Window.PROGRESS_VISIBILITY_ON);
} else {
// stop the progress spinner
a.getWindow().setFeatureInt(
Window.FEATURE_INDETERMINATE_PROGRESS, Window.PROGRESS_VISIBILITY_OFF);
}
}
private static String mLastSdStatus;
public static void displayDatabaseError(Activity a) {
if (a.isFinishing()) {
// When switching tabs really fast, we can end up with a null
// cursor (not sure why), which will bring us here.
// Don't bother showing an error message in that case.
return;
}
String status = Environment.getExternalStorageState();
int title, message;
if (android.os.Environment.isExternalStorageRemovable()) {
title = R.string.sdcard_error_title;
message = R.string.sdcard_error_message;
} else {
title = R.string.sdcard_error_title_nosdcard;
message = R.string.sdcard_error_message_nosdcard;
}
if (status.equals(Environment.MEDIA_SHARED) || status.equals(Environment.MEDIA_UNMOUNTED)) {
if (android.os.Environment.isExternalStorageRemovable()) {
title = R.string.sdcard_busy_title;
message = R.string.sdcard_busy_message;
} else {
title = R.string.sdcard_busy_title_nosdcard;
message = R.string.sdcard_busy_message_nosdcard;
}
} else if (status.equals(Environment.MEDIA_REMOVED)) {
if (android.os.Environment.isExternalStorageRemovable()) {
title = R.string.sdcard_missing_title;
message = R.string.sdcard_missing_message;
} else {
title = R.string.sdcard_missing_title_nosdcard;
message = R.string.sdcard_missing_message_nosdcard;
}
} else if (status.equals(Environment.MEDIA_MOUNTED)) {
// The card is mounted, but we didn't get a valid cursor.
// This probably means the mediascanner hasn't started scanning the
// card yet (there is a small window of time during boot where this
// will happen).
a.setTitle("");
Intent intent = new Intent();
intent.setClass(a, ScanningProgress.class);
a.startActivityForResult(intent, Defs.SCAN_DONE);
} else if (!TextUtils.equals(mLastSdStatus, status)) {
mLastSdStatus = status;
Log.d(TAG, "sd card: " + status);
}
a.setTitle(title);
View v = a.findViewById(R.id.sd_message);
if (v != null) {
v.setVisibility(View.VISIBLE);
}
v = a.findViewById(R.id.sd_icon);
if (v != null) {
v.setVisibility(View.VISIBLE);
}
v = a.findViewById(android.R.id.list);
if (v != null) {
v.setVisibility(View.GONE);
}
v = a.findViewById(R.id.buttonbar);
if (v != null) {
v.setVisibility(View.GONE);
}
TextView tv = (TextView) a.findViewById(R.id.sd_message);
tv.setText(message);
}
public static void hideDatabaseError(Activity a) {
View v = a.findViewById(R.id.sd_message);
if (v != null) {
v.setVisibility(View.GONE);
}
v = a.findViewById(R.id.sd_icon);
if (v != null) {
v.setVisibility(View.GONE);
}
v = a.findViewById(android.R.id.list);
if (v != null) {
v.setVisibility(View.VISIBLE);
}
}
static protected Uri getContentURIForPath(String path) {
return Uri.fromFile(new File(path));
}
/* Try to use String.format() as little as possible, because it creates a
* new Formatter every time you call it, which is very inefficient.
* Reusing an existing Formatter more than tripled the speed of
* makeTimeString().
* This Formatter/StringBuilder are also used by makeAlbumSongsLabel()
*/
private static StringBuilder sFormatBuilder = new StringBuilder();
private static Formatter sFormatter = new Formatter(sFormatBuilder, Locale.getDefault());
private static final Object[] sTimeArgs = new Object[5];
public static String makeTimeString(Context context, long secs) {
String durationformat = context.getString(
secs < 3600 ? R.string.durationformatshort : R.string.durationformatlong);
/* Provide multiple arguments so the format can be changed easily
* by modifying the xml.
*/
sFormatBuilder.setLength(0);
final Object[] timeArgs = sTimeArgs;
timeArgs[0] = secs / 3600;
timeArgs[1] = secs / 60;
timeArgs[2] = (secs / 60) % 60;
timeArgs[3] = secs;
timeArgs[4] = secs % 60;
return sFormatter.format(durationformat, timeArgs).toString();
}
public static void shuffleAll(Context context, Cursor cursor) {
playAll(context, cursor, 0, true);
}
public static void playAll(Context context, Cursor cursor) {
playAll(context, cursor, 0, false);
}
public static void playAll(Context context, Cursor cursor, int position) {
playAll(context, cursor, position, false);
}
public static void playAll(Context context, long[] list, int position) {
playAll(context, list, position, false);
}
private static void playAll(
Context context, Cursor cursor, int position, boolean force_shuffle) {
long[] list = getSongListForCursor(cursor);
playAll(context, list, position, force_shuffle);
}
private static void playAll(Context context, long[] list, int position, boolean force_shuffle) {
if (list.length == 0 || sService == null) {
Log.d("MusicUtils", "attempt to play empty song list");
// Don't try to play empty playlists. Nothing good will come of it.
String message = context.getString(R.string.emptyplaylist, list.length);
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
return;
}
try {
if (force_shuffle) {
sService.setShuffleMode(MediaPlaybackService.SHUFFLE_NORMAL);
}
long curid = sService.getAudioId();
int curpos = sService.getQueuePosition();
if (position != -1 && curpos == position && curid == list[position]) {
// The selected file is the file that's currently playing;
// figure out if we need to restart with a new playlist,
// or just launch the playback activity.
long[] playlist = sService.getQueue();
if (Arrays.equals(list, playlist)) {
// we don't need to set a new list, but we should resume playback if needed
sService.play();
return; // the 'finally' block will still run
}
}
if (position < 0) {
position = 0;
}
sService.open(list, force_shuffle ? -1 : position);
sService.play();
} catch (RemoteException ex) {
} finally {
Intent intent = new Intent("com.android.music.PLAYBACK_VIEWER")
.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
context.startActivity(intent);
}
}
public static void clearQueue() {
try {
sService.removeTracks(0, Integer.MAX_VALUE);
} catch (RemoteException ex) {
}
}
// A really simple BitmapDrawable-like class, that doesn't do
// scaling, dithering or filtering.
private static class FastBitmapDrawable extends Drawable {
private Bitmap mBitmap;
public FastBitmapDrawable(Bitmap b) {
mBitmap = b;
}
@Override
public void draw(Canvas canvas) {
canvas.drawBitmap(mBitmap, 0, 0, null);
}
@Override
public int getOpacity() {
return PixelFormat.OPAQUE;
}
@Override
public void setAlpha(int alpha) {}
@Override
public void setColorFilter(ColorFilter cf) {}
}
private static int sArtId = -2;
private static Bitmap mCachedBit = null;
private static final BitmapFactory.Options sBitmapOptionsCache = new BitmapFactory.Options();
private static final BitmapFactory.Options sBitmapOptions = new BitmapFactory.Options();
private static final Uri sArtworkUri = Uri.parse("content://media/external/audio/albumart");
private static final HashMap<Long, Drawable> sArtCache = new HashMap<Long, Drawable>();
private static int sArtCacheId = -1;
static {
// for the cache,
// 565 is faster to decode and display
// and we don't want to dither here because the image will be scaled down later
sBitmapOptionsCache.inPreferredConfig = Bitmap.Config.RGB_565;
sBitmapOptionsCache.inDither = false;
sBitmapOptions.inPreferredConfig = Bitmap.Config.RGB_565;
sBitmapOptions.inDither = false;
}
public static void initAlbumArtCache() {
try {
int id = sService.getMediaMountedCount();
if (id != sArtCacheId) {
clearAlbumArtCache();
sArtCacheId = id;
}
} catch (RemoteException e) {
e.printStackTrace();
}
}
public static void clearAlbumArtCache() {
synchronized (sArtCache) {
sArtCache.clear();
}
}
public static Drawable getCachedArtwork(
Context context, long artIndex, BitmapDrawable defaultArtwork) {
Drawable d = null;
synchronized (sArtCache) {
d = sArtCache.get(artIndex);
}
if (d == null) {
d = defaultArtwork;
final Bitmap icon = defaultArtwork.getBitmap();
int w = icon.getWidth();
int h = icon.getHeight();
Bitmap b = MusicUtils.getArtworkQuick(context, artIndex, w, h);
if (b != null) {
d = new FastBitmapDrawable(b);
synchronized (sArtCache) {
// the cache may have changed since we checked
Drawable value = sArtCache.get(artIndex);
if (value == null) {
sArtCache.put(artIndex, d);
} else {
d = value;
}
}
}
}
return d;
}
// Get album art for specified album. This method will not try to
// fall back to getting artwork directly from the file, nor will
// it attempt to repair the database.
private static Bitmap getArtworkQuick(Context context, long album_id, int w, int h) {
// NOTE: There is in fact a 1 pixel border on the right side in the ImageView
// used to display this drawable. Take it into account now, so we don't have to
// scale later.
w -= 1;
ContentResolver res = context.getContentResolver();
Uri uri = ContentUris.withAppendedId(sArtworkUri, album_id);
if (uri != null) {
ParcelFileDescriptor fd = null;
try {
fd = res.openFileDescriptor(uri, "r");
int sampleSize = 1;
// Compute the closest power-of-two scale factor
// and pass that to sBitmapOptionsCache.inSampleSize, which will
// result in faster decoding and better quality
sBitmapOptionsCache.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(
fd.getFileDescriptor(), null, sBitmapOptionsCache);
int nextWidth = sBitmapOptionsCache.outWidth >> 1;
int nextHeight = sBitmapOptionsCache.outHeight >> 1;
while (nextWidth > w && nextHeight > h) {
sampleSize <<= 1;
nextWidth >>= 1;
nextHeight >>= 1;
}
sBitmapOptionsCache.inSampleSize = sampleSize;
sBitmapOptionsCache.inJustDecodeBounds = false;
Bitmap b = BitmapFactory.decodeFileDescriptor(
fd.getFileDescriptor(), null, sBitmapOptionsCache);
if (b != null) {
// finally rescale to exactly the size we need
if (sBitmapOptionsCache.outWidth != w || sBitmapOptionsCache.outHeight != h) {
Bitmap tmp = Bitmap.createScaledBitmap(b, w, h, true);
// Bitmap.createScaledBitmap() can return the same bitmap
if (tmp != b) b.recycle();
b = tmp;
}
}
return b;
} catch (FileNotFoundException e) {
} finally {
try {
if (fd != null) fd.close();
} catch (IOException e) {
}
}
}
return null;
}
/** Get album art for specified album. You should not pass in the album id
* for the "unknown" album here (use -1 instead)
* This method always returns the default album art icon when no album art is found.
*/
public static Bitmap getArtwork(Context context, long song_id, long album_id) {
return getArtwork(context, song_id, album_id, true);
}
/** Get album art for specified album. You should not pass in the album id
* for the "unknown" album here (use -1 instead)
*/
public static Bitmap getArtwork(
Context context, long song_id, long album_id, boolean allowdefault) {
if (album_id < 0) {
// This is something that is not in the database, so get the album art directly
// from the file.
if (song_id >= 0) {
Bitmap bm = getArtworkFromFile(context, song_id, -1);
if (bm != null) {
return bm;
}
}
if (allowdefault) {
return getDefaultArtwork(context);
}
return null;
}
ContentResolver res = context.getContentResolver();
Uri uri = ContentUris.withAppendedId(sArtworkUri, album_id);
if (uri != null) {
InputStream in = null;
try {
in = res.openInputStream(uri);
return BitmapFactory.decodeStream(in, null, sBitmapOptions);
} catch (FileNotFoundException ex) {
// The album art thumbnail does not actually exist. Maybe the user deleted it, or
// maybe it never existed to begin with.
Bitmap bm = getArtworkFromFile(context, song_id, album_id);
if (bm != null) {
if (bm.getConfig() == null) {
bm = bm.copy(Bitmap.Config.RGB_565, false);
if (bm == null && allowdefault) {
return getDefaultArtwork(context);
}
}
} else if (allowdefault) {
bm = getDefaultArtwork(context);
}
return bm;
} finally {
try {
if (in != null) {
in.close();
}
} catch (IOException ex) {
}
}
}
return null;
}
// get album art for specified file
private static final String sExternalMediaUri =
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString();
private static Bitmap getArtworkFromFile(Context context, long songid, long albumid) {
Bitmap bm = null;
byte[] art = null;
String path = null;
if (albumid < 0 && songid < 0) {
throw new IllegalArgumentException("Must specify an album or a song id");
}
try {
if (albumid < 0) {
Uri uri = Uri.parse("content://media/external/audio/media/" + songid + "/albumart");
ParcelFileDescriptor pfd =
context.getContentResolver().openFileDescriptor(uri, "r");
if (pfd != null) {
FileDescriptor fd = pfd.getFileDescriptor();
bm = BitmapFactory.decodeFileDescriptor(fd);
}
} else {
Uri uri = ContentUris.withAppendedId(sArtworkUri, albumid);
ParcelFileDescriptor pfd =
context.getContentResolver().openFileDescriptor(uri, "r");
if (pfd != null) {
FileDescriptor fd = pfd.getFileDescriptor();
bm = BitmapFactory.decodeFileDescriptor(fd);
}
}
} catch (IllegalStateException ex) {
} catch (FileNotFoundException ex) {
}
if (bm != null) {
mCachedBit = bm;
}
return bm;
}
private static Bitmap getDefaultArtwork(Context context) {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inPreferredConfig = Bitmap.Config.ARGB_8888;
return BitmapFactory.decodeStream(
context.getResources().openRawResource(R.drawable.albumart_mp_unknown), null, opts);
}
static int getIntPref(Context context, String name, int def) {
SharedPreferences prefs =
context.getSharedPreferences(context.getPackageName(), Context.MODE_PRIVATE);
return prefs.getInt(name, def);
}
static void setIntPref(Context context, String name, int value) {
SharedPreferences prefs =
context.getSharedPreferences(context.getPackageName(), Context.MODE_PRIVATE);
Editor ed = prefs.edit();
ed.putInt(name, value);
SharedPreferencesCompat.apply(ed);
}
static void setRingtone(Context context, long id) {
ContentResolver resolver = context.getContentResolver();
// Set the flag in the database to mark this as a ringtone
Uri ringUri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
try {
ContentValues values = new ContentValues(2);
values.put(MediaStore.Audio.Media.IS_RINGTONE, "1");
values.put(MediaStore.Audio.Media.IS_ALARM, "1");
resolver.update(ringUri, values, null, null);
} catch (UnsupportedOperationException ex) {
// most likely the card just got unmounted
Log.e(TAG, "couldn't set ringtone flag for id " + id);
return;
}
String[] cols = new String[] {MediaStore.Audio.Media._ID, MediaStore.Audio.Media.DATA,
MediaStore.Audio.Media.TITLE};
String where = MediaStore.Audio.Media._ID + "=" + id;
Cursor cursor = query(
context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, cols, where, null, null);
try {
if (cursor != null && cursor.getCount() == 1) {
// Set the system setting to make this the current ringtone
cursor.moveToFirst();
Settings.System.putString(resolver, Settings.System.RINGTONE, ringUri.toString());
String message = context.getString(R.string.ringtone_set, cursor.getString(2));
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
}
} finally {
if (cursor != null) {
cursor.close();
}
}
}
static int sActiveTabIndex = -1;
static boolean updateButtonBar(Activity a, int highlight) {
final TabWidget ll = (TabWidget) a.findViewById(R.id.buttonbar);
boolean withtabs = false;
Intent intent = a.getIntent();
if (intent != null) {
withtabs = intent.getBooleanExtra("withtabs", false);
}
if (highlight == 0 || !withtabs) {
ll.setVisibility(View.GONE);
return withtabs;
} else if (withtabs) {
ll.setVisibility(View.VISIBLE);
}
for (int i = ll.getChildCount() - 1; i >= 0; i--) {
View v = ll.getChildAt(i);
boolean isActive = (v.getId() == highlight);
if (isActive) {
ll.setCurrentTab(i);
sActiveTabIndex = i;
}
v.setTag(i);
v.setOnFocusChangeListener(new View.OnFocusChangeListener() {
public void onFocusChange(View v, boolean hasFocus) {
if (hasFocus) {
for (int i = 0; i < ll.getTabCount(); i++) {
if (ll.getChildTabViewAt(i) == v) {
ll.setCurrentTab(i);
processTabClick((Activity) ll.getContext(), v,
ll.getChildAt(sActiveTabIndex).getId());
break;
}
}
}
}
});
v.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
processTabClick(
(Activity) ll.getContext(), v, ll.getChildAt(sActiveTabIndex).getId());
}
});
}
return withtabs;
}
static void processTabClick(Activity a, View v, int current) {
int id = v.getId();
if (id == current) {
return;
}
final TabWidget ll = (TabWidget) a.findViewById(R.id.buttonbar);
activateTab(a, id);
if (id != R.id.nowplayingtab) {
ll.setCurrentTab((Integer) v.getTag());
setIntPref(a, "activetab", id);
}
}
static void activateTab(Activity a, int id) {
Intent intent = new Intent(Intent.ACTION_PICK);
switch (id) {
case R.id.artisttab:
intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/artistalbum");
break;
case R.id.albumtab:
intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/album");
break;
case R.id.songtab:
intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track");
break;
case R.id.playlisttab:
intent.setDataAndType(Uri.EMPTY, MediaStore.Audio.Playlists.CONTENT_TYPE);
break;
case R.id.nowplayingtab:
intent = new Intent(a, MediaPlaybackActivity.class);
a.startActivity(intent);
// fall through and return
default:
return;
}
intent.putExtra("withtabs", true);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
a.startActivity(intent);
a.finish();
a.overridePendingTransition(0, 0);
}
static void updateNowPlaying(Activity a) {
View nowPlayingView = a.findViewById(R.id.nowplaying);
if (nowPlayingView == null) {
return;
}
try {
boolean withtabs = false;
Intent intent = a.getIntent();
if (intent != null) {
withtabs = intent.getBooleanExtra("withtabs", false);
}
if (true && MusicUtils.sService != null && MusicUtils.sService.getAudioId() != -1) {
TextView title = (TextView) nowPlayingView.findViewById(R.id.title);
TextView artist = (TextView) nowPlayingView.findViewById(R.id.artist);
title.setText(MusicUtils.sService.getTrackName());
String artistName = MusicUtils.sService.getArtistName();
if (MediaStore.UNKNOWN_STRING.equals(artistName)) {
artistName = a.getString(R.string.unknown_artist_name);
}
artist.setText(artistName);
// mNowPlayingView.setOnFocusChangeListener(mFocuser);
// mNowPlayingView.setOnClickListener(this);
nowPlayingView.setVisibility(View.VISIBLE);
nowPlayingView.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
Context c = v.getContext();
c.startActivity(new Intent(c, MediaPlaybackActivity.class));
}
});
return;
}
} catch (RemoteException ex) {
}
nowPlayingView.setVisibility(View.GONE);
}
static void setBackground(View v, Bitmap bm) {
if (bm == null) {
v.setBackgroundResource(0);
return;
}
int vwidth = v.getWidth();
int vheight = v.getHeight();
int bwidth = bm.getWidth();
int bheight = bm.getHeight();
float scalex = (float) vwidth / bwidth;
float scaley = (float) vheight / bheight;
float scale = Math.max(scalex, scaley) * 1.3f;
Bitmap.Config config = Bitmap.Config.ARGB_8888;
Bitmap bg = Bitmap.createBitmap(vwidth, vheight, config);
Canvas c = new Canvas(bg);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setFilterBitmap(true);
ColorMatrix greymatrix = new ColorMatrix();
greymatrix.setSaturation(0);
ColorMatrix darkmatrix = new ColorMatrix();
darkmatrix.setScale(.3f, .3f, .3f, 1.0f);
greymatrix.postConcat(darkmatrix);
ColorFilter filter = new ColorMatrixColorFilter(greymatrix);
paint.setColorFilter(filter);
Matrix matrix = new Matrix();
matrix.setTranslate(-bwidth / 2, -bheight / 2); // move bitmap center to origin
matrix.postRotate(10);
matrix.postScale(scale, scale);
matrix.postTranslate(vwidth / 2, vheight / 2); // Move bitmap center to view center
c.drawBitmap(bm, matrix, paint);
v.setBackgroundDrawable(new BitmapDrawable(bg));
}
static int getCardId(Context context) {
ContentResolver res = context.getContentResolver();
Cursor c = res.query(Uri.parse("content://media/external/fs_id"), null, null, null, null);
int id = -1;
if (c != null) {
c.moveToFirst();
id = c.getInt(0);
c.close();
}
return id;
}
static class LogEntry {
Object item;
long time;
LogEntry(Object o) {
item = o;
time = System.currentTimeMillis();
}
void dump(PrintWriter out) {
sTime.set(time);
out.print(sTime.toString() + " : ");
if (item instanceof Exception) {
((Exception) item).printStackTrace(out);
} else {
out.println(item);
}
}
}
private static LogEntry[] sMusicLog = new LogEntry[100];
private static int sLogPtr = 0;
private static Time sTime = new Time();
static void debugLog(Object o) {
sMusicLog[sLogPtr] = new LogEntry(o);
sLogPtr++;
if (sLogPtr >= sMusicLog.length) {
sLogPtr = 0;
}
}
static void debugDump(PrintWriter out) {
for (int i = 0; i < sMusicLog.length; i++) {
int idx = (sLogPtr + i);
if (idx >= sMusicLog.length) {
idx -= sMusicLog.length;
}
LogEntry entry = sMusicLog[idx];
if (entry != null) {
entry.dump(out);
}
}
}
}