auto import from //depot/cupcake/@135843
diff --git a/src/com/android/music/MusicUtils.java b/src/com/android/music/MusicUtils.java
new file mode 100644
index 0000000..2f5aeca
--- /dev/null
+++ b/src/com/android/music/MusicUtils.java
@@ -0,0 +1,1196 @@
+/*
+ * 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 java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.Formatter;
+import java.util.HashMap;
+import java.util.Locale;
+
+import android.app.Activity;
+import android.app.ExpandableListActivity;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+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.PixelFormat;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.MediaFile;
+import android.media.MediaScanner;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.os.Environment;
+import android.os.ParcelFileDescriptor;
+import android.provider.MediaStore;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.Window;
+import android.widget.TextView;
+import android.widget.Toast;
+
+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 CHILD_MENU_BASE = 13; // 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 boolean bindToService(Context context) {
+ return bindToService(context, null);
+ }
+
+ public static boolean bindToService(Context context, ServiceConnection callback) {
+ context.startService(new Intent(context, MediaPlaybackService.class));
+ ServiceBinder sb = new ServiceBinder(callback);
+ sConnectionMap.put(context, sb);
+ return context.bindService((new Intent()).setClass(context,
+ MediaPlaybackService.class), sb, 0);
+ }
+
+ public static void unbindFromService(Context context) {
+ ServiceBinder sb = (ServiceBinder) sConnectionMap.remove(context);
+ if (sb == null) {
+ Log.e("MusicUtils", "Trying to unbind for unknown Context");
+ return;
+ }
+ context.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 int getCurrentAlbumId() {
+ if (sService != null) {
+ try {
+ return sService.getAlbumId();
+ } catch (RemoteException ex) {
+ }
+ }
+ return -1;
+ }
+
+ public static int getCurrentArtistId() {
+ if (MusicUtils.sService != null) {
+ try {
+ return sService.getArtistId();
+ } catch (RemoteException ex) {
+ }
+ }
+ return -1;
+ }
+
+ public static int 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;
+ }
+
+ /*
+ * 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 int [] sEmptyList = new int[0];
+
+ public static int [] getSongListForCursor(Cursor cursor) {
+ if (cursor == null) {
+ return sEmptyList;
+ }
+ int len = cursor.getCount();
+ int [] list = new int[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.getInt(colidx);
+ cursor.moveToNext();
+ }
+ return list;
+ }
+
+ public static int [] getSongListForArtist(Context context, int 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) {
+ int [] list = getSongListForCursor(cursor);
+ cursor.close();
+ return list;
+ }
+ return sEmptyList;
+ }
+
+ public static int [] getSongListForAlbum(Context context, int 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) {
+ int [] list = getSongListForCursor(cursor);
+ cursor.close();
+ return list;
+ }
+ return sEmptyList;
+ }
+
+ public static int [] 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) {
+ int [] list = getSongListForCursor(cursor);
+ cursor.close();
+ return list;
+ }
+ return sEmptyList;
+ }
+
+ public static void playPlaylist(Context context, long plid) {
+ int [] list = getSongListForPlaylist(context, plid);
+ if (list != null) {
+ playAll(context, list, -1, false);
+ }
+ }
+
+ public static int [] 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();
+ int[] list = new int[len];
+ for (int i = 0; i < len; i++) {
+ c.moveToNext();
+ list[i] = c.getInt(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.getInt(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, int [] 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
+ int id = c.getInt(0);
+ sService.removeTrack(id);
+ // remove from album art cache
+ int artIndex = c.getInt(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, int [] 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) {
+ }
+ }
+
+ public static void addToPlaylist(Context context, int [] 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;
+ ContentValues values [] = new ContentValues[size];
+ 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();
+
+ for (int i = 0; i < size; i++) {
+ values[i] = new ContentValues();
+ values[i].put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(base + i));
+ values[i].put(MediaStore.Audio.Playlists.Members.AUDIO_ID, ids[i]);
+ }
+ resolver.bulkInsert(uri, values);
+ String message = context.getResources().getQuantityString(
+ R.plurals.NNNtrackstoplaylist, size, Integer.valueOf(size));
+ 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) {
+ try {
+ ContentResolver resolver = context.getContentResolver();
+ if (resolver == null) {
+ return null;
+ }
+ return resolver.query(uri, projection, selection, selectionArgs, sortOrder);
+ } catch (UnsupportedOperationException ex) {
+ return null;
+ }
+
+ }
+
+ 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);
+ }
+ }
+
+ public static void displayDatabaseError(Activity a) {
+ String status = Environment.getExternalStorageState();
+ int title = R.string.sdcard_error_title;
+ int message = R.string.sdcard_error_message;
+
+ if (status.equals(Environment.MEDIA_SHARED) ||
+ status.equals(Environment.MEDIA_UNMOUNTED)) {
+ title = R.string.sdcard_busy_title;
+ message = R.string.sdcard_busy_message;
+ } else if (status.equals(Environment.MEDIA_REMOVED)) {
+ title = R.string.sdcard_missing_title;
+ message = R.string.sdcard_missing_message;
+ } 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 {
+ 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);
+ }
+ 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(R.string.durationformat);
+
+ /* 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, int [] list, int position) {
+ playAll(context, list, position, false);
+ }
+
+ private static void playAll(Context context, Cursor cursor, int position, boolean force_shuffle) {
+
+ int [] list = getSongListForCursor(cursor);
+ playAll(context, list, position, force_shuffle);
+ }
+
+ private static void playAll(Context context, int [] 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);
+ }
+ int 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.
+ int [] 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 byte [] mCachedArt;
+ 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<Integer, Drawable> sArtCache = new HashMap<Integer, 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, int 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, int 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)
+ */
+ public static Bitmap getArtwork(Context context, int album_id) {
+ return getArtwork(context, 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, int 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.
+ Bitmap bm = getArtworkFromFile(context, null, -1);
+ if (bm != null) {
+ return bm;
+ }
+ if (allowDefault) {
+ return getDefaultArtwork(context);
+ } else {
+ 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, null, album_id);
+ if (bm != null) {
+ // Put the newly found artwork in the database.
+ // Note that this shouldn't be done for the "unknown" album,
+ // but if this method is called correctly, that won't happen.
+
+ // first write it somewhere
+ String file = Environment.getExternalStorageDirectory()
+ + "/albumthumbs/" + String.valueOf(System.currentTimeMillis());
+ if (ensureFileExists(file)) {
+ try {
+ OutputStream outstream = new FileOutputStream(file);
+ if (bm.getConfig() == null) {
+ bm = bm.copy(Bitmap.Config.RGB_565, false);
+ if (bm == null) {
+ if (allowDefault) {
+ return getDefaultArtwork(context);
+ } else {
+ return null;
+ }
+ }
+ }
+ boolean success = bm.compress(Bitmap.CompressFormat.JPEG, 75, outstream);
+ outstream.close();
+ if (success) {
+ ContentValues values = new ContentValues();
+ values.put("album_id", album_id);
+ values.put("_data", file);
+ Uri newuri = res.insert(sArtworkUri, values);
+ if (newuri == null) {
+ // Failed to insert in to the database. The most likely
+ // cause of this is that the item already existed in the
+ // database, and the most likely cause of that is that
+ // the album was scanned before, but the user deleted the
+ // album art from the sd card.
+ // We can ignore that case here, since the media provider
+ // will regenerate the album art for those entries when
+ // it detects this.
+ success = false;
+ }
+ }
+ if (!success) {
+ File f = new File(file);
+ f.delete();
+ }
+ } catch (FileNotFoundException e) {
+ Log.e(TAG, "error creating file", e);
+ } catch (IOException e) {
+ Log.e(TAG, "error creating file", e);
+ }
+ }
+ } else if (allowDefault) {
+ bm = getDefaultArtwork(context);
+ } else {
+ bm = null;
+ }
+ return bm;
+ } finally {
+ try {
+ if (in != null) {
+ in.close();
+ }
+ } catch (IOException ex) {
+ }
+ }
+ }
+
+ return null;
+ }
+
+ // copied from MediaProvider
+ private static boolean ensureFileExists(String path) {
+ File file = new File(path);
+ if (file.exists()) {
+ return true;
+ } else {
+ // we will not attempt to create the first directory in the path
+ // (for example, do not create /sdcard if the SD card is not mounted)
+ int secondSlash = path.indexOf('/', 1);
+ if (secondSlash < 1) return false;
+ String directoryPath = path.substring(0, secondSlash);
+ File directory = new File(directoryPath);
+ if (!directory.exists())
+ return false;
+ file.getParentFile().mkdirs();
+ try {
+ return file.createNewFile();
+ } catch(IOException ioe) {
+ Log.d(TAG, "File creation failed for " + path);
+ }
+ return false;
+ }
+ }
+
+ // get album art for specified file
+ private static final String sExternalMediaUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString();
+ private static Bitmap getArtworkFromFile(Context context, Uri uri, int albumid) {
+ Bitmap bm = null;
+ byte [] art = null;
+ String path = null;
+
+ if (sArtId == albumid) {
+ //Log.i("@@@@@@ ", "reusing cached data", new Exception());
+ if (mCachedBit != null) {
+ return mCachedBit;
+ }
+ art = mCachedArt;
+ } else {
+ // try reading embedded artwork
+ if (uri == null) {
+ try {
+ int curalbum = sService.getAlbumId();
+ if (curalbum == albumid || albumid < 0) {
+ path = sService.getPath();
+ if (path != null) {
+ uri = Uri.parse(path);
+ }
+ }
+ } catch (RemoteException ex) {
+ return null;
+ } catch (NullPointerException ex) {
+ return null;
+ }
+ }
+ if (uri == null) {
+ if (albumid >= 0) {
+ Cursor c = query(context,MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ new String[] { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.ALBUM },
+ MediaStore.Audio.Media.ALBUM_ID + "=?", new String [] {String.valueOf(albumid)},
+ null);
+ if (c != null) {
+ c.moveToFirst();
+ if (!c.isAfterLast()) {
+ int trackid = c.getInt(0);
+ uri = ContentUris.withAppendedId(
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, trackid);
+ }
+ if (c.getString(1).equals(MediaFile.UNKNOWN_STRING)) {
+ albumid = -1;
+ }
+ c.close();
+ }
+ }
+ }
+ if (uri != null) {
+ MediaScanner scanner = new MediaScanner(context);
+ ParcelFileDescriptor pfd = null;
+ try {
+ pfd = context.getContentResolver().openFileDescriptor(uri, "r");
+ if (pfd != null) {
+ FileDescriptor fd = pfd.getFileDescriptor();
+ art = scanner.extractAlbumArt(fd);
+ }
+ } catch (IOException ex) {
+ } catch (SecurityException ex) {
+ } finally {
+ try {
+ if (pfd != null) {
+ pfd.close();
+ }
+ } catch (IOException ex) {
+ }
+ }
+ }
+ }
+ // if no embedded art exists, look for AlbumArt.jpg in same directory as the media file
+ if (art == null && path != null) {
+ if (path.startsWith(sExternalMediaUri)) {
+ // get the real path
+ Cursor c = query(context,Uri.parse(path),
+ new String[] { MediaStore.Audio.Media.DATA},
+ null, null, null);
+ if (c != null) {
+ c.moveToFirst();
+ if (!c.isAfterLast()) {
+ path = c.getString(0);
+ }
+ c.close();
+ }
+ }
+ int lastSlash = path.lastIndexOf('/');
+ if (lastSlash > 0) {
+ String artPath = path.substring(0, lastSlash + 1) + "AlbumArt.jpg";
+ File file = new File(artPath);
+ if (file.exists()) {
+ art = new byte[(int)file.length()];
+ FileInputStream stream = null;
+ try {
+ stream = new FileInputStream(file);
+ stream.read(art);
+ } catch (IOException ex) {
+ art = null;
+ } finally {
+ try {
+ if (stream != null) {
+ stream.close();
+ }
+ } catch (IOException ex) {
+ }
+ }
+ } else {
+ // TODO: try getting album art from the web
+ }
+ }
+ }
+
+ if (art != null) {
+ try {
+ // get the size of the bitmap
+ BitmapFactory.Options opts = new BitmapFactory.Options();
+ opts.inJustDecodeBounds = true;
+ opts.inSampleSize = 1;
+ BitmapFactory.decodeByteArray(art, 0, art.length, opts);
+
+ // request a reasonably sized output image
+ // TODO: don't hardcode the size
+ while (opts.outHeight > 320 || opts.outWidth > 320) {
+ opts.outHeight /= 2;
+ opts.outWidth /= 2;
+ opts.inSampleSize *= 2;
+ }
+
+ // get the image for real now
+ opts.inJustDecodeBounds = false;
+ bm = BitmapFactory.decodeByteArray(art, 0, art.length, opts);
+ if (albumid != -1) {
+ sArtId = albumid;
+ }
+ mCachedArt = art;
+ mCachedBit = bm;
+ } catch (Exception e) {
+ }
+ }
+ 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("com.android.music", Context.MODE_PRIVATE);
+ return prefs.getInt(name, def);
+ }
+
+ static void setIntPref(Context context, String name, int value) {
+ SharedPreferences prefs =
+ context.getSharedPreferences("com.android.music", Context.MODE_PRIVATE);
+ Editor ed = prefs.edit();
+ ed.putInt(name, value);
+ ed.commit();
+ }
+
+ 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();
+ }
+ }
+ }
+}