2 * Copyright (C) 2008 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.android.music;
20 import java.io.FileDescriptor;
21 import java.io.FileInputStream;
22 import java.io.FileNotFoundException;
23 import java.io.FileOutputStream;
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.io.OutputStream;
27 import java.util.Arrays;
28 import java.util.Formatter;
29 import java.util.HashMap;
30 import java.util.Locale;
32 import android.app.Activity;
33 import android.app.ExpandableListActivity;
34 import android.content.ComponentName;
35 import android.content.ContentResolver;
36 import android.content.ContentUris;
37 import android.content.ContentValues;
38 import android.content.Context;
39 import android.content.Intent;
40 import android.content.ServiceConnection;
41 import android.content.SharedPreferences;
42 import android.content.SharedPreferences.Editor;
43 import android.content.res.Resources;
44 import android.database.Cursor;
45 import android.graphics.Bitmap;
46 import android.graphics.BitmapFactory;
47 import android.graphics.Canvas;
48 import android.graphics.ColorFilter;
49 import android.graphics.PixelFormat;
50 import android.graphics.drawable.BitmapDrawable;
51 import android.graphics.drawable.Drawable;
52 import android.media.MediaFile;
53 import android.media.MediaScanner;
54 import android.net.Uri;
55 import android.os.RemoteException;
56 import android.os.Environment;
57 import android.os.ParcelFileDescriptor;
58 import android.provider.MediaStore;
59 import android.provider.Settings;
60 import android.util.Log;
61 import android.view.SubMenu;
62 import android.view.Window;
63 import android.widget.TextView;
64 import android.widget.Toast;
66 public class MusicUtils {
68 private static final String TAG = "MusicUtils";
70 public interface Defs {
71 public final static int OPEN_URL = 0;
72 public final static int ADD_TO_PLAYLIST = 1;
73 public final static int USE_AS_RINGTONE = 2;
74 public final static int PLAYLIST_SELECTED = 3;
75 public final static int NEW_PLAYLIST = 4;
76 public final static int PLAY_SELECTION = 5;
77 public final static int GOTO_START = 6;
78 public final static int GOTO_PLAYBACK = 7;
79 public final static int PARTY_SHUFFLE = 8;
80 public final static int SHUFFLE_ALL = 9;
81 public final static int DELETE_ITEM = 10;
82 public final static int SCAN_DONE = 11;
83 public final static int CHILD_MENU_BASE = 12;
84 public final static int QUEUE = 13;
87 public static String makeAlbumsSongsLabel(Context context, int numalbums, int numsongs, boolean isUnknown) {
88 // There are several formats for the albums/songs information:
89 // "1 Song" - used if there is only 1 song
90 // "N Songs" - used for the "unknown artist" item
91 // "1 Album"/"N Songs"
92 // "N Album"/"M Songs"
93 // Depending on locale, these may need to be further subdivided
95 StringBuilder songs_albums = new StringBuilder();
98 songs_albums.append(context.getString(R.string.onesong));
100 Resources r = context.getResources();
102 String f = r.getQuantityText(R.plurals.Nalbums, numalbums).toString();
103 sFormatBuilder.setLength(0);
104 sFormatter.format(f, Integer.valueOf(numalbums));
105 songs_albums.append(sFormatBuilder);
106 songs_albums.append(context.getString(R.string.albumsongseparator));
108 String f = r.getQuantityText(R.plurals.Nsongs, numsongs).toString();
109 sFormatBuilder.setLength(0);
110 sFormatter.format(f, Integer.valueOf(numsongs));
111 songs_albums.append(sFormatBuilder);
113 return songs_albums.toString();
116 public static IMediaPlaybackService sService = null;
117 private static HashMap<Context, ServiceBinder> sConnectionMap = new HashMap<Context, ServiceBinder>();
119 public static boolean bindToService(Context context) {
120 return bindToService(context, null);
123 public static boolean bindToService(Context context, ServiceConnection callback) {
124 context.startService(new Intent(context, MediaPlaybackService.class));
125 ServiceBinder sb = new ServiceBinder(callback);
126 sConnectionMap.put(context, sb);
127 return context.bindService((new Intent()).setClass(context,
128 MediaPlaybackService.class), sb, 0);
131 public static void unbindFromService(Context context) {
132 ServiceBinder sb = (ServiceBinder) sConnectionMap.remove(context);
134 Log.e("MusicUtils", "Trying to unbind for unknown Context");
137 context.unbindService(sb);
140 private static class ServiceBinder implements ServiceConnection {
141 ServiceConnection mCallback;
142 ServiceBinder(ServiceConnection callback) {
143 mCallback = callback;
146 public void onServiceConnected(ComponentName className, android.os.IBinder service) {
147 sService = IMediaPlaybackService.Stub.asInterface(service);
149 if (mCallback != null) {
150 mCallback.onServiceConnected(className, service);
154 public void onServiceDisconnected(ComponentName className) {
155 if (mCallback != null) {
156 mCallback.onServiceDisconnected(className);
162 public static int getCurrentAlbumId() {
163 if (sService != null) {
165 return sService.getAlbumId();
166 } catch (RemoteException ex) {
172 public static int getCurrentArtistId() {
173 if (MusicUtils.sService != null) {
175 return sService.getArtistId();
176 } catch (RemoteException ex) {
182 public static int getCurrentAudioId() {
183 if (MusicUtils.sService != null) {
185 return sService.getAudioId();
186 } catch (RemoteException ex) {
192 public static int getCurrentShuffleMode() {
193 int mode = MediaPlaybackService.SHUFFLE_NONE;
194 if (sService != null) {
196 mode = sService.getShuffleMode();
197 } catch (RemoteException ex) {
204 * Returns true if a file is currently opened for playback (regardless
205 * of whether it's playing or paused).
207 public static boolean isMusicLoaded() {
208 if (MusicUtils.sService != null) {
210 return sService.getPath() != null;
211 } catch (RemoteException ex) {
217 private final static int [] sEmptyList = new int[0];
219 public static int [] getSongListForCursor(Cursor cursor) {
220 if (cursor == null) {
223 int len = cursor.getCount();
224 int [] list = new int[len];
225 cursor.moveToFirst();
226 int colidx = cursor.getColumnIndex(MediaStore.Audio.Playlists.Members.AUDIO_ID);
228 colidx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID);
230 for (int i = 0; i < len; i++) {
231 list[i] = cursor.getInt(colidx);
237 public static int [] getSongListForArtist(Context context, int id) {
238 final String[] ccols = new String[] { MediaStore.Audio.Media._ID };
239 String where = MediaStore.Audio.Media.ARTIST_ID + "=" + id + " AND " +
240 MediaStore.Audio.Media.IS_MUSIC + "=1";
241 Cursor cursor = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
243 MediaStore.Audio.Media.ALBUM_KEY + "," + MediaStore.Audio.Media.TRACK);
245 if (cursor != null) {
246 int [] list = getSongListForCursor(cursor);
253 public static int [] getSongListForAlbum(Context context, int id) {
254 final String[] ccols = new String[] { MediaStore.Audio.Media._ID };
255 String where = MediaStore.Audio.Media.ALBUM_ID + "=" + id + " AND " +
256 MediaStore.Audio.Media.IS_MUSIC + "=1";
257 Cursor cursor = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
258 ccols, where, null, MediaStore.Audio.Media.TRACK);
260 if (cursor != null) {
261 int [] list = getSongListForCursor(cursor);
268 public static int [] getSongListForPlaylist(Context context, long plid) {
269 final String[] ccols = new String[] { MediaStore.Audio.Playlists.Members.AUDIO_ID };
270 Cursor cursor = query(context, MediaStore.Audio.Playlists.Members.getContentUri("external", plid),
271 ccols, null, null, MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER);
273 if (cursor != null) {
274 int [] list = getSongListForCursor(cursor);
281 public static void playPlaylist(Context context, long plid) {
282 int [] list = getSongListForPlaylist(context, plid);
284 playAll(context, list, -1, false);
288 public static int [] getAllSongs(Context context) {
289 Cursor c = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
290 new String[] {MediaStore.Audio.Media._ID}, MediaStore.Audio.Media.IS_MUSIC + "=1",
293 if (c == null || c.getCount() == 0) {
296 int len = c.getCount();
297 int[] list = new int[len];
298 for (int i = 0; i < len; i++) {
300 list[i] = c.getInt(0);
312 * Fills out the given submenu with items for "new playlist" and
313 * any existing playlists. When the user selects an item, the
314 * application will receive PLAYLIST_SELECTED with the Uri of
315 * the selected playlist, NEW_PLAYLIST if a new playlist
316 * should be created, and QUEUE if the "current playlist" was
318 * @param context The context to use for creating the menu items
319 * @param sub The submenu to add the items to.
321 public static void makePlaylistMenu(Context context, SubMenu sub) {
322 String[] cols = new String[] {
323 MediaStore.Audio.Playlists._ID,
324 MediaStore.Audio.Playlists.NAME
326 ContentResolver resolver = context.getContentResolver();
327 if (resolver == null) {
328 System.out.println("resolver = null");
330 String whereclause = MediaStore.Audio.Playlists.NAME + " != ''";
331 Cursor cur = resolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
332 cols, whereclause, null,
333 MediaStore.Audio.Playlists.NAME);
335 sub.add(1, Defs.QUEUE, 0, R.string.queue);
336 sub.add(1, Defs.NEW_PLAYLIST, 0, R.string.new_playlist);
337 if (cur != null && cur.getCount() > 0) {
338 //sub.addSeparator(1, 0);
340 while (! cur.isAfterLast()) {
341 Intent intent = new Intent();
342 intent.putExtra("playlist", cur.getInt(0));
343 // if (cur.getInt(0) == mLastPlaylistSelected) {
344 // sub.add(0, MusicBaseActivity.PLAYLIST_SELECTED, cur.getString(1)).setIntent(intent);
346 sub.add(1, Defs.PLAYLIST_SELECTED, 0, cur.getString(1)).setIntent(intent);
357 public static void clearPlaylist(Context context, int plid) {
358 final String[] ccols = new String[] { MediaStore.Audio.Playlists.Members._ID };
359 Cursor cursor = query(context, MediaStore.Audio.Playlists.Members.getContentUri("external", plid),
360 ccols, null, null, MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER);
362 if (cursor == null) {
365 cursor.moveToFirst();
366 while (!cursor.isAfterLast()) {
369 cursor.commitUpdates();
374 public static void deleteTracks(Context context, int [] list) {
376 String [] cols = new String [] { MediaStore.Audio.Media._ID,
377 MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.ALBUM_ID };
378 StringBuilder where = new StringBuilder();
379 where.append(MediaStore.Audio.Media._ID + " IN (");
380 for (int i = 0; i < list.length; i++) {
381 where.append(list[i]);
382 if (i < list.length - 1) {
387 Cursor c = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, cols,
388 where.toString(), null, null);
392 // step 1: remove selected tracks from the current playlist, as well
393 // as from the album art cache
396 while (! c.isAfterLast()) {
397 // remove from current playlist
398 int id = c.getInt(0);
399 sService.removeTrack(id);
400 // remove from album art cache
401 int artIndex = c.getInt(2);
402 synchronized(sArtCache) {
403 sArtCache.remove(artIndex);
407 } catch (RemoteException ex) {
410 // step 2: remove selected tracks from the database
411 context.getContentResolver().delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, where.toString(), null);
413 // step 3: remove files from card
415 while (! c.isAfterLast()) {
416 String name = c.getString(1);
417 File f = new File(name);
418 try { // File.delete can throw a security exception
420 // I'm not sure if we'd ever get here (deletion would
421 // have to fail, but no exception thrown)
422 Log.e("MusicUtils", "Failed to delete file " + name);
425 } catch (SecurityException ex) {
433 String message = context.getResources().getQuantityString(
434 R.plurals.NNNtracksdeleted, list.length, Integer.valueOf(list.length));
436 Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
437 // We deleted a number of tracks, which could affect any number of things
438 // in the media content domain, so update everything.
439 context.getContentResolver().notifyChange(Uri.parse("content://media"), null);
442 public static void addToCurrentPlaylist(Context context, int [] list) {
443 if (sService == null) {
447 sService.enqueue(list, MediaPlaybackService.LAST);
448 String message = context.getResources().getQuantityString(
449 R.plurals.NNNtrackstoplaylist, list.length, Integer.valueOf(list.length));
450 Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
451 } catch (RemoteException ex) {
455 public static void addToPlaylist(Context context, int [] ids, long playlistid) {
457 // this shouldn't happen (the menuitems shouldn't be visible
458 // unless the selected item represents something playable
459 Log.e("MusicBase", "ListSelection null");
461 int size = ids.length;
462 ContentValues values [] = new ContentValues[size];
463 ContentResolver resolver = context.getContentResolver();
464 // need to determine the number of items currently in the playlist,
465 // so the play_order field can be maintained.
466 String[] cols = new String[] {
469 Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistid);
470 Cursor cur = resolver.query(uri, cols, null, null, null);
472 int base = cur.getInt(0);
475 for (int i = 0; i < size; i++) {
476 values[i] = new ContentValues();
477 values[i].put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(base + i));
478 values[i].put(MediaStore.Audio.Playlists.Members.AUDIO_ID, ids[i]);
480 resolver.bulkInsert(uri, values);
481 String message = context.getResources().getQuantityString(
482 R.plurals.NNNtrackstoplaylist, size, Integer.valueOf(size));
483 Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
484 //mLastPlaylistSelected = playlistid;
488 public static Cursor query(Context context, Uri uri, String[] projection,
489 String selection, String[] selectionArgs, String sortOrder) {
491 ContentResolver resolver = context.getContentResolver();
492 if (resolver == null) {
495 return resolver.query(uri, projection, selection, selectionArgs, sortOrder);
496 } catch (UnsupportedOperationException ex) {
502 public static boolean isMediaScannerScanning(Context context) {
503 boolean result = false;
504 Uri uri = MediaStore.getMediaScannerUri();
505 Cursor cursor = query(context, MediaStore.getMediaScannerUri(),
506 new String [] { MediaStore.MEDIA_SCANNER_VOLUME }, null, null, null);
507 if (cursor != null) {
508 if (cursor.getCount() == 1) {
509 cursor.moveToFirst();
510 result = "external".equals(cursor.getString(0));
518 public static void setSpinnerState(Activity a) {
519 if (isMediaScannerScanning(a)) {
520 // start the progress spinner
521 a.getWindow().setFeatureInt(
522 Window.FEATURE_INDETERMINATE_PROGRESS,
523 Window.PROGRESS_INDETERMINATE_ON);
525 a.getWindow().setFeatureInt(
526 Window.FEATURE_INDETERMINATE_PROGRESS,
527 Window.PROGRESS_VISIBILITY_ON);
529 // stop the progress spinner
530 a.getWindow().setFeatureInt(
531 Window.FEATURE_INDETERMINATE_PROGRESS,
532 Window.PROGRESS_VISIBILITY_OFF);
536 public static void displayDatabaseError(Activity a) {
537 String status = Environment.getExternalStorageState();
538 int title = R.string.sdcard_error_title;
539 int message = R.string.sdcard_error_message;
541 if (status.equals(Environment.MEDIA_SHARED)) {
542 title = R.string.sdcard_busy_title;
543 message = R.string.sdcard_busy_message;
544 } else if (status.equals(Environment.MEDIA_REMOVED)) {
545 title = R.string.sdcard_missing_title;
546 message = R.string.sdcard_missing_message;
547 } else if (status.equals(Environment.MEDIA_MOUNTED)){
548 // The card is mounted, but we didn't get a valid cursor.
549 // This probably means the mediascanner hasn't started scanning the
550 // card yet (there is a small window of time during boot where this
553 Intent intent = new Intent();
554 intent.setClass(a, ScanningProgress.class);
555 a.startActivityForResult(intent, Defs.SCAN_DONE);
557 Log.d(TAG, "sd card: " + status);
561 if (a instanceof ExpandableListActivity) {
562 a.setContentView(R.layout.no_sd_card_expanding);
564 a.setContentView(R.layout.no_sd_card);
566 TextView tv = (TextView) a.findViewById(R.id.sd_message);
570 static protected Uri getContentURIForPath(String path) {
571 return Uri.fromFile(new File(path));
575 /* Try to use String.format() as little as possible, because it creates a
576 * new Formatter every time you call it, which is very inefficient.
577 * Reusing an existing Formatter more than tripled the speed of
579 * This Formatter/StringBuilder are also used by makeAlbumSongsLabel()
581 private static StringBuilder sFormatBuilder = new StringBuilder();
582 private static Formatter sFormatter = new Formatter(sFormatBuilder, Locale.getDefault());
583 private static final Object[] sTimeArgs = new Object[5];
585 public static String makeTimeString(Context context, long secs) {
586 String durationformat = context.getString(R.string.durationformat);
588 /* Provide multiple arguments so the format can be changed easily
589 * by modifying the xml.
591 sFormatBuilder.setLength(0);
593 final Object[] timeArgs = sTimeArgs;
594 timeArgs[0] = secs / 3600;
595 timeArgs[1] = secs / 60;
596 timeArgs[2] = (secs / 60) % 60;
598 timeArgs[4] = secs % 60;
600 return sFormatter.format(durationformat, timeArgs).toString();
603 public static void shuffleAll(Context context, Cursor cursor) {
604 playAll(context, cursor, 0, true);
607 public static void playAll(Context context, Cursor cursor) {
608 playAll(context, cursor, 0, false);
611 public static void playAll(Context context, Cursor cursor, int position) {
612 playAll(context, cursor, position, false);
615 public static void playAll(Context context, int [] list, int position) {
616 playAll(context, list, position, false);
619 private static void playAll(Context context, Cursor cursor, int position, boolean force_shuffle) {
621 int [] list = getSongListForCursor(cursor);
622 playAll(context, list, position, force_shuffle);
625 private static void playAll(Context context, int [] list, int position, boolean force_shuffle) {
626 if (list.length == 0 || sService == null) {
627 Log.d("MusicUtils", "attempt to play empty song list");
628 // Don't try to play empty playlists. Nothing good will come of it.
629 String message = context.getString(R.string.emptyplaylist, list.length);
630 Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
635 sService.setShuffleMode(MediaPlaybackService.SHUFFLE_NORMAL);
637 int curid = sService.getAudioId();
638 int curpos = sService.getQueuePosition();
639 if (position != -1 && curpos == position && curid == list[position]) {
640 // The selected file is the file that's currently playing;
641 // figure out if we need to restart with a new playlist,
642 // or just launch the playback activity.
643 int [] playlist = sService.getQueue();
644 if (Arrays.equals(list, playlist)) {
645 // we don't need to set a new list, but we should resume playback if needed
647 return; // the 'finally' block will still run
653 sService.open(list, position);
655 } catch (RemoteException ex) {
657 Intent intent = new Intent("com.android.music.PLAYBACK_VIEWER")
658 .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
659 context.startActivity(intent);
663 public static void clearQueue() {
665 sService.removeTracks(0, Integer.MAX_VALUE);
666 } catch (RemoteException ex) {
670 // A really simple BitmapDrawable-like class, that doesn't do
671 // scaling, dithering or filtering.
672 private static class FastBitmapDrawable extends Drawable {
673 private Bitmap mBitmap;
674 public FastBitmapDrawable(Bitmap b) {
678 public void draw(Canvas canvas) {
679 canvas.drawBitmap(mBitmap, 0, 0, null);
682 public int getOpacity() {
683 return PixelFormat.OPAQUE;
686 public void setAlpha(int alpha) {
689 public void setColorFilter(ColorFilter cf) {
693 private static int sArtId = -2;
694 private static byte [] mCachedArt;
695 private static Bitmap mCachedBit = null;
696 private static final BitmapFactory.Options sBitmapOptionsCache = new BitmapFactory.Options();
697 private static final BitmapFactory.Options sBitmapOptions = new BitmapFactory.Options();
698 private static final Uri sArtworkUri = Uri.parse("content://media/external/audio/albumart");
699 private static final HashMap<Integer, Drawable> sArtCache = new HashMap<Integer, Drawable>();
700 private static int sArtCacheId = -1;
704 // 565 is faster to decode and display
705 // and we don't want to dither here because the image will be scaled down later
706 sBitmapOptionsCache.inPreferredConfig = Bitmap.Config.RGB_565;
707 sBitmapOptionsCache.inDither = false;
709 sBitmapOptions.inPreferredConfig = Bitmap.Config.RGB_565;
710 sBitmapOptions.inDither = false;
713 public static void initAlbumArtCache() {
715 int id = sService.getMediaMountedCount();
716 if (id != sArtCacheId) {
717 clearAlbumArtCache();
720 } catch (RemoteException e) {
725 public static void clearAlbumArtCache() {
726 synchronized(sArtCache) {
731 public static Drawable getCachedArtwork(Context context, int artIndex, BitmapDrawable defaultArtwork) {
733 synchronized(sArtCache) {
734 d = sArtCache.get(artIndex);
738 final Bitmap icon = defaultArtwork.getBitmap();
739 int w = icon.getWidth();
740 int h = icon.getHeight();
741 Bitmap b = MusicUtils.getArtworkQuick(context, artIndex, w, h);
743 d = new FastBitmapDrawable(b);
744 synchronized(sArtCache) {
745 // the cache may have changed since we checked
746 Drawable value = sArtCache.get(artIndex);
748 sArtCache.put(artIndex, d);
758 // Get album art for specified album. This method will not try to
759 // fall back to getting artwork directly from the file, nor will
760 // it attempt to repair the database.
761 private static Bitmap getArtworkQuick(Context context, int album_id, int w, int h) {
762 // NOTE: There is in fact a 1 pixel frame in the ImageView used to
763 // display this drawable. Take it into account now, so we don't have to
767 ContentResolver res = context.getContentResolver();
768 Uri uri = ContentUris.withAppendedId(sArtworkUri, album_id);
770 ParcelFileDescriptor fd = null;
772 fd = res.openFileDescriptor(uri, "r");
775 // Compute the closest power-of-two scale factor
776 // and pass that to sBitmapOptionsCache.inSampleSize, which will
777 // result in faster decoding and better quality
778 sBitmapOptionsCache.inJustDecodeBounds = true;
779 BitmapFactory.decodeFileDescriptor(
780 fd.getFileDescriptor(), null, sBitmapOptionsCache);
781 int nextWidth = sBitmapOptionsCache.outWidth >> 1;
782 int nextHeight = sBitmapOptionsCache.outHeight >> 1;
783 while (nextWidth>w && nextHeight>h) {
789 sBitmapOptionsCache.inSampleSize = sampleSize;
790 sBitmapOptionsCache.inJustDecodeBounds = false;
791 Bitmap b = BitmapFactory.decodeFileDescriptor(
792 fd.getFileDescriptor(), null, sBitmapOptionsCache);
795 // finally rescale to exactly the size we need
796 if (sBitmapOptionsCache.outWidth != w || sBitmapOptionsCache.outHeight != h) {
797 Bitmap tmp = Bitmap.createScaledBitmap(b, w, h, true);
804 } catch (FileNotFoundException e) {
809 } catch (IOException e) {
816 // Get album art for specified album. You should not pass in the album id
817 // for the "unknown" album here (use -1 instead)
818 public static Bitmap getArtwork(Context context, int album_id) {
821 // This is something that is not in the database, so get the album art directly
823 Bitmap bm = getArtworkFromFile(context, null, -1);
827 return getDefaultArtwork(context);
830 ContentResolver res = context.getContentResolver();
831 Uri uri = ContentUris.withAppendedId(sArtworkUri, album_id);
833 InputStream in = null;
835 in = res.openInputStream(uri);
836 return BitmapFactory.decodeStream(in, null, sBitmapOptions);
837 } catch (FileNotFoundException ex) {
838 // The album art thumbnail does not actually exist. Maybe the user deleted it, or
839 // maybe it never existed to begin with.
840 Bitmap bm = getArtworkFromFile(context, null, album_id);
842 // Put the newly found artwork in the database.
843 // Note that this shouldn't be done for the "unknown" album,
844 // but if this method is called correctly, that won't happen.
846 // first write it somewhere
847 String file = Environment.getExternalStorageDirectory()
848 + "/albumthumbs/" + String.valueOf(System.currentTimeMillis());
849 if (ensureFileExists(file)) {
851 OutputStream outstream = new FileOutputStream(file);
852 if (bm.getConfig() == null) {
853 bm = bm.copy(Bitmap.Config.RGB_565, false);
855 return getDefaultArtwork(context);
858 boolean success = bm.compress(Bitmap.CompressFormat.JPEG, 75, outstream);
861 ContentValues values = new ContentValues();
862 values.put("album_id", album_id);
863 values.put("_data", file);
864 Uri newuri = res.insert(sArtworkUri, values);
865 if (newuri == null) {
866 // Failed to insert in to the database. The most likely
867 // cause of this is that the item already existed in the
868 // database, and the most likely cause of that is that
869 // the album was scanned before, but the user deleted the
870 // album art from the sd card.
871 // We can ignore that case here, since the media provider
872 // will regenerate the album art for those entries when
878 File f = new File(file);
881 } catch (FileNotFoundException e) {
882 Log.e(TAG, "error creating file", e);
883 } catch (IOException e) {
884 Log.e(TAG, "error creating file", e);
888 bm = getDefaultArtwork(context);
896 } catch (IOException ex) {
904 // copied from MediaProvider
905 private static boolean ensureFileExists(String path) {
906 File file = new File(path);
910 // we will not attempt to create the first directory in the path
911 // (for example, do not create /sdcard if the SD card is not mounted)
912 int secondSlash = path.indexOf('/', 1);
913 if (secondSlash < 1) return false;
914 String directoryPath = path.substring(0, secondSlash);
915 File directory = new File(directoryPath);
916 if (!directory.exists())
918 file.getParentFile().mkdirs();
920 return file.createNewFile();
921 } catch(IOException ioe) {
922 Log.e(TAG, "File creation failed", ioe);
928 // get album art for specified file
929 private static final String sExternalMediaUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString();
930 private static Bitmap getArtworkFromFile(Context context, Uri uri, int albumid) {
935 if (sArtId == albumid) {
936 //Log.i("@@@@@@ ", "reusing cached data", new Exception());
937 if (mCachedBit != null) {
942 // try reading embedded artwork
945 int curalbum = sService.getAlbumId();
946 if (curalbum == albumid || albumid < 0) {
947 path = sService.getPath();
949 uri = Uri.parse(path);
952 } catch (RemoteException ex) {
957 Cursor c = query(context,MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
958 new String[] { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.ALBUM },
959 MediaStore.Audio.Media.ALBUM_ID + "=?", new String [] {String.valueOf(albumid)},
963 if (!c.isAfterLast()) {
964 int trackid = c.getInt(0);
965 uri = ContentUris.withAppendedId(
966 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, trackid);
968 if (c.getString(1).equals(MediaFile.UNKNOWN_STRING)) {
976 MediaScanner scanner = new MediaScanner(context);
977 ParcelFileDescriptor pfd = null;
979 pfd = context.getContentResolver().openFileDescriptor(uri, "r");
981 FileDescriptor fd = pfd.getFileDescriptor();
982 art = scanner.extractAlbumArt(fd);
984 } catch (IOException ex) {
985 } catch (SecurityException ex) {
991 } catch (IOException ex) {
996 // if no embedded art exists, look for AlbumArt.jpg in same directory as the media file
997 if (art == null && path != null) {
998 if (path.startsWith(sExternalMediaUri)) {
1000 Cursor c = query(context,Uri.parse(path),
1001 new String[] { MediaStore.Audio.Media.DATA},
1005 if (!c.isAfterLast()) {
1006 path = c.getString(0);
1011 int lastSlash = path.lastIndexOf('/');
1012 if (lastSlash > 0) {
1013 String artPath = path.substring(0, lastSlash + 1) + "AlbumArt.jpg";
1014 File file = new File(artPath);
1015 if (file.exists()) {
1016 art = new byte[(int)file.length()];
1017 FileInputStream stream = null;
1019 stream = new FileInputStream(file);
1021 } catch (IOException ex) {
1025 if (stream != null) {
1028 } catch (IOException ex) {
1032 // TODO: try getting album art from the web
1039 // get the size of the bitmap
1040 BitmapFactory.Options opts = new BitmapFactory.Options();
1041 opts.inJustDecodeBounds = true;
1042 opts.inSampleSize = 1;
1043 BitmapFactory.decodeByteArray(art, 0, art.length, opts);
1045 // request a reasonably sized output image
1046 // TODO: don't hardcode the size
1047 while (opts.outHeight > 320 || opts.outWidth > 320) {
1048 opts.outHeight /= 2;
1050 opts.inSampleSize *= 2;
1053 // get the image for real now
1054 opts.inJustDecodeBounds = false;
1055 bm = BitmapFactory.decodeByteArray(art, 0, art.length, opts);
1056 if (albumid != -1) {
1061 } catch (Exception e) {
1067 private static Bitmap getDefaultArtwork(Context context) {
1068 BitmapFactory.Options opts = new BitmapFactory.Options();
1069 opts.inPreferredConfig = Bitmap.Config.ARGB_8888;
1070 return BitmapFactory.decodeStream(
1071 context.getResources().openRawResource(R.drawable.albumart_mp_unknown), null, opts);
1074 static int getIntPref(Context context, String name, int def) {
1075 SharedPreferences prefs =
1076 context.getSharedPreferences("com.android.music", Context.MODE_PRIVATE);
1077 return prefs.getInt(name, def);
1080 static void setIntPref(Context context, String name, int value) {
1081 SharedPreferences prefs =
1082 context.getSharedPreferences("com.android.music", Context.MODE_PRIVATE);
1083 Editor ed = prefs.edit();
1084 ed.putInt(name, value);
1088 static void setRingtone(Context context, long id) {
1089 ContentResolver resolver = context.getContentResolver();
1090 // Set the flag in the database to mark this as a ringtone
1091 Uri ringUri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
1093 ContentValues values = new ContentValues(2);
1094 values.put(MediaStore.Audio.Media.IS_RINGTONE, "1");
1095 values.put(MediaStore.Audio.Media.IS_ALARM, "1");
1096 resolver.update(ringUri, values, null, null);
1097 } catch (UnsupportedOperationException ex) {
1098 // most likely the card just got unmounted
1099 Log.e(TAG, "couldn't set ringtone flag for id " + id);
1103 String[] cols = new String[] {
1104 MediaStore.Audio.Media._ID,
1105 MediaStore.Audio.Media.DATA,
1106 MediaStore.Audio.Media.TITLE
1109 String where = MediaStore.Audio.Media._ID + "=" + id;
1110 Cursor cursor = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1111 cols, where , null, null);
1113 if (cursor != null && cursor.getCount() == 1) {
1114 // Set the system setting to make this the current ringtone
1115 cursor.moveToFirst();
1116 Settings.System.putString(resolver, Settings.System.RINGTONE, ringUri.toString());
1117 String message = context.getString(R.string.ringtone_set, cursor.getString(2));
1118 Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
1121 if (cursor != null) {