/* * 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) { if (bm.getConfig() == null) { bm = bm.copy(Bitmap.Config.RGB_565, false); if (bm == null) { if (allowDefault) { return getDefaultArtwork(context); } else { return 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); 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) { if (c.moveToFirst()) { int trackid = c.getInt(0); uri = ContentUris.withAppendedId( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, trackid); if (MediaFile.UNKNOWN_STRING.equals(c.getString(1))) { 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(); } } } }