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;
19 import android.app.Activity;
20 import android.content.ComponentName;
21 import android.content.ContentResolver;
22 import android.content.ContentUris;
23 import android.content.ContentValues;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.ServiceConnection;
27 import android.content.SharedPreferences;
28 import android.content.SharedPreferences.Editor;
29 import android.content.res.Resources;
30 import android.database.Cursor;
31 import android.graphics.Bitmap;
32 import android.graphics.BitmapFactory;
33 import android.graphics.Canvas;
34 import android.graphics.ColorFilter;
35 import android.graphics.PixelFormat;
36 import android.graphics.drawable.BitmapDrawable;
37 import android.graphics.drawable.Drawable;
38 import android.net.Uri;
39 import android.os.Environment;
40 import android.os.ParcelFileDescriptor;
41 import android.os.RemoteException;
42 import android.provider.MediaStore;
43 import android.provider.Settings;
44 import android.text.TextUtils;
45 import android.util.Log;
46 import android.view.SubMenu;
47 import android.view.View;
48 import android.view.Window;
49 import android.widget.TextView;
50 import android.widget.Toast;
53 import java.io.FileDescriptor;
54 import java.io.FileNotFoundException;
55 import java.io.IOException;
56 import java.io.InputStream;
57 import java.util.Arrays;
58 import java.util.Formatter;
59 import java.util.HashMap;
60 import java.util.Locale;
62 public class MusicUtils {
64 private static final String TAG = "MusicUtils";
66 public interface Defs {
67 public final static int OPEN_URL = 0;
68 public final static int ADD_TO_PLAYLIST = 1;
69 public final static int USE_AS_RINGTONE = 2;
70 public final static int PLAYLIST_SELECTED = 3;
71 public final static int NEW_PLAYLIST = 4;
72 public final static int PLAY_SELECTION = 5;
73 public final static int GOTO_START = 6;
74 public final static int GOTO_PLAYBACK = 7;
75 public final static int PARTY_SHUFFLE = 8;
76 public final static int SHUFFLE_ALL = 9;
77 public final static int DELETE_ITEM = 10;
78 public final static int SCAN_DONE = 11;
79 public final static int QUEUE = 12;
80 public final static int CHILD_MENU_BASE = 13; // this should be the last item
83 public static String makeAlbumsLabel(Context context, int numalbums, int numsongs, boolean isUnknown) {
84 // There are two formats for the albums/songs information:
85 // "N Song(s)" - used for unknown artist/album
86 // "N Album(s)" - used for known albums
88 StringBuilder songs_albums = new StringBuilder();
90 Resources r = context.getResources();
93 songs_albums.append(context.getString(R.string.onesong));
95 String f = r.getQuantityText(R.plurals.Nsongs, numsongs).toString();
96 sFormatBuilder.setLength(0);
97 sFormatter.format(f, Integer.valueOf(numsongs));
98 songs_albums.append(sFormatBuilder);
101 String f = r.getQuantityText(R.plurals.Nalbums, numalbums).toString();
102 sFormatBuilder.setLength(0);
103 sFormatter.format(f, Integer.valueOf(numalbums));
104 songs_albums.append(sFormatBuilder);
105 songs_albums.append(context.getString(R.string.albumsongseparator));
107 return songs_albums.toString();
111 * This is now only used for the query screen
113 public static String makeAlbumsSongsLabel(Context context, int numalbums, int numsongs, boolean isUnknown) {
114 // There are several formats for the albums/songs information:
115 // "1 Song" - used if there is only 1 song
116 // "N Songs" - used for the "unknown artist" item
117 // "1 Album"/"N Songs"
118 // "N Album"/"M Songs"
119 // Depending on locale, these may need to be further subdivided
121 StringBuilder songs_albums = new StringBuilder();
124 songs_albums.append(context.getString(R.string.onesong));
126 Resources r = context.getResources();
128 String f = r.getQuantityText(R.plurals.Nalbums, numalbums).toString();
129 sFormatBuilder.setLength(0);
130 sFormatter.format(f, Integer.valueOf(numalbums));
131 songs_albums.append(sFormatBuilder);
132 songs_albums.append(context.getString(R.string.albumsongseparator));
134 String f = r.getQuantityText(R.plurals.Nsongs, numsongs).toString();
135 sFormatBuilder.setLength(0);
136 sFormatter.format(f, Integer.valueOf(numsongs));
137 songs_albums.append(sFormatBuilder);
139 return songs_albums.toString();
142 public static IMediaPlaybackService sService = null;
143 private static HashMap<Context, ServiceBinder> sConnectionMap = new HashMap<Context, ServiceBinder>();
145 public static boolean bindToService(Context context) {
146 return bindToService(context, null);
149 public static boolean bindToService(Context context, ServiceConnection callback) {
150 context.startService(new Intent(context, MediaPlaybackService.class));
151 ServiceBinder sb = new ServiceBinder(callback);
152 sConnectionMap.put(context, sb);
153 return context.bindService((new Intent()).setClass(context,
154 MediaPlaybackService.class), sb, 0);
157 public static void unbindFromService(Context context) {
158 ServiceBinder sb = (ServiceBinder) sConnectionMap.remove(context);
160 Log.e("MusicUtils", "Trying to unbind for unknown Context");
163 context.unbindService(sb);
164 if (sConnectionMap.isEmpty()) {
165 // presumably there is nobody interested in the service at this point,
166 // so don't hang on to the ServiceConnection
171 private static class ServiceBinder implements ServiceConnection {
172 ServiceConnection mCallback;
173 ServiceBinder(ServiceConnection callback) {
174 mCallback = callback;
177 public void onServiceConnected(ComponentName className, android.os.IBinder service) {
178 sService = IMediaPlaybackService.Stub.asInterface(service);
180 if (mCallback != null) {
181 mCallback.onServiceConnected(className, service);
185 public void onServiceDisconnected(ComponentName className) {
186 if (mCallback != null) {
187 mCallback.onServiceDisconnected(className);
193 public static long getCurrentAlbumId() {
194 if (sService != null) {
196 return sService.getAlbumId();
197 } catch (RemoteException ex) {
203 public static long getCurrentArtistId() {
204 if (MusicUtils.sService != null) {
206 return sService.getArtistId();
207 } catch (RemoteException ex) {
213 public static long getCurrentAudioId() {
214 if (MusicUtils.sService != null) {
216 return sService.getAudioId();
217 } catch (RemoteException ex) {
223 public static int getCurrentShuffleMode() {
224 int mode = MediaPlaybackService.SHUFFLE_NONE;
225 if (sService != null) {
227 mode = sService.getShuffleMode();
228 } catch (RemoteException ex) {
235 * Returns true if a file is currently opened for playback (regardless
236 * of whether it's playing or paused).
238 public static boolean isMusicLoaded() {
239 if (MusicUtils.sService != null) {
241 return sService.getPath() != null;
242 } catch (RemoteException ex) {
248 private final static long [] sEmptyList = new long[0];
250 public static long [] getSongListForCursor(Cursor cursor) {
251 if (cursor == null) {
254 int len = cursor.getCount();
255 long [] list = new long[len];
256 cursor.moveToFirst();
259 colidx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.Members.AUDIO_ID);
260 } catch (IllegalArgumentException ex) {
261 colidx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID);
263 for (int i = 0; i < len; i++) {
264 list[i] = cursor.getLong(colidx);
270 public static long [] getSongListForArtist(Context context, long id) {
271 final String[] ccols = new String[] { MediaStore.Audio.Media._ID };
272 String where = MediaStore.Audio.Media.ARTIST_ID + "=" + id + " AND " +
273 MediaStore.Audio.Media.IS_MUSIC + "=1";
274 Cursor cursor = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
276 MediaStore.Audio.Media.ALBUM_KEY + "," + MediaStore.Audio.Media.TRACK);
278 if (cursor != null) {
279 long [] list = getSongListForCursor(cursor);
286 public static long [] getSongListForAlbum(Context context, long id) {
287 final String[] ccols = new String[] { MediaStore.Audio.Media._ID };
288 String where = MediaStore.Audio.Media.ALBUM_ID + "=" + id + " AND " +
289 MediaStore.Audio.Media.IS_MUSIC + "=1";
290 Cursor cursor = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
291 ccols, where, null, MediaStore.Audio.Media.TRACK);
293 if (cursor != null) {
294 long [] list = getSongListForCursor(cursor);
301 public static long [] getSongListForPlaylist(Context context, long plid) {
302 final String[] ccols = new String[] { MediaStore.Audio.Playlists.Members.AUDIO_ID };
303 Cursor cursor = query(context, MediaStore.Audio.Playlists.Members.getContentUri("external", plid),
304 ccols, null, null, MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER);
306 if (cursor != null) {
307 long [] list = getSongListForCursor(cursor);
314 public static void playPlaylist(Context context, long plid) {
315 long [] list = getSongListForPlaylist(context, plid);
317 playAll(context, list, -1, false);
321 public static long [] getAllSongs(Context context) {
322 Cursor c = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
323 new String[] {MediaStore.Audio.Media._ID}, MediaStore.Audio.Media.IS_MUSIC + "=1",
326 if (c == null || c.getCount() == 0) {
329 int len = c.getCount();
330 long [] list = new long[len];
331 for (int i = 0; i < len; i++) {
333 list[i] = c.getLong(0);
345 * Fills out the given submenu with items for "new playlist" and
346 * any existing playlists. When the user selects an item, the
347 * application will receive PLAYLIST_SELECTED with the Uri of
348 * the selected playlist, NEW_PLAYLIST if a new playlist
349 * should be created, and QUEUE if the "current playlist" was
351 * @param context The context to use for creating the menu items
352 * @param sub The submenu to add the items to.
354 public static void makePlaylistMenu(Context context, SubMenu sub) {
355 String[] cols = new String[] {
356 MediaStore.Audio.Playlists._ID,
357 MediaStore.Audio.Playlists.NAME
359 ContentResolver resolver = context.getContentResolver();
360 if (resolver == null) {
361 System.out.println("resolver = null");
363 String whereclause = MediaStore.Audio.Playlists.NAME + " != ''";
364 Cursor cur = resolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
365 cols, whereclause, null,
366 MediaStore.Audio.Playlists.NAME);
368 sub.add(1, Defs.QUEUE, 0, R.string.queue);
369 sub.add(1, Defs.NEW_PLAYLIST, 0, R.string.new_playlist);
370 if (cur != null && cur.getCount() > 0) {
371 //sub.addSeparator(1, 0);
373 while (! cur.isAfterLast()) {
374 Intent intent = new Intent();
375 intent.putExtra("playlist", cur.getLong(0));
376 // if (cur.getInt(0) == mLastPlaylistSelected) {
377 // sub.add(0, MusicBaseActivity.PLAYLIST_SELECTED, cur.getString(1)).setIntent(intent);
379 sub.add(1, Defs.PLAYLIST_SELECTED, 0, cur.getString(1)).setIntent(intent);
390 public static void clearPlaylist(Context context, int plid) {
392 Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", plid);
393 context.getContentResolver().delete(uri, null, null);
397 public static void deleteTracks(Context context, long [] list) {
399 String [] cols = new String [] { MediaStore.Audio.Media._ID,
400 MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.ALBUM_ID };
401 StringBuilder where = new StringBuilder();
402 where.append(MediaStore.Audio.Media._ID + " IN (");
403 for (int i = 0; i < list.length; i++) {
404 where.append(list[i]);
405 if (i < list.length - 1) {
410 Cursor c = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, cols,
411 where.toString(), null, null);
415 // step 1: remove selected tracks from the current playlist, as well
416 // as from the album art cache
419 while (! c.isAfterLast()) {
420 // remove from current playlist
421 long id = c.getLong(0);
422 sService.removeTrack(id);
423 // remove from album art cache
424 long artIndex = c.getLong(2);
425 synchronized(sArtCache) {
426 sArtCache.remove(artIndex);
430 } catch (RemoteException ex) {
433 // step 2: remove selected tracks from the database
434 context.getContentResolver().delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, where.toString(), null);
436 // step 3: remove files from card
438 while (! c.isAfterLast()) {
439 String name = c.getString(1);
440 File f = new File(name);
441 try { // File.delete can throw a security exception
443 // I'm not sure if we'd ever get here (deletion would
444 // have to fail, but no exception thrown)
445 Log.e("MusicUtils", "Failed to delete file " + name);
448 } catch (SecurityException ex) {
455 String message = context.getResources().getQuantityString(
456 R.plurals.NNNtracksdeleted, list.length, Integer.valueOf(list.length));
458 Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
459 // We deleted a number of tracks, which could affect any number of things
460 // in the media content domain, so update everything.
461 context.getContentResolver().notifyChange(Uri.parse("content://media"), null);
464 public static void addToCurrentPlaylist(Context context, long [] list) {
465 if (sService == null) {
469 sService.enqueue(list, MediaPlaybackService.LAST);
470 String message = context.getResources().getQuantityString(
471 R.plurals.NNNtrackstoplaylist, list.length, Integer.valueOf(list.length));
472 Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
473 } catch (RemoteException ex) {
477 public static void addToPlaylist(Context context, long [] ids, long playlistid) {
479 // this shouldn't happen (the menuitems shouldn't be visible
480 // unless the selected item represents something playable
481 Log.e("MusicBase", "ListSelection null");
483 int size = ids.length;
484 ContentValues values [] = new ContentValues[size];
485 ContentResolver resolver = context.getContentResolver();
486 // need to determine the number of items currently in the playlist,
487 // so the play_order field can be maintained.
488 String[] cols = new String[] {
491 Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistid);
492 Cursor cur = resolver.query(uri, cols, null, null, null);
494 int base = cur.getInt(0);
497 for (int i = 0; i < size; i++) {
498 values[i] = new ContentValues();
499 values[i].put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(base + i));
500 values[i].put(MediaStore.Audio.Playlists.Members.AUDIO_ID, ids[i]);
502 resolver.bulkInsert(uri, values);
503 String message = context.getResources().getQuantityString(
504 R.plurals.NNNtrackstoplaylist, size, Integer.valueOf(size));
505 Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
506 //mLastPlaylistSelected = playlistid;
510 public static Cursor query(Context context, Uri uri, String[] projection,
511 String selection, String[] selectionArgs, String sortOrder, int limit) {
513 ContentResolver resolver = context.getContentResolver();
514 if (resolver == null) {
518 uri = uri.buildUpon().appendQueryParameter("limit", "" + limit).build();
520 return resolver.query(uri, projection, selection, selectionArgs, sortOrder);
521 } catch (UnsupportedOperationException ex) {
526 public static Cursor query(Context context, Uri uri, String[] projection,
527 String selection, String[] selectionArgs, String sortOrder) {
528 return query(context, uri, projection, selection, selectionArgs, sortOrder, 0);
531 public static boolean isMediaScannerScanning(Context context) {
532 boolean result = false;
533 Cursor cursor = query(context, MediaStore.getMediaScannerUri(),
534 new String [] { MediaStore.MEDIA_SCANNER_VOLUME }, null, null, null);
535 if (cursor != null) {
536 if (cursor.getCount() == 1) {
537 cursor.moveToFirst();
538 result = "external".equals(cursor.getString(0));
546 public static void setSpinnerState(Activity a) {
547 if (isMediaScannerScanning(a)) {
548 // start the progress spinner
549 a.getWindow().setFeatureInt(
550 Window.FEATURE_INDETERMINATE_PROGRESS,
551 Window.PROGRESS_INDETERMINATE_ON);
553 a.getWindow().setFeatureInt(
554 Window.FEATURE_INDETERMINATE_PROGRESS,
555 Window.PROGRESS_VISIBILITY_ON);
557 // stop the progress spinner
558 a.getWindow().setFeatureInt(
559 Window.FEATURE_INDETERMINATE_PROGRESS,
560 Window.PROGRESS_VISIBILITY_OFF);
564 private static String mLastSdStatus;
566 public static void displayDatabaseError(Activity a) {
567 String status = Environment.getExternalStorageState();
568 int title = R.string.sdcard_error_title;
569 int message = R.string.sdcard_error_message;
571 if (status.equals(Environment.MEDIA_SHARED) ||
572 status.equals(Environment.MEDIA_UNMOUNTED)) {
573 title = R.string.sdcard_busy_title;
574 message = R.string.sdcard_busy_message;
575 } else if (status.equals(Environment.MEDIA_REMOVED)) {
576 title = R.string.sdcard_missing_title;
577 message = R.string.sdcard_missing_message;
578 } else if (status.equals(Environment.MEDIA_MOUNTED)){
579 // The card is mounted, but we didn't get a valid cursor.
580 // This probably means the mediascanner hasn't started scanning the
581 // card yet (there is a small window of time during boot where this
584 Intent intent = new Intent();
585 intent.setClass(a, ScanningProgress.class);
586 a.startActivityForResult(intent, Defs.SCAN_DONE);
587 } else if (!TextUtils.equals(mLastSdStatus, status)) {
588 mLastSdStatus = status;
589 Log.d(TAG, "sd card: " + status);
593 View v = a.findViewById(R.id.sd_message);
595 v.setVisibility(View.VISIBLE);
597 v = a.findViewById(R.id.sd_icon);
599 v.setVisibility(View.VISIBLE);
601 v = a.findViewById(android.R.id.list);
603 v.setVisibility(View.GONE);
605 TextView tv = (TextView) a.findViewById(R.id.sd_message);
609 public static void hideDatabaseError(Activity a) {
610 View v = a.findViewById(R.id.sd_message);
612 v.setVisibility(View.GONE);
614 v = a.findViewById(R.id.sd_icon);
616 v.setVisibility(View.GONE);
618 v = a.findViewById(android.R.id.list);
620 v.setVisibility(View.VISIBLE);
624 static protected Uri getContentURIForPath(String path) {
625 return Uri.fromFile(new File(path));
629 /* Try to use String.format() as little as possible, because it creates a
630 * new Formatter every time you call it, which is very inefficient.
631 * Reusing an existing Formatter more than tripled the speed of
633 * This Formatter/StringBuilder are also used by makeAlbumSongsLabel()
635 private static StringBuilder sFormatBuilder = new StringBuilder();
636 private static Formatter sFormatter = new Formatter(sFormatBuilder, Locale.getDefault());
637 private static final Object[] sTimeArgs = new Object[5];
639 public static String makeTimeString(Context context, long secs) {
640 String durationformat = context.getString(
641 secs < 3600 ? R.string.durationformatshort : R.string.durationformatlong);
643 /* Provide multiple arguments so the format can be changed easily
644 * by modifying the xml.
646 sFormatBuilder.setLength(0);
648 final Object[] timeArgs = sTimeArgs;
649 timeArgs[0] = secs / 3600;
650 timeArgs[1] = secs / 60;
651 timeArgs[2] = (secs / 60) % 60;
653 timeArgs[4] = secs % 60;
655 return sFormatter.format(durationformat, timeArgs).toString();
658 public static void shuffleAll(Context context, Cursor cursor) {
659 playAll(context, cursor, 0, true);
662 public static void playAll(Context context, Cursor cursor) {
663 playAll(context, cursor, 0, false);
666 public static void playAll(Context context, Cursor cursor, int position) {
667 playAll(context, cursor, position, false);
670 public static void playAll(Context context, long [] list, int position) {
671 playAll(context, list, position, false);
674 private static void playAll(Context context, Cursor cursor, int position, boolean force_shuffle) {
676 long [] list = getSongListForCursor(cursor);
677 playAll(context, list, position, force_shuffle);
680 private static void playAll(Context context, long [] list, int position, boolean force_shuffle) {
681 if (list.length == 0 || sService == null) {
682 Log.d("MusicUtils", "attempt to play empty song list");
683 // Don't try to play empty playlists. Nothing good will come of it.
684 String message = context.getString(R.string.emptyplaylist, list.length);
685 Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
690 sService.setShuffleMode(MediaPlaybackService.SHUFFLE_NORMAL);
692 long curid = sService.getAudioId();
693 int curpos = sService.getQueuePosition();
694 if (position != -1 && curpos == position && curid == list[position]) {
695 // The selected file is the file that's currently playing;
696 // figure out if we need to restart with a new playlist,
697 // or just launch the playback activity.
698 long [] playlist = sService.getQueue();
699 if (Arrays.equals(list, playlist)) {
700 // we don't need to set a new list, but we should resume playback if needed
702 return; // the 'finally' block will still run
708 sService.open(list, force_shuffle ? -1 : position);
710 } catch (RemoteException ex) {
712 Intent intent = new Intent("com.android.music.PLAYBACK_VIEWER")
713 .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
714 context.startActivity(intent);
718 public static void clearQueue() {
720 sService.removeTracks(0, Integer.MAX_VALUE);
721 } catch (RemoteException ex) {
725 // A really simple BitmapDrawable-like class, that doesn't do
726 // scaling, dithering or filtering.
727 private static class FastBitmapDrawable extends Drawable {
728 private Bitmap mBitmap;
729 public FastBitmapDrawable(Bitmap b) {
733 public void draw(Canvas canvas) {
734 canvas.drawBitmap(mBitmap, 0, 0, null);
737 public int getOpacity() {
738 return PixelFormat.OPAQUE;
741 public void setAlpha(int alpha) {
744 public void setColorFilter(ColorFilter cf) {
748 private static int sArtId = -2;
749 private static Bitmap mCachedBit = null;
750 private static final BitmapFactory.Options sBitmapOptionsCache = new BitmapFactory.Options();
751 private static final BitmapFactory.Options sBitmapOptions = new BitmapFactory.Options();
752 private static final Uri sArtworkUri = Uri.parse("content://media/external/audio/albumart");
753 private static final HashMap<Long, Drawable> sArtCache = new HashMap<Long, Drawable>();
754 private static int sArtCacheId = -1;
758 // 565 is faster to decode and display
759 // and we don't want to dither here because the image will be scaled down later
760 sBitmapOptionsCache.inPreferredConfig = Bitmap.Config.RGB_565;
761 sBitmapOptionsCache.inDither = false;
763 sBitmapOptions.inPreferredConfig = Bitmap.Config.RGB_565;
764 sBitmapOptions.inDither = false;
767 public static void initAlbumArtCache() {
769 int id = sService.getMediaMountedCount();
770 if (id != sArtCacheId) {
771 clearAlbumArtCache();
774 } catch (RemoteException e) {
779 public static void clearAlbumArtCache() {
780 synchronized(sArtCache) {
785 public static Drawable getCachedArtwork(Context context, long artIndex, BitmapDrawable defaultArtwork) {
787 synchronized(sArtCache) {
788 d = sArtCache.get(artIndex);
792 final Bitmap icon = defaultArtwork.getBitmap();
793 int w = icon.getWidth();
794 int h = icon.getHeight();
795 Bitmap b = MusicUtils.getArtworkQuick(context, artIndex, w, h);
797 d = new FastBitmapDrawable(b);
798 synchronized(sArtCache) {
799 // the cache may have changed since we checked
800 Drawable value = sArtCache.get(artIndex);
802 sArtCache.put(artIndex, d);
812 // Get album art for specified album. This method will not try to
813 // fall back to getting artwork directly from the file, nor will
814 // it attempt to repair the database.
815 private static Bitmap getArtworkQuick(Context context, long album_id, int w, int h) {
816 // NOTE: There is in fact a 1 pixel border on the right side in the ImageView
817 // used to display this drawable. Take it into account now, so we don't have to
820 ContentResolver res = context.getContentResolver();
821 Uri uri = ContentUris.withAppendedId(sArtworkUri, album_id);
823 ParcelFileDescriptor fd = null;
825 fd = res.openFileDescriptor(uri, "r");
828 // Compute the closest power-of-two scale factor
829 // and pass that to sBitmapOptionsCache.inSampleSize, which will
830 // result in faster decoding and better quality
831 sBitmapOptionsCache.inJustDecodeBounds = true;
832 BitmapFactory.decodeFileDescriptor(
833 fd.getFileDescriptor(), null, sBitmapOptionsCache);
834 int nextWidth = sBitmapOptionsCache.outWidth >> 1;
835 int nextHeight = sBitmapOptionsCache.outHeight >> 1;
836 while (nextWidth>w && nextHeight>h) {
842 sBitmapOptionsCache.inSampleSize = sampleSize;
843 sBitmapOptionsCache.inJustDecodeBounds = false;
844 Bitmap b = BitmapFactory.decodeFileDescriptor(
845 fd.getFileDescriptor(), null, sBitmapOptionsCache);
848 // finally rescale to exactly the size we need
849 if (sBitmapOptionsCache.outWidth != w || sBitmapOptionsCache.outHeight != h) {
850 Bitmap tmp = Bitmap.createScaledBitmap(b, w, h, true);
851 // Bitmap.createScaledBitmap() can return the same bitmap
852 if (tmp != b) b.recycle();
858 } catch (FileNotFoundException e) {
863 } catch (IOException e) {
870 /** Get album art for specified album. You should not pass in the album id
871 * for the "unknown" album here (use -1 instead)
873 public static Bitmap getArtwork(Context context, long song_id, long album_id) {
876 // This is something that is not in the database, so get the album art directly
879 Bitmap bm = getArtworkFromFile(context, song_id, -1);
884 return getDefaultArtwork(context);
887 ContentResolver res = context.getContentResolver();
888 Uri uri = ContentUris.withAppendedId(sArtworkUri, album_id);
890 InputStream in = null;
892 in = res.openInputStream(uri);
893 return BitmapFactory.decodeStream(in, null, sBitmapOptions);
894 } catch (FileNotFoundException ex) {
895 // The album art thumbnail does not actually exist. Maybe the user deleted it, or
896 // maybe it never existed to begin with.
897 Bitmap bm = getArtworkFromFile(context, song_id, album_id);
899 if (bm.getConfig() == null) {
900 bm = bm.copy(Bitmap.Config.RGB_565, false);
902 return getDefaultArtwork(context);
906 bm = getDefaultArtwork(context);
914 } catch (IOException ex) {
922 // get album art for specified file
923 private static final String sExternalMediaUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString();
924 private static Bitmap getArtworkFromFile(Context context, long songid, long albumid) {
929 if (albumid < 0 && songid < 0) {
930 throw new IllegalArgumentException("Must specify an album or a song id");
935 Uri uri = Uri.parse("content://media/external/audio/media/" + songid + "/albumart");
936 ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
938 FileDescriptor fd = pfd.getFileDescriptor();
939 bm = BitmapFactory.decodeFileDescriptor(fd);
942 Uri uri = ContentUris.withAppendedId(sArtworkUri, albumid);
943 ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
945 FileDescriptor fd = pfd.getFileDescriptor();
946 bm = BitmapFactory.decodeFileDescriptor(fd);
949 } catch (FileNotFoundException ex) {
958 private static Bitmap getDefaultArtwork(Context context) {
959 BitmapFactory.Options opts = new BitmapFactory.Options();
960 opts.inPreferredConfig = Bitmap.Config.ARGB_8888;
961 return BitmapFactory.decodeStream(
962 context.getResources().openRawResource(R.drawable.albumart_mp_unknown), null, opts);
965 static int getIntPref(Context context, String name, int def) {
966 SharedPreferences prefs =
967 context.getSharedPreferences("com.android.music", Context.MODE_PRIVATE);
968 return prefs.getInt(name, def);
971 static void setIntPref(Context context, String name, int value) {
972 SharedPreferences prefs =
973 context.getSharedPreferences("com.android.music", Context.MODE_PRIVATE);
974 Editor ed = prefs.edit();
975 ed.putInt(name, value);
979 static void setRingtone(Context context, long id) {
980 ContentResolver resolver = context.getContentResolver();
981 // Set the flag in the database to mark this as a ringtone
982 Uri ringUri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
984 ContentValues values = new ContentValues(2);
985 values.put(MediaStore.Audio.Media.IS_RINGTONE, "1");
986 values.put(MediaStore.Audio.Media.IS_ALARM, "1");
987 resolver.update(ringUri, values, null, null);
988 } catch (UnsupportedOperationException ex) {
989 // most likely the card just got unmounted
990 Log.e(TAG, "couldn't set ringtone flag for id " + id);
994 String[] cols = new String[] {
995 MediaStore.Audio.Media._ID,
996 MediaStore.Audio.Media.DATA,
997 MediaStore.Audio.Media.TITLE
1000 String where = MediaStore.Audio.Media._ID + "=" + id;
1001 Cursor cursor = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1002 cols, where , null, null);
1004 if (cursor != null && cursor.getCount() == 1) {
1005 // Set the system setting to make this the current ringtone
1006 cursor.moveToFirst();
1007 Settings.System.putString(resolver, Settings.System.RINGTONE, ringUri.toString());
1008 String message = context.getString(R.string.ringtone_set, cursor.getString(2));
1009 Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
1012 if (cursor != null) {