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.media.MediaFile;
39 import android.net.Uri;
40 import android.os.Environment;
41 import android.os.ParcelFileDescriptor;
42 import android.os.RemoteException;
43 import android.provider.MediaStore;
44 import android.provider.Settings;
45 import android.text.TextUtils;
46 import android.util.Log;
47 import android.view.Menu;
48 import android.view.MenuItem;
49 import android.view.SubMenu;
50 import android.view.View;
51 import android.view.ViewGroup;
52 import android.view.Window;
53 import android.widget.LinearLayout;
54 import android.widget.TabWidget;
55 import android.widget.TextView;
56 import android.widget.Toast;
59 import java.io.FileDescriptor;
60 import java.io.FileNotFoundException;
61 import java.io.IOException;
62 import java.io.InputStream;
63 import java.util.Arrays;
64 import java.util.Formatter;
65 import java.util.HashMap;
66 import java.util.Locale;
68 public class MusicUtils {
70 private static final String TAG = "MusicUtils";
72 public interface Defs {
73 public final static int OPEN_URL = 0;
74 public final static int ADD_TO_PLAYLIST = 1;
75 public final static int USE_AS_RINGTONE = 2;
76 public final static int PLAYLIST_SELECTED = 3;
77 public final static int NEW_PLAYLIST = 4;
78 public final static int PLAY_SELECTION = 5;
79 public final static int GOTO_START = 6;
80 public final static int GOTO_PLAYBACK = 7;
81 public final static int PARTY_SHUFFLE = 8;
82 public final static int SHUFFLE_ALL = 9;
83 public final static int DELETE_ITEM = 10;
84 public final static int SCAN_DONE = 11;
85 public final static int QUEUE = 12;
86 public final static int CHILD_MENU_BASE = 13; // this should be the last item
89 public static String makeAlbumsLabel(Context context, int numalbums, int numsongs, boolean isUnknown) {
90 // There are two formats for the albums/songs information:
91 // "N Song(s)" - used for unknown artist/album
92 // "N Album(s)" - used for known albums
94 StringBuilder songs_albums = new StringBuilder();
96 Resources r = context.getResources();
99 songs_albums.append(context.getString(R.string.onesong));
101 String f = r.getQuantityText(R.plurals.Nsongs, numsongs).toString();
102 sFormatBuilder.setLength(0);
103 sFormatter.format(f, Integer.valueOf(numsongs));
104 songs_albums.append(sFormatBuilder);
107 String f = r.getQuantityText(R.plurals.Nalbums, numalbums).toString();
108 sFormatBuilder.setLength(0);
109 sFormatter.format(f, Integer.valueOf(numalbums));
110 songs_albums.append(sFormatBuilder);
111 songs_albums.append(context.getString(R.string.albumsongseparator));
113 return songs_albums.toString();
117 * This is now only used for the query screen
119 public static String makeAlbumsSongsLabel(Context context, int numalbums, int numsongs, boolean isUnknown) {
120 // There are several formats for the albums/songs information:
121 // "1 Song" - used if there is only 1 song
122 // "N Songs" - used for the "unknown artist" item
123 // "1 Album"/"N Songs"
124 // "N Album"/"M Songs"
125 // Depending on locale, these may need to be further subdivided
127 StringBuilder songs_albums = new StringBuilder();
130 songs_albums.append(context.getString(R.string.onesong));
132 Resources r = context.getResources();
134 String f = r.getQuantityText(R.plurals.Nalbums, numalbums).toString();
135 sFormatBuilder.setLength(0);
136 sFormatter.format(f, Integer.valueOf(numalbums));
137 songs_albums.append(sFormatBuilder);
138 songs_albums.append(context.getString(R.string.albumsongseparator));
140 String f = r.getQuantityText(R.plurals.Nsongs, numsongs).toString();
141 sFormatBuilder.setLength(0);
142 sFormatter.format(f, Integer.valueOf(numsongs));
143 songs_albums.append(sFormatBuilder);
145 return songs_albums.toString();
148 public static IMediaPlaybackService sService = null;
149 private static HashMap<Context, ServiceBinder> sConnectionMap = new HashMap<Context, ServiceBinder>();
151 public static boolean bindToService(Context context) {
152 return bindToService(context, null);
155 public static boolean bindToService(Context context, ServiceConnection callback) {
156 context.startService(new Intent(context, MediaPlaybackService.class));
157 ServiceBinder sb = new ServiceBinder(callback);
158 sConnectionMap.put(context, sb);
159 return context.bindService((new Intent()).setClass(context,
160 MediaPlaybackService.class), sb, 0);
163 public static void unbindFromService(Context context) {
164 ServiceBinder sb = (ServiceBinder) sConnectionMap.remove(context);
166 Log.e("MusicUtils", "Trying to unbind for unknown Context");
169 context.unbindService(sb);
170 if (sConnectionMap.isEmpty()) {
171 // presumably there is nobody interested in the service at this point,
172 // so don't hang on to the ServiceConnection
177 private static class ServiceBinder implements ServiceConnection {
178 ServiceConnection mCallback;
179 ServiceBinder(ServiceConnection callback) {
180 mCallback = callback;
183 public void onServiceConnected(ComponentName className, android.os.IBinder service) {
184 sService = IMediaPlaybackService.Stub.asInterface(service);
186 if (mCallback != null) {
187 mCallback.onServiceConnected(className, service);
191 public void onServiceDisconnected(ComponentName className) {
192 if (mCallback != null) {
193 mCallback.onServiceDisconnected(className);
199 public static long getCurrentAlbumId() {
200 if (sService != null) {
202 return sService.getAlbumId();
203 } catch (RemoteException ex) {
209 public static long getCurrentArtistId() {
210 if (MusicUtils.sService != null) {
212 return sService.getArtistId();
213 } catch (RemoteException ex) {
219 public static long getCurrentAudioId() {
220 if (MusicUtils.sService != null) {
222 return sService.getAudioId();
223 } catch (RemoteException ex) {
229 public static int getCurrentShuffleMode() {
230 int mode = MediaPlaybackService.SHUFFLE_NONE;
231 if (sService != null) {
233 mode = sService.getShuffleMode();
234 } catch (RemoteException ex) {
240 public static void togglePartyShuffle() {
241 if (sService != null) {
242 int shuffle = getCurrentShuffleMode();
244 if (shuffle == MediaPlaybackService.SHUFFLE_AUTO) {
245 sService.setShuffleMode(MediaPlaybackService.SHUFFLE_NONE);
247 sService.setShuffleMode(MediaPlaybackService.SHUFFLE_AUTO);
249 } catch (RemoteException ex) {
254 public static void setPartyShuffleMenuIcon(Menu menu) {
255 MenuItem item = menu.findItem(Defs.PARTY_SHUFFLE);
257 int shuffle = MusicUtils.getCurrentShuffleMode();
258 if (shuffle == MediaPlaybackService.SHUFFLE_AUTO) {
259 item.setIcon(R.drawable.ic_menu_party_shuffle);
260 item.setTitle(R.string.party_shuffle_off);
262 item.setIcon(R.drawable.ic_menu_party_shuffle);
263 item.setTitle(R.string.party_shuffle);
269 * Returns true if a file is currently opened for playback (regardless
270 * of whether it's playing or paused).
272 public static boolean isMusicLoaded() {
273 if (MusicUtils.sService != null) {
275 return sService.getPath() != null;
276 } catch (RemoteException ex) {
282 private final static long [] sEmptyList = new long[0];
284 public static long [] getSongListForCursor(Cursor cursor) {
285 if (cursor == null) {
288 int len = cursor.getCount();
289 long [] list = new long[len];
290 cursor.moveToFirst();
293 colidx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.Members.AUDIO_ID);
294 } catch (IllegalArgumentException ex) {
295 colidx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID);
297 for (int i = 0; i < len; i++) {
298 list[i] = cursor.getLong(colidx);
304 public static long [] getSongListForArtist(Context context, long id) {
305 final String[] ccols = new String[] { MediaStore.Audio.Media._ID };
306 String where = MediaStore.Audio.Media.ARTIST_ID + "=" + id + " AND " +
307 MediaStore.Audio.Media.IS_MUSIC + "=1";
308 Cursor cursor = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
310 MediaStore.Audio.Media.ALBUM_KEY + "," + MediaStore.Audio.Media.TRACK);
312 if (cursor != null) {
313 long [] list = getSongListForCursor(cursor);
320 public static long [] getSongListForAlbum(Context context, long id) {
321 final String[] ccols = new String[] { MediaStore.Audio.Media._ID };
322 String where = MediaStore.Audio.Media.ALBUM_ID + "=" + id + " AND " +
323 MediaStore.Audio.Media.IS_MUSIC + "=1";
324 Cursor cursor = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
325 ccols, where, null, MediaStore.Audio.Media.TRACK);
327 if (cursor != null) {
328 long [] list = getSongListForCursor(cursor);
335 public static long [] getSongListForPlaylist(Context context, long plid) {
336 final String[] ccols = new String[] { MediaStore.Audio.Playlists.Members.AUDIO_ID };
337 Cursor cursor = query(context, MediaStore.Audio.Playlists.Members.getContentUri("external", plid),
338 ccols, null, null, MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER);
340 if (cursor != null) {
341 long [] list = getSongListForCursor(cursor);
348 public static void playPlaylist(Context context, long plid) {
349 long [] list = getSongListForPlaylist(context, plid);
351 playAll(context, list, -1, false);
355 public static long [] getAllSongs(Context context) {
356 Cursor c = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
357 new String[] {MediaStore.Audio.Media._ID}, MediaStore.Audio.Media.IS_MUSIC + "=1",
360 if (c == null || c.getCount() == 0) {
363 int len = c.getCount();
364 long [] list = new long[len];
365 for (int i = 0; i < len; i++) {
367 list[i] = c.getLong(0);
379 * Fills out the given submenu with items for "new playlist" and
380 * any existing playlists. When the user selects an item, the
381 * application will receive PLAYLIST_SELECTED with the Uri of
382 * the selected playlist, NEW_PLAYLIST if a new playlist
383 * should be created, and QUEUE if the "current playlist" was
385 * @param context The context to use for creating the menu items
386 * @param sub The submenu to add the items to.
388 public static void makePlaylistMenu(Context context, SubMenu sub) {
389 String[] cols = new String[] {
390 MediaStore.Audio.Playlists._ID,
391 MediaStore.Audio.Playlists.NAME
393 ContentResolver resolver = context.getContentResolver();
394 if (resolver == null) {
395 System.out.println("resolver = null");
397 String whereclause = MediaStore.Audio.Playlists.NAME + " != ''";
398 Cursor cur = resolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
399 cols, whereclause, null,
400 MediaStore.Audio.Playlists.NAME);
402 sub.add(1, Defs.QUEUE, 0, R.string.queue);
403 sub.add(1, Defs.NEW_PLAYLIST, 0, R.string.new_playlist);
404 if (cur != null && cur.getCount() > 0) {
405 //sub.addSeparator(1, 0);
407 while (! cur.isAfterLast()) {
408 Intent intent = new Intent();
409 intent.putExtra("playlist", cur.getLong(0));
410 // if (cur.getInt(0) == mLastPlaylistSelected) {
411 // sub.add(0, MusicBaseActivity.PLAYLIST_SELECTED, cur.getString(1)).setIntent(intent);
413 sub.add(1, Defs.PLAYLIST_SELECTED, 0, cur.getString(1)).setIntent(intent);
424 public static void clearPlaylist(Context context, int plid) {
426 Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", plid);
427 context.getContentResolver().delete(uri, null, null);
431 public static void deleteTracks(Context context, long [] list) {
433 String [] cols = new String [] { MediaStore.Audio.Media._ID,
434 MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.ALBUM_ID };
435 StringBuilder where = new StringBuilder();
436 where.append(MediaStore.Audio.Media._ID + " IN (");
437 for (int i = 0; i < list.length; i++) {
438 where.append(list[i]);
439 if (i < list.length - 1) {
444 Cursor c = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, cols,
445 where.toString(), null, null);
449 // step 1: remove selected tracks from the current playlist, as well
450 // as from the album art cache
453 while (! c.isAfterLast()) {
454 // remove from current playlist
455 long id = c.getLong(0);
456 sService.removeTrack(id);
457 // remove from album art cache
458 long artIndex = c.getLong(2);
459 synchronized(sArtCache) {
460 sArtCache.remove(artIndex);
464 } catch (RemoteException ex) {
467 // step 2: remove selected tracks from the database
468 context.getContentResolver().delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, where.toString(), null);
470 // step 3: remove files from card
472 while (! c.isAfterLast()) {
473 String name = c.getString(1);
474 File f = new File(name);
475 try { // File.delete can throw a security exception
477 // I'm not sure if we'd ever get here (deletion would
478 // have to fail, but no exception thrown)
479 Log.e("MusicUtils", "Failed to delete file " + name);
482 } catch (SecurityException ex) {
489 String message = context.getResources().getQuantityString(
490 R.plurals.NNNtracksdeleted, list.length, Integer.valueOf(list.length));
492 Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
493 // We deleted a number of tracks, which could affect any number of things
494 // in the media content domain, so update everything.
495 context.getContentResolver().notifyChange(Uri.parse("content://media"), null);
498 public static void addToCurrentPlaylist(Context context, long [] list) {
499 if (sService == null) {
503 sService.enqueue(list, MediaPlaybackService.LAST);
504 String message = context.getResources().getQuantityString(
505 R.plurals.NNNtrackstoplaylist, list.length, Integer.valueOf(list.length));
506 Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
507 } catch (RemoteException ex) {
511 public static void addToPlaylist(Context context, long [] ids, long playlistid) {
513 // this shouldn't happen (the menuitems shouldn't be visible
514 // unless the selected item represents something playable
515 Log.e("MusicBase", "ListSelection null");
517 int size = ids.length;
518 ContentValues values [] = new ContentValues[size];
519 ContentResolver resolver = context.getContentResolver();
520 // need to determine the number of items currently in the playlist,
521 // so the play_order field can be maintained.
522 String[] cols = new String[] {
525 Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistid);
526 Cursor cur = resolver.query(uri, cols, null, null, null);
528 int base = cur.getInt(0);
531 for (int i = 0; i < size; i++) {
532 values[i] = new ContentValues();
533 values[i].put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(base + i));
534 values[i].put(MediaStore.Audio.Playlists.Members.AUDIO_ID, ids[i]);
536 resolver.bulkInsert(uri, values);
537 String message = context.getResources().getQuantityString(
538 R.plurals.NNNtrackstoplaylist, size, Integer.valueOf(size));
539 Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
540 //mLastPlaylistSelected = playlistid;
544 public static Cursor query(Context context, Uri uri, String[] projection,
545 String selection, String[] selectionArgs, String sortOrder, int limit) {
547 ContentResolver resolver = context.getContentResolver();
548 if (resolver == null) {
552 uri = uri.buildUpon().appendQueryParameter("limit", "" + limit).build();
554 return resolver.query(uri, projection, selection, selectionArgs, sortOrder);
555 } catch (UnsupportedOperationException ex) {
560 public static Cursor query(Context context, Uri uri, String[] projection,
561 String selection, String[] selectionArgs, String sortOrder) {
562 return query(context, uri, projection, selection, selectionArgs, sortOrder, 0);
565 public static boolean isMediaScannerScanning(Context context) {
566 boolean result = false;
567 Cursor cursor = query(context, MediaStore.getMediaScannerUri(),
568 new String [] { MediaStore.MEDIA_SCANNER_VOLUME }, null, null, null);
569 if (cursor != null) {
570 if (cursor.getCount() == 1) {
571 cursor.moveToFirst();
572 result = "external".equals(cursor.getString(0));
580 public static void setSpinnerState(Activity a) {
581 if (isMediaScannerScanning(a)) {
582 // start the progress spinner
583 a.getWindow().setFeatureInt(
584 Window.FEATURE_INDETERMINATE_PROGRESS,
585 Window.PROGRESS_INDETERMINATE_ON);
587 a.getWindow().setFeatureInt(
588 Window.FEATURE_INDETERMINATE_PROGRESS,
589 Window.PROGRESS_VISIBILITY_ON);
591 // stop the progress spinner
592 a.getWindow().setFeatureInt(
593 Window.FEATURE_INDETERMINATE_PROGRESS,
594 Window.PROGRESS_VISIBILITY_OFF);
598 private static String mLastSdStatus;
600 public static void displayDatabaseError(Activity a) {
601 String status = Environment.getExternalStorageState();
602 int title = R.string.sdcard_error_title;
603 int message = R.string.sdcard_error_message;
605 if (status.equals(Environment.MEDIA_SHARED) ||
606 status.equals(Environment.MEDIA_UNMOUNTED)) {
607 title = R.string.sdcard_busy_title;
608 message = R.string.sdcard_busy_message;
609 } else if (status.equals(Environment.MEDIA_REMOVED)) {
610 title = R.string.sdcard_missing_title;
611 message = R.string.sdcard_missing_message;
612 } else if (status.equals(Environment.MEDIA_MOUNTED)){
613 // The card is mounted, but we didn't get a valid cursor.
614 // This probably means the mediascanner hasn't started scanning the
615 // card yet (there is a small window of time during boot where this
618 Intent intent = new Intent();
619 intent.setClass(a, ScanningProgress.class);
620 a.startActivityForResult(intent, Defs.SCAN_DONE);
621 } else if (!TextUtils.equals(mLastSdStatus, status)) {
622 mLastSdStatus = status;
623 Log.d(TAG, "sd card: " + status);
627 View v = a.findViewById(R.id.sd_message);
629 v.setVisibility(View.VISIBLE);
631 v = a.findViewById(R.id.sd_icon);
633 v.setVisibility(View.VISIBLE);
635 v = a.findViewById(android.R.id.list);
637 v.setVisibility(View.GONE);
639 TextView tv = (TextView) a.findViewById(R.id.sd_message);
643 public static void hideDatabaseError(Activity a) {
644 View v = a.findViewById(R.id.sd_message);
646 v.setVisibility(View.GONE);
648 v = a.findViewById(R.id.sd_icon);
650 v.setVisibility(View.GONE);
652 v = a.findViewById(android.R.id.list);
654 v.setVisibility(View.VISIBLE);
658 static protected Uri getContentURIForPath(String path) {
659 return Uri.fromFile(new File(path));
663 /* Try to use String.format() as little as possible, because it creates a
664 * new Formatter every time you call it, which is very inefficient.
665 * Reusing an existing Formatter more than tripled the speed of
667 * This Formatter/StringBuilder are also used by makeAlbumSongsLabel()
669 private static StringBuilder sFormatBuilder = new StringBuilder();
670 private static Formatter sFormatter = new Formatter(sFormatBuilder, Locale.getDefault());
671 private static final Object[] sTimeArgs = new Object[5];
673 public static String makeTimeString(Context context, long secs) {
674 String durationformat = context.getString(
675 secs < 3600 ? R.string.durationformatshort : R.string.durationformatlong);
677 /* Provide multiple arguments so the format can be changed easily
678 * by modifying the xml.
680 sFormatBuilder.setLength(0);
682 final Object[] timeArgs = sTimeArgs;
683 timeArgs[0] = secs / 3600;
684 timeArgs[1] = secs / 60;
685 timeArgs[2] = (secs / 60) % 60;
687 timeArgs[4] = secs % 60;
689 return sFormatter.format(durationformat, timeArgs).toString();
692 public static void shuffleAll(Context context, Cursor cursor) {
693 playAll(context, cursor, 0, true);
696 public static void playAll(Context context, Cursor cursor) {
697 playAll(context, cursor, 0, false);
700 public static void playAll(Context context, Cursor cursor, int position) {
701 playAll(context, cursor, position, false);
704 public static void playAll(Context context, long [] list, int position) {
705 playAll(context, list, position, false);
708 private static void playAll(Context context, Cursor cursor, int position, boolean force_shuffle) {
710 long [] list = getSongListForCursor(cursor);
711 playAll(context, list, position, force_shuffle);
714 private static void playAll(Context context, long [] list, int position, boolean force_shuffle) {
715 if (list.length == 0 || sService == null) {
716 Log.d("MusicUtils", "attempt to play empty song list");
717 // Don't try to play empty playlists. Nothing good will come of it.
718 String message = context.getString(R.string.emptyplaylist, list.length);
719 Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
724 sService.setShuffleMode(MediaPlaybackService.SHUFFLE_NORMAL);
726 long curid = sService.getAudioId();
727 int curpos = sService.getQueuePosition();
728 if (position != -1 && curpos == position && curid == list[position]) {
729 // The selected file is the file that's currently playing;
730 // figure out if we need to restart with a new playlist,
731 // or just launch the playback activity.
732 long [] playlist = sService.getQueue();
733 if (Arrays.equals(list, playlist)) {
734 // we don't need to set a new list, but we should resume playback if needed
736 return; // the 'finally' block will still run
742 sService.open(list, force_shuffle ? -1 : position);
744 } catch (RemoteException ex) {
746 Intent intent = new Intent(context, MediaPlaybackActivity.class)
747 .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
748 context.startActivity(intent);
752 public static void clearQueue() {
754 sService.removeTracks(0, Integer.MAX_VALUE);
755 } catch (RemoteException ex) {
759 // A really simple BitmapDrawable-like class, that doesn't do
760 // scaling, dithering or filtering.
761 private static class FastBitmapDrawable extends Drawable {
762 private Bitmap mBitmap;
763 public FastBitmapDrawable(Bitmap b) {
767 public void draw(Canvas canvas) {
768 canvas.drawBitmap(mBitmap, 0, 0, null);
771 public int getOpacity() {
772 return PixelFormat.OPAQUE;
775 public void setAlpha(int alpha) {
778 public void setColorFilter(ColorFilter cf) {
782 private static int sArtId = -2;
783 private static Bitmap mCachedBit = null;
784 private static final BitmapFactory.Options sBitmapOptionsCache = new BitmapFactory.Options();
785 private static final BitmapFactory.Options sBitmapOptions = new BitmapFactory.Options();
786 private static final Uri sArtworkUri = Uri.parse("content://media/external/audio/albumart");
787 private static final HashMap<Long, Drawable> sArtCache = new HashMap<Long, Drawable>();
788 private static int sArtCacheId = -1;
792 // 565 is faster to decode and display
793 // and we don't want to dither here because the image will be scaled down later
794 sBitmapOptionsCache.inPreferredConfig = Bitmap.Config.RGB_565;
795 sBitmapOptionsCache.inDither = false;
797 sBitmapOptions.inPreferredConfig = Bitmap.Config.RGB_565;
798 sBitmapOptions.inDither = false;
801 public static void initAlbumArtCache() {
803 int id = sService.getMediaMountedCount();
804 if (id != sArtCacheId) {
805 clearAlbumArtCache();
808 } catch (RemoteException e) {
813 public static void clearAlbumArtCache() {
814 synchronized(sArtCache) {
819 public static Drawable getCachedArtwork(Context context, long artIndex, BitmapDrawable defaultArtwork) {
821 synchronized(sArtCache) {
822 d = sArtCache.get(artIndex);
826 final Bitmap icon = defaultArtwork.getBitmap();
827 int w = icon.getWidth();
828 int h = icon.getHeight();
829 Bitmap b = MusicUtils.getArtworkQuick(context, artIndex, w, h);
831 d = new FastBitmapDrawable(b);
832 synchronized(sArtCache) {
833 // the cache may have changed since we checked
834 Drawable value = sArtCache.get(artIndex);
836 sArtCache.put(artIndex, d);
846 // Get album art for specified album. This method will not try to
847 // fall back to getting artwork directly from the file, nor will
848 // it attempt to repair the database.
849 private static Bitmap getArtworkQuick(Context context, long album_id, int w, int h) {
850 // NOTE: There is in fact a 1 pixel border on the right side in the ImageView
851 // used to display this drawable. Take it into account now, so we don't have to
854 ContentResolver res = context.getContentResolver();
855 Uri uri = ContentUris.withAppendedId(sArtworkUri, album_id);
857 ParcelFileDescriptor fd = null;
859 fd = res.openFileDescriptor(uri, "r");
862 // Compute the closest power-of-two scale factor
863 // and pass that to sBitmapOptionsCache.inSampleSize, which will
864 // result in faster decoding and better quality
865 sBitmapOptionsCache.inJustDecodeBounds = true;
866 BitmapFactory.decodeFileDescriptor(
867 fd.getFileDescriptor(), null, sBitmapOptionsCache);
868 int nextWidth = sBitmapOptionsCache.outWidth >> 1;
869 int nextHeight = sBitmapOptionsCache.outHeight >> 1;
870 while (nextWidth>w && nextHeight>h) {
876 sBitmapOptionsCache.inSampleSize = sampleSize;
877 sBitmapOptionsCache.inJustDecodeBounds = false;
878 Bitmap b = BitmapFactory.decodeFileDescriptor(
879 fd.getFileDescriptor(), null, sBitmapOptionsCache);
882 // finally rescale to exactly the size we need
883 if (sBitmapOptionsCache.outWidth != w || sBitmapOptionsCache.outHeight != h) {
884 Bitmap tmp = Bitmap.createScaledBitmap(b, w, h, true);
885 // Bitmap.createScaledBitmap() can return the same bitmap
886 if (tmp != b) b.recycle();
892 } catch (FileNotFoundException e) {
897 } catch (IOException e) {
904 /** Get album art for specified album. You should not pass in the album id
905 * for the "unknown" album here (use -1 instead)
907 public static Bitmap getArtwork(Context context, long song_id, long album_id) {
910 // This is something that is not in the database, so get the album art directly
913 Bitmap bm = getArtworkFromFile(context, song_id, -1);
918 return getDefaultArtwork(context);
921 ContentResolver res = context.getContentResolver();
922 Uri uri = ContentUris.withAppendedId(sArtworkUri, album_id);
924 InputStream in = null;
926 in = res.openInputStream(uri);
927 return BitmapFactory.decodeStream(in, null, sBitmapOptions);
928 } catch (FileNotFoundException ex) {
929 // The album art thumbnail does not actually exist. Maybe the user deleted it, or
930 // maybe it never existed to begin with.
931 Bitmap bm = getArtworkFromFile(context, song_id, album_id);
933 if (bm.getConfig() == null) {
934 bm = bm.copy(Bitmap.Config.RGB_565, false);
936 return getDefaultArtwork(context);
940 bm = getDefaultArtwork(context);
948 } catch (IOException ex) {
956 // get album art for specified file
957 private static final String sExternalMediaUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString();
958 private static Bitmap getArtworkFromFile(Context context, long songid, long albumid) {
963 if (albumid < 0 && songid < 0) {
964 throw new IllegalArgumentException("Must specify an album or a song id");
969 Uri uri = Uri.parse("content://media/external/audio/media/" + songid + "/albumart");
970 ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
972 FileDescriptor fd = pfd.getFileDescriptor();
973 bm = BitmapFactory.decodeFileDescriptor(fd);
976 Uri uri = ContentUris.withAppendedId(sArtworkUri, albumid);
977 ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
979 FileDescriptor fd = pfd.getFileDescriptor();
980 bm = BitmapFactory.decodeFileDescriptor(fd);
983 } catch (FileNotFoundException ex) {
992 private static Bitmap getDefaultArtwork(Context context) {
993 BitmapFactory.Options opts = new BitmapFactory.Options();
994 opts.inPreferredConfig = Bitmap.Config.ARGB_8888;
995 return BitmapFactory.decodeStream(
996 context.getResources().openRawResource(R.drawable.albumart_mp_unknown), null, opts);
999 static int getIntPref(Context context, String name, int def) {
1000 SharedPreferences prefs =
1001 context.getSharedPreferences("com.android.music", Context.MODE_PRIVATE);
1002 return prefs.getInt(name, def);
1005 static void setIntPref(Context context, String name, int value) {
1006 SharedPreferences prefs =
1007 context.getSharedPreferences("com.android.music", Context.MODE_PRIVATE);
1008 Editor ed = prefs.edit();
1009 ed.putInt(name, value);
1013 static void setRingtone(Context context, long id) {
1014 ContentResolver resolver = context.getContentResolver();
1015 // Set the flag in the database to mark this as a ringtone
1016 Uri ringUri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
1018 ContentValues values = new ContentValues(2);
1019 values.put(MediaStore.Audio.Media.IS_RINGTONE, "1");
1020 values.put(MediaStore.Audio.Media.IS_ALARM, "1");
1021 resolver.update(ringUri, values, null, null);
1022 } catch (UnsupportedOperationException ex) {
1023 // most likely the card just got unmounted
1024 Log.e(TAG, "couldn't set ringtone flag for id " + id);
1028 String[] cols = new String[] {
1029 MediaStore.Audio.Media._ID,
1030 MediaStore.Audio.Media.DATA,
1031 MediaStore.Audio.Media.TITLE
1034 String where = MediaStore.Audio.Media._ID + "=" + id;
1035 Cursor cursor = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1036 cols, where , null, null);
1038 if (cursor != null && cursor.getCount() == 1) {
1039 // Set the system setting to make this the current ringtone
1040 cursor.moveToFirst();
1041 Settings.System.putString(resolver, Settings.System.RINGTONE, ringUri.toString());
1042 String message = context.getString(R.string.ringtone_set, cursor.getString(2));
1043 Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
1046 if (cursor != null) {
1052 static int sActiveTabIndex = -1;
1054 static void updateButtonBar(Activity a, int highlight) {
1055 final TabWidget ll = (TabWidget) a.findViewById(R.id.buttonbar);
1056 boolean withtabs = false;
1057 Intent intent = a.getIntent();
1058 if (intent != null) {
1059 withtabs = intent.getBooleanExtra("withtabs", false);
1062 if (highlight == 0 || !withtabs) {
1063 ll.setVisibility(View.GONE);
1065 } else if (withtabs) {
1066 ll.setVisibility(View.VISIBLE);
1068 for (int i = ll.getChildCount() - 1; i >= 0; i--) {
1070 View v = ll.getChildAt(i);
1071 boolean isActive = (v.getId() == highlight);
1073 ll.setCurrentTab(i);
1074 sActiveTabIndex = i;
1076 v.setOnFocusChangeListener(new View.OnFocusChangeListener() {
1079 public void onFocusChange(View v, boolean hasFocus) {
1081 for (int i = 0; i < ll.getTabCount(); i++) {
1082 if (ll.getChildTabViewAt(i) == v) {
1083 ll.setCurrentTab(i);
1084 processTabClick((Activity)ll.getContext(), v, ll.getChildAt(sActiveTabIndex).getId());
1091 v.setOnClickListener(new View.OnClickListener() {
1094 public void onClick(View v) {
1095 processTabClick((Activity)ll.getContext(), v, ll.getChildAt(sActiveTabIndex).getId());
1101 static void processTabClick(Activity a, View v, int current) {
1103 if (id == current) {
1107 if (id != R.id.nowplayingtab) {
1108 setIntPref(a, "activetab", id);
1112 static void activateTab(Activity a, int id) {
1113 Intent intent = new Intent(Intent.ACTION_PICK);
1115 case R.id.artisttab:
1116 intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/artistalbum");
1119 intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/album");
1122 intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track");
1124 case R.id.playlisttab:
1125 intent.setDataAndType(Uri.EMPTY, MediaStore.Audio.Playlists.CONTENT_TYPE);
1127 case R.id.nowplayingtab:
1128 intent = new Intent(a, MediaPlaybackActivity.class);
1129 a.startActivity(intent);
1130 // fall through and return
1134 intent.putExtra("withtabs", true);
1135 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
1136 a.startActivity(intent);
1138 a.overridePendingTransition(0, 0);
1141 static void updateNowPlaying(Activity a) {
1142 View nowPlayingView = a.findViewById(R.id.nowplaying);
1143 if (nowPlayingView == null) {
1147 boolean withtabs = false;
1148 Intent intent = a.getIntent();
1149 if (intent != null) {
1150 withtabs = intent.getBooleanExtra("withtabs", false);
1152 if (true && MusicUtils.sService != null && MusicUtils.sService.getAudioId() != -1) {
1153 TextView title = (TextView) nowPlayingView.findViewById(R.id.title);
1154 TextView artist = (TextView) nowPlayingView.findViewById(R.id.artist);
1155 title.setText(MusicUtils.sService.getTrackName());
1156 String artistName = MusicUtils.sService.getArtistName();
1157 if (MediaFile.UNKNOWN_STRING.equals(artistName)) {
1158 artistName = a.getString(R.string.unknown_artist_name);
1160 artist.setText(artistName);
1161 //mNowPlayingView.setOnFocusChangeListener(mFocuser);
1162 //mNowPlayingView.setOnClickListener(this);
1163 nowPlayingView.setVisibility(View.VISIBLE);
1164 nowPlayingView.setOnClickListener(new View.OnClickListener() {
1167 public void onClick(View v) {
1168 Context c = v.getContext();
1169 c.startActivity(new Intent(c, MediaPlaybackActivity.class));
1173 } catch (RemoteException ex) {
1175 nowPlayingView.setVisibility(View.GONE);