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.ContextWrapper;
26 import android.content.Intent;
27 import android.content.ServiceConnection;
28 import android.content.SharedPreferences;
29 import android.content.SharedPreferences.Editor;
30 import android.content.res.Resources;
31 import android.database.Cursor;
32 import android.graphics.Bitmap;
33 import android.graphics.BitmapFactory;
34 import android.graphics.Canvas;
35 import android.graphics.ColorFilter;
36 import android.graphics.ColorMatrix;
37 import android.graphics.ColorMatrixColorFilter;
38 import android.graphics.Matrix;
39 import android.graphics.Paint;
40 import android.graphics.PixelFormat;
41 import android.graphics.drawable.BitmapDrawable;
42 import android.graphics.drawable.Drawable;
43 import android.net.Uri;
44 import android.os.Environment;
45 import android.os.ParcelFileDescriptor;
46 import android.os.RemoteException;
47 import android.provider.MediaStore;
48 import android.provider.Settings;
49 import android.text.TextUtils;
50 import android.util.Log;
51 import android.view.Menu;
52 import android.view.MenuItem;
53 import android.view.SubMenu;
54 import android.view.View;
55 import android.view.ViewGroup;
56 import android.view.Window;
57 import android.view.MenuItem.OnMenuItemClickListener;
58 import android.widget.TabWidget;
59 import android.widget.TextView;
60 import android.widget.Toast;
63 import java.io.FileDescriptor;
64 import java.io.FileNotFoundException;
65 import java.io.IOException;
66 import java.io.InputStream;
67 import java.util.Arrays;
68 import java.util.Formatter;
69 import java.util.HashMap;
70 import java.util.Locale;
72 public class MusicUtils {
74 private static final String TAG = "MusicUtils";
76 public interface Defs {
77 public final static int OPEN_URL = 0;
78 public final static int ADD_TO_PLAYLIST = 1;
79 public final static int USE_AS_RINGTONE = 2;
80 public final static int PLAYLIST_SELECTED = 3;
81 public final static int NEW_PLAYLIST = 4;
82 public final static int PLAY_SELECTION = 5;
83 public final static int GOTO_START = 6;
84 public final static int GOTO_PLAYBACK = 7;
85 public final static int PARTY_SHUFFLE = 8;
86 public final static int SHUFFLE_ALL = 9;
87 public final static int DELETE_ITEM = 10;
88 public final static int SCAN_DONE = 11;
89 public final static int QUEUE = 12;
90 public final static int CHILD_MENU_BASE = 13; // this should be the last item
93 public static String makeAlbumsLabel(Context context, int numalbums, int numsongs, boolean isUnknown) {
94 // There are two formats for the albums/songs information:
95 // "N Song(s)" - used for unknown artist/album
96 // "N Album(s)" - used for known albums
98 StringBuilder songs_albums = new StringBuilder();
100 Resources r = context.getResources();
103 songs_albums.append(context.getString(R.string.onesong));
105 String f = r.getQuantityText(R.plurals.Nsongs, numsongs).toString();
106 sFormatBuilder.setLength(0);
107 sFormatter.format(f, Integer.valueOf(numsongs));
108 songs_albums.append(sFormatBuilder);
111 String f = r.getQuantityText(R.plurals.Nalbums, numalbums).toString();
112 sFormatBuilder.setLength(0);
113 sFormatter.format(f, Integer.valueOf(numalbums));
114 songs_albums.append(sFormatBuilder);
115 songs_albums.append(context.getString(R.string.albumsongseparator));
117 return songs_albums.toString();
121 * This is now only used for the query screen
123 public static String makeAlbumsSongsLabel(Context context, int numalbums, int numsongs, boolean isUnknown) {
124 // There are several formats for the albums/songs information:
125 // "1 Song" - used if there is only 1 song
126 // "N Songs" - used for the "unknown artist" item
127 // "1 Album"/"N Songs"
128 // "N Album"/"M Songs"
129 // Depending on locale, these may need to be further subdivided
131 StringBuilder songs_albums = new StringBuilder();
134 songs_albums.append(context.getString(R.string.onesong));
136 Resources r = context.getResources();
138 String f = r.getQuantityText(R.plurals.Nalbums, numalbums).toString();
139 sFormatBuilder.setLength(0);
140 sFormatter.format(f, Integer.valueOf(numalbums));
141 songs_albums.append(sFormatBuilder);
142 songs_albums.append(context.getString(R.string.albumsongseparator));
144 String f = r.getQuantityText(R.plurals.Nsongs, numsongs).toString();
145 sFormatBuilder.setLength(0);
146 sFormatter.format(f, Integer.valueOf(numsongs));
147 songs_albums.append(sFormatBuilder);
149 return songs_albums.toString();
152 public static IMediaPlaybackService sService = null;
153 private static HashMap<Context, ServiceBinder> sConnectionMap = new HashMap<Context, ServiceBinder>();
155 public static class ServiceToken {
156 ContextWrapper mWrappedContext;
157 ServiceToken(ContextWrapper context) {
158 mWrappedContext = context;
162 public static ServiceToken bindToService(Activity context) {
163 return bindToService(context, null);
166 public static ServiceToken bindToService(Activity context, ServiceConnection callback) {
167 Activity realActivity = context.getParent();
168 if (realActivity == null) {
169 realActivity = context;
171 ContextWrapper cw = new ContextWrapper(realActivity);
172 cw.startService(new Intent(cw, MediaPlaybackService.class));
173 ServiceBinder sb = new ServiceBinder(callback);
174 if (cw.bindService((new Intent()).setClass(cw, MediaPlaybackService.class), sb, 0)) {
175 sConnectionMap.put(cw, sb);
176 return new ServiceToken(cw);
178 Log.e("Music", "Failed to bind to service");
182 public static void unbindFromService(ServiceToken token) {
184 Log.e("MusicUtils", "Trying to unbind with null token");
187 ContextWrapper cw = token.mWrappedContext;
188 ServiceBinder sb = sConnectionMap.remove(cw);
190 Log.e("MusicUtils", "Trying to unbind for unknown Context");
193 cw.unbindService(sb);
194 if (sConnectionMap.isEmpty()) {
195 // presumably there is nobody interested in the service at this point,
196 // so don't hang on to the ServiceConnection
201 private static class ServiceBinder implements ServiceConnection {
202 ServiceConnection mCallback;
203 ServiceBinder(ServiceConnection callback) {
204 mCallback = callback;
207 public void onServiceConnected(ComponentName className, android.os.IBinder service) {
208 sService = IMediaPlaybackService.Stub.asInterface(service);
210 if (mCallback != null) {
211 mCallback.onServiceConnected(className, service);
215 public void onServiceDisconnected(ComponentName className) {
216 if (mCallback != null) {
217 mCallback.onServiceDisconnected(className);
223 public static long getCurrentAlbumId() {
224 if (sService != null) {
226 return sService.getAlbumId();
227 } catch (RemoteException ex) {
233 public static long getCurrentArtistId() {
234 if (MusicUtils.sService != null) {
236 return sService.getArtistId();
237 } catch (RemoteException ex) {
243 public static long getCurrentAudioId() {
244 if (MusicUtils.sService != null) {
246 return sService.getAudioId();
247 } catch (RemoteException ex) {
253 public static int getCurrentShuffleMode() {
254 int mode = MediaPlaybackService.SHUFFLE_NONE;
255 if (sService != null) {
257 mode = sService.getShuffleMode();
258 } catch (RemoteException ex) {
264 public static void togglePartyShuffle() {
265 if (sService != null) {
266 int shuffle = getCurrentShuffleMode();
268 if (shuffle == MediaPlaybackService.SHUFFLE_AUTO) {
269 sService.setShuffleMode(MediaPlaybackService.SHUFFLE_NONE);
271 sService.setShuffleMode(MediaPlaybackService.SHUFFLE_AUTO);
273 } catch (RemoteException ex) {
278 public static void setPartyShuffleMenuIcon(Menu menu) {
279 MenuItem item = menu.findItem(Defs.PARTY_SHUFFLE);
281 int shuffle = MusicUtils.getCurrentShuffleMode();
282 if (shuffle == MediaPlaybackService.SHUFFLE_AUTO) {
283 item.setIcon(R.drawable.ic_menu_party_shuffle);
284 item.setTitle(R.string.party_shuffle_off);
286 item.setIcon(R.drawable.ic_menu_party_shuffle);
287 item.setTitle(R.string.party_shuffle);
293 * Returns true if a file is currently opened for playback (regardless
294 * of whether it's playing or paused).
296 public static boolean isMusicLoaded() {
297 if (MusicUtils.sService != null) {
299 return sService.getPath() != null;
300 } catch (RemoteException ex) {
306 private final static long [] sEmptyList = new long[0];
308 public static long [] getSongListForCursor(Cursor cursor) {
309 if (cursor == null) {
312 int len = cursor.getCount();
313 long [] list = new long[len];
314 cursor.moveToFirst();
317 colidx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.Members.AUDIO_ID);
318 } catch (IllegalArgumentException ex) {
319 colidx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID);
321 for (int i = 0; i < len; i++) {
322 list[i] = cursor.getLong(colidx);
328 public static long [] getSongListForArtist(Context context, long id) {
329 final String[] ccols = new String[] { MediaStore.Audio.Media._ID };
330 String where = MediaStore.Audio.Media.ARTIST_ID + "=" + id + " AND " +
331 MediaStore.Audio.Media.IS_MUSIC + "=1";
332 Cursor cursor = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
334 MediaStore.Audio.Media.ALBUM_KEY + "," + MediaStore.Audio.Media.TRACK);
336 if (cursor != null) {
337 long [] list = getSongListForCursor(cursor);
344 public static long [] getSongListForAlbum(Context context, long id) {
345 final String[] ccols = new String[] { MediaStore.Audio.Media._ID };
346 String where = MediaStore.Audio.Media.ALBUM_ID + "=" + id + " AND " +
347 MediaStore.Audio.Media.IS_MUSIC + "=1";
348 Cursor cursor = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
349 ccols, where, null, MediaStore.Audio.Media.TRACK);
351 if (cursor != null) {
352 long [] list = getSongListForCursor(cursor);
359 public static long [] getSongListForPlaylist(Context context, long plid) {
360 final String[] ccols = new String[] { MediaStore.Audio.Playlists.Members.AUDIO_ID };
361 Cursor cursor = query(context, MediaStore.Audio.Playlists.Members.getContentUri("external", plid),
362 ccols, null, null, MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER);
364 if (cursor != null) {
365 long [] list = getSongListForCursor(cursor);
372 public static void playPlaylist(Context context, long plid) {
373 long [] list = getSongListForPlaylist(context, plid);
375 playAll(context, list, -1, false);
379 public static long [] getAllSongs(Context context) {
380 Cursor c = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
381 new String[] {MediaStore.Audio.Media._ID}, MediaStore.Audio.Media.IS_MUSIC + "=1",
384 if (c == null || c.getCount() == 0) {
387 int len = c.getCount();
388 long [] list = new long[len];
389 for (int i = 0; i < len; i++) {
391 list[i] = c.getLong(0);
403 * Fills out the given submenu with items for "new playlist" and
404 * any existing playlists. When the user selects an item, the
405 * application will receive PLAYLIST_SELECTED with the Uri of
406 * the selected playlist, NEW_PLAYLIST if a new playlist
407 * should be created, and QUEUE if the "current playlist" was
409 * @param context The context to use for creating the menu items
410 * @param sub The submenu to add the items to.
412 public static void makePlaylistMenu(Context context, SubMenu sub) {
413 String[] cols = new String[] {
414 MediaStore.Audio.Playlists._ID,
415 MediaStore.Audio.Playlists.NAME
417 ContentResolver resolver = context.getContentResolver();
418 if (resolver == null) {
419 System.out.println("resolver = null");
421 String whereclause = MediaStore.Audio.Playlists.NAME + " != ''";
422 Cursor cur = resolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
423 cols, whereclause, null,
424 MediaStore.Audio.Playlists.NAME);
426 sub.add(1, Defs.QUEUE, 0, R.string.queue);
427 sub.add(1, Defs.NEW_PLAYLIST, 0, R.string.new_playlist);
428 if (cur != null && cur.getCount() > 0) {
429 //sub.addSeparator(1, 0);
431 while (! cur.isAfterLast()) {
432 Intent intent = new Intent();
433 intent.putExtra("playlist", cur.getLong(0));
434 // if (cur.getInt(0) == mLastPlaylistSelected) {
435 // sub.add(0, MusicBaseActivity.PLAYLIST_SELECTED, cur.getString(1)).setIntent(intent);
437 sub.add(1, Defs.PLAYLIST_SELECTED, 0, cur.getString(1)).setIntent(intent);
448 public static void clearPlaylist(Context context, int plid) {
450 Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", plid);
451 context.getContentResolver().delete(uri, null, null);
455 public static void deleteTracks(Context context, long [] list) {
457 String [] cols = new String [] { MediaStore.Audio.Media._ID,
458 MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.ALBUM_ID };
459 StringBuilder where = new StringBuilder();
460 where.append(MediaStore.Audio.Media._ID + " IN (");
461 for (int i = 0; i < list.length; i++) {
462 where.append(list[i]);
463 if (i < list.length - 1) {
468 Cursor c = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, cols,
469 where.toString(), null, null);
473 // step 1: remove selected tracks from the current playlist, as well
474 // as from the album art cache
477 while (! c.isAfterLast()) {
478 // remove from current playlist
479 long id = c.getLong(0);
480 sService.removeTrack(id);
481 // remove from album art cache
482 long artIndex = c.getLong(2);
483 synchronized(sArtCache) {
484 sArtCache.remove(artIndex);
488 } catch (RemoteException ex) {
491 // step 2: remove selected tracks from the database
492 context.getContentResolver().delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, where.toString(), null);
494 // step 3: remove files from card
496 while (! c.isAfterLast()) {
497 String name = c.getString(1);
498 File f = new File(name);
499 try { // File.delete can throw a security exception
501 // I'm not sure if we'd ever get here (deletion would
502 // have to fail, but no exception thrown)
503 Log.e("MusicUtils", "Failed to delete file " + name);
506 } catch (SecurityException ex) {
513 String message = context.getResources().getQuantityString(
514 R.plurals.NNNtracksdeleted, list.length, Integer.valueOf(list.length));
516 Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
517 // We deleted a number of tracks, which could affect any number of things
518 // in the media content domain, so update everything.
519 context.getContentResolver().notifyChange(Uri.parse("content://media"), null);
522 public static void addToCurrentPlaylist(Context context, long [] list) {
523 if (sService == null) {
527 sService.enqueue(list, MediaPlaybackService.LAST);
528 String message = context.getResources().getQuantityString(
529 R.plurals.NNNtrackstoplaylist, list.length, Integer.valueOf(list.length));
530 Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
531 } catch (RemoteException ex) {
535 private static ContentValues[] sContentValuesCache = null;
538 * @param ids The source array containing all the ids to be added to the playlist
539 * @param offset Where in the 'ids' array we start reading
540 * @param len How many items to copy during this pass
541 * @param base The play order offset to use for this pass
543 private static void makeInsertItems(long[] ids, int offset, int len, int base) {
544 // adjust 'len' if would extend beyond the end of the source array
545 if (offset + len > ids.length) {
546 len = ids.length - offset;
548 // allocate the ContentValues array, or reallocate if it is the wrong size
549 if (sContentValuesCache == null || sContentValuesCache.length != len) {
550 sContentValuesCache = new ContentValues[len];
552 // fill in the ContentValues array with the right values for this pass
553 for (int i = 0; i < len; i++) {
554 if (sContentValuesCache[i] == null) {
555 sContentValuesCache[i] = new ContentValues();
558 sContentValuesCache[i].put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, base + offset + i);
559 sContentValuesCache[i].put(MediaStore.Audio.Playlists.Members.AUDIO_ID, ids[offset + i]);
563 public static void addToPlaylist(Context context, long [] ids, long playlistid) {
565 // this shouldn't happen (the menuitems shouldn't be visible
566 // unless the selected item represents something playable
567 Log.e("MusicBase", "ListSelection null");
569 int size = ids.length;
570 ContentResolver resolver = context.getContentResolver();
571 // need to determine the number of items currently in the playlist,
572 // so the play_order field can be maintained.
573 String[] cols = new String[] {
576 Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistid);
577 Cursor cur = resolver.query(uri, cols, null, null, null);
579 int base = cur.getInt(0);
582 for (int i = 0; i < size; i += 1000) {
583 makeInsertItems(ids, i, 1000, base);
584 numinserted += resolver.bulkInsert(uri, sContentValuesCache);
586 String message = context.getResources().getQuantityString(
587 R.plurals.NNNtrackstoplaylist, numinserted, numinserted);
588 Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
589 //mLastPlaylistSelected = playlistid;
593 public static Cursor query(Context context, Uri uri, String[] projection,
594 String selection, String[] selectionArgs, String sortOrder, int limit) {
596 ContentResolver resolver = context.getContentResolver();
597 if (resolver == null) {
601 uri = uri.buildUpon().appendQueryParameter("limit", "" + limit).build();
603 return resolver.query(uri, projection, selection, selectionArgs, sortOrder);
604 } catch (UnsupportedOperationException ex) {
609 public static Cursor query(Context context, Uri uri, String[] projection,
610 String selection, String[] selectionArgs, String sortOrder) {
611 return query(context, uri, projection, selection, selectionArgs, sortOrder, 0);
614 public static boolean isMediaScannerScanning(Context context) {
615 boolean result = false;
616 Cursor cursor = query(context, MediaStore.getMediaScannerUri(),
617 new String [] { MediaStore.MEDIA_SCANNER_VOLUME }, null, null, null);
618 if (cursor != null) {
619 if (cursor.getCount() == 1) {
620 cursor.moveToFirst();
621 result = "external".equals(cursor.getString(0));
629 public static void setSpinnerState(Activity a) {
630 if (isMediaScannerScanning(a)) {
631 // start the progress spinner
632 a.getWindow().setFeatureInt(
633 Window.FEATURE_INDETERMINATE_PROGRESS,
634 Window.PROGRESS_INDETERMINATE_ON);
636 a.getWindow().setFeatureInt(
637 Window.FEATURE_INDETERMINATE_PROGRESS,
638 Window.PROGRESS_VISIBILITY_ON);
640 // stop the progress spinner
641 a.getWindow().setFeatureInt(
642 Window.FEATURE_INDETERMINATE_PROGRESS,
643 Window.PROGRESS_VISIBILITY_OFF);
647 private static String mLastSdStatus;
649 public static void displayDatabaseError(Activity a) {
650 if (a.isFinishing()) {
651 // When switching tabs really fast, we can end up with a null
652 // cursor (not sure why), which will bring us here.
653 // Don't bother showing an error message in that case.
657 String status = Environment.getExternalStorageState();
658 int title = R.string.sdcard_error_title;
659 int message = R.string.sdcard_error_message;
661 if (status.equals(Environment.MEDIA_SHARED) ||
662 status.equals(Environment.MEDIA_UNMOUNTED)) {
663 title = R.string.sdcard_busy_title;
664 message = R.string.sdcard_busy_message;
665 } else if (status.equals(Environment.MEDIA_REMOVED)) {
666 title = R.string.sdcard_missing_title;
667 message = R.string.sdcard_missing_message;
668 } else if (status.equals(Environment.MEDIA_MOUNTED)){
669 // The card is mounted, but we didn't get a valid cursor.
670 // This probably means the mediascanner hasn't started scanning the
671 // card yet (there is a small window of time during boot where this
674 Intent intent = new Intent();
675 intent.setClass(a, ScanningProgress.class);
676 a.startActivityForResult(intent, Defs.SCAN_DONE);
677 } else if (!TextUtils.equals(mLastSdStatus, status)) {
678 mLastSdStatus = status;
679 Log.d(TAG, "sd card: " + status);
683 View v = a.findViewById(R.id.sd_message);
685 v.setVisibility(View.VISIBLE);
687 v = a.findViewById(R.id.sd_icon);
689 v.setVisibility(View.VISIBLE);
691 v = a.findViewById(android.R.id.list);
693 v.setVisibility(View.GONE);
695 v = a.findViewById(R.id.buttonbar);
697 v.setVisibility(View.GONE);
699 TextView tv = (TextView) a.findViewById(R.id.sd_message);
703 public static void hideDatabaseError(Activity a) {
704 View v = a.findViewById(R.id.sd_message);
706 v.setVisibility(View.GONE);
708 v = a.findViewById(R.id.sd_icon);
710 v.setVisibility(View.GONE);
712 v = a.findViewById(android.R.id.list);
714 v.setVisibility(View.VISIBLE);
718 static protected Uri getContentURIForPath(String path) {
719 return Uri.fromFile(new File(path));
723 /* Try to use String.format() as little as possible, because it creates a
724 * new Formatter every time you call it, which is very inefficient.
725 * Reusing an existing Formatter more than tripled the speed of
727 * This Formatter/StringBuilder are also used by makeAlbumSongsLabel()
729 private static StringBuilder sFormatBuilder = new StringBuilder();
730 private static Formatter sFormatter = new Formatter(sFormatBuilder, Locale.getDefault());
731 private static final Object[] sTimeArgs = new Object[5];
733 public static String makeTimeString(Context context, long secs) {
734 String durationformat = context.getString(
735 secs < 3600 ? R.string.durationformatshort : R.string.durationformatlong);
737 /* Provide multiple arguments so the format can be changed easily
738 * by modifying the xml.
740 sFormatBuilder.setLength(0);
742 final Object[] timeArgs = sTimeArgs;
743 timeArgs[0] = secs / 3600;
744 timeArgs[1] = secs / 60;
745 timeArgs[2] = (secs / 60) % 60;
747 timeArgs[4] = secs % 60;
749 return sFormatter.format(durationformat, timeArgs).toString();
752 public static void shuffleAll(Context context, Cursor cursor) {
753 playAll(context, cursor, 0, true);
756 public static void playAll(Context context, Cursor cursor) {
757 playAll(context, cursor, 0, false);
760 public static void playAll(Context context, Cursor cursor, int position) {
761 playAll(context, cursor, position, false);
764 public static void playAll(Context context, long [] list, int position) {
765 playAll(context, list, position, false);
768 private static void playAll(Context context, Cursor cursor, int position, boolean force_shuffle) {
770 long [] list = getSongListForCursor(cursor);
771 playAll(context, list, position, force_shuffle);
774 private static void playAll(Context context, long [] list, int position, boolean force_shuffle) {
775 if (list.length == 0 || sService == null) {
776 Log.d("MusicUtils", "attempt to play empty song list");
777 // Don't try to play empty playlists. Nothing good will come of it.
778 String message = context.getString(R.string.emptyplaylist, list.length);
779 Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
784 sService.setShuffleMode(MediaPlaybackService.SHUFFLE_NORMAL);
786 long curid = sService.getAudioId();
787 int curpos = sService.getQueuePosition();
788 if (position != -1 && curpos == position && curid == list[position]) {
789 // The selected file is the file that's currently playing;
790 // figure out if we need to restart with a new playlist,
791 // or just launch the playback activity.
792 long [] playlist = sService.getQueue();
793 if (Arrays.equals(list, playlist)) {
794 // we don't need to set a new list, but we should resume playback if needed
796 return; // the 'finally' block will still run
802 sService.open(list, force_shuffle ? -1 : position);
804 } catch (RemoteException ex) {
806 Intent intent = new Intent(context, MediaPlaybackActivity.class)
807 .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
808 context.startActivity(intent);
812 public static void clearQueue() {
814 sService.removeTracks(0, Integer.MAX_VALUE);
815 } catch (RemoteException ex) {
819 // A really simple BitmapDrawable-like class, that doesn't do
820 // scaling, dithering or filtering.
821 private static class FastBitmapDrawable extends Drawable {
822 private Bitmap mBitmap;
823 public FastBitmapDrawable(Bitmap b) {
827 public void draw(Canvas canvas) {
828 canvas.drawBitmap(mBitmap, 0, 0, null);
831 public int getOpacity() {
832 return PixelFormat.OPAQUE;
835 public void setAlpha(int alpha) {
838 public void setColorFilter(ColorFilter cf) {
842 private static int sArtId = -2;
843 private static Bitmap mCachedBit = null;
844 private static final BitmapFactory.Options sBitmapOptionsCache = new BitmapFactory.Options();
845 private static final BitmapFactory.Options sBitmapOptions = new BitmapFactory.Options();
846 private static final Uri sArtworkUri = Uri.parse("content://media/external/audio/albumart");
847 private static final HashMap<Long, Drawable> sArtCache = new HashMap<Long, Drawable>();
848 private static int sArtCacheId = -1;
852 // 565 is faster to decode and display
853 // and we don't want to dither here because the image will be scaled down later
854 sBitmapOptionsCache.inPreferredConfig = Bitmap.Config.RGB_565;
855 sBitmapOptionsCache.inDither = false;
857 sBitmapOptions.inPreferredConfig = Bitmap.Config.RGB_565;
858 sBitmapOptions.inDither = false;
861 public static void initAlbumArtCache() {
863 int id = sService.getMediaMountedCount();
864 if (id != sArtCacheId) {
865 clearAlbumArtCache();
868 } catch (RemoteException e) {
873 public static void clearAlbumArtCache() {
874 synchronized(sArtCache) {
879 public static Drawable getCachedArtwork(Context context, long artIndex, BitmapDrawable defaultArtwork) {
881 synchronized(sArtCache) {
882 d = sArtCache.get(artIndex);
886 final Bitmap icon = defaultArtwork.getBitmap();
887 int w = icon.getWidth();
888 int h = icon.getHeight();
889 Bitmap b = MusicUtils.getArtworkQuick(context, artIndex, w, h);
891 d = new FastBitmapDrawable(b);
892 synchronized(sArtCache) {
893 // the cache may have changed since we checked
894 Drawable value = sArtCache.get(artIndex);
896 sArtCache.put(artIndex, d);
906 // Get album art for specified album. This method will not try to
907 // fall back to getting artwork directly from the file, nor will
908 // it attempt to repair the database.
909 private static Bitmap getArtworkQuick(Context context, long album_id, int w, int h) {
910 // NOTE: There is in fact a 1 pixel border on the right side in the ImageView
911 // used to display this drawable. Take it into account now, so we don't have to
914 ContentResolver res = context.getContentResolver();
915 Uri uri = ContentUris.withAppendedId(sArtworkUri, album_id);
917 ParcelFileDescriptor fd = null;
919 fd = res.openFileDescriptor(uri, "r");
922 // Compute the closest power-of-two scale factor
923 // and pass that to sBitmapOptionsCache.inSampleSize, which will
924 // result in faster decoding and better quality
925 sBitmapOptionsCache.inJustDecodeBounds = true;
926 BitmapFactory.decodeFileDescriptor(
927 fd.getFileDescriptor(), null, sBitmapOptionsCache);
928 int nextWidth = sBitmapOptionsCache.outWidth >> 1;
929 int nextHeight = sBitmapOptionsCache.outHeight >> 1;
930 while (nextWidth>w && nextHeight>h) {
936 sBitmapOptionsCache.inSampleSize = sampleSize;
937 sBitmapOptionsCache.inJustDecodeBounds = false;
938 Bitmap b = BitmapFactory.decodeFileDescriptor(
939 fd.getFileDescriptor(), null, sBitmapOptionsCache);
942 // finally rescale to exactly the size we need
943 if (sBitmapOptionsCache.outWidth != w || sBitmapOptionsCache.outHeight != h) {
944 Bitmap tmp = Bitmap.createScaledBitmap(b, w, h, true);
945 // Bitmap.createScaledBitmap() can return the same bitmap
946 if (tmp != b) b.recycle();
952 } catch (FileNotFoundException e) {
957 } catch (IOException e) {
964 /** Get album art for specified album. You should not pass in the album id
965 * for the "unknown" album here (use -1 instead)
966 * This method always returns the default album art icon when no album art is found.
968 public static Bitmap getArtwork(Context context, long song_id, long album_id) {
969 return getArtwork(context, song_id, album_id, true);
972 /** Get album art for specified album. You should not pass in the album id
973 * for the "unknown" album here (use -1 instead)
975 public static Bitmap getArtwork(Context context, long song_id, long album_id,
976 boolean allowdefault) {
979 // This is something that is not in the database, so get the album art directly
982 Bitmap bm = getArtworkFromFile(context, song_id, -1);
988 return getDefaultArtwork(context);
993 ContentResolver res = context.getContentResolver();
994 Uri uri = ContentUris.withAppendedId(sArtworkUri, album_id);
996 InputStream in = null;
998 in = res.openInputStream(uri);
999 return BitmapFactory.decodeStream(in, null, sBitmapOptions);
1000 } catch (FileNotFoundException ex) {
1001 // The album art thumbnail does not actually exist. Maybe the user deleted it, or
1002 // maybe it never existed to begin with.
1003 Bitmap bm = getArtworkFromFile(context, song_id, album_id);
1005 if (bm.getConfig() == null) {
1006 bm = bm.copy(Bitmap.Config.RGB_565, false);
1007 if (bm == null && allowdefault) {
1008 return getDefaultArtwork(context);
1011 } else if (allowdefault) {
1012 bm = getDefaultArtwork(context);
1020 } catch (IOException ex) {
1028 // get album art for specified file
1029 private static final String sExternalMediaUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString();
1030 private static Bitmap getArtworkFromFile(Context context, long songid, long albumid) {
1035 if (albumid < 0 && songid < 0) {
1036 throw new IllegalArgumentException("Must specify an album or a song id");
1041 Uri uri = Uri.parse("content://media/external/audio/media/" + songid + "/albumart");
1042 ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
1044 FileDescriptor fd = pfd.getFileDescriptor();
1045 bm = BitmapFactory.decodeFileDescriptor(fd);
1048 Uri uri = ContentUris.withAppendedId(sArtworkUri, albumid);
1049 ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
1051 FileDescriptor fd = pfd.getFileDescriptor();
1052 bm = BitmapFactory.decodeFileDescriptor(fd);
1055 } catch (FileNotFoundException ex) {
1064 private static Bitmap getDefaultArtwork(Context context) {
1065 BitmapFactory.Options opts = new BitmapFactory.Options();
1066 opts.inPreferredConfig = Bitmap.Config.ARGB_8888;
1067 return BitmapFactory.decodeStream(
1068 context.getResources().openRawResource(R.drawable.albumart_mp_unknown), null, opts);
1071 static int getIntPref(Context context, String name, int def) {
1072 SharedPreferences prefs =
1073 context.getSharedPreferences("com.android.music", Context.MODE_PRIVATE);
1074 return prefs.getInt(name, def);
1077 static void setIntPref(Context context, String name, int value) {
1078 SharedPreferences prefs =
1079 context.getSharedPreferences("com.android.music", Context.MODE_PRIVATE);
1080 Editor ed = prefs.edit();
1081 ed.putInt(name, value);
1085 static void setRingtone(Context context, long id) {
1086 ContentResolver resolver = context.getContentResolver();
1087 // Set the flag in the database to mark this as a ringtone
1088 Uri ringUri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
1090 ContentValues values = new ContentValues(2);
1091 values.put(MediaStore.Audio.Media.IS_RINGTONE, "1");
1092 values.put(MediaStore.Audio.Media.IS_ALARM, "1");
1093 resolver.update(ringUri, values, null, null);
1094 } catch (UnsupportedOperationException ex) {
1095 // most likely the card just got unmounted
1096 Log.e(TAG, "couldn't set ringtone flag for id " + id);
1100 String[] cols = new String[] {
1101 MediaStore.Audio.Media._ID,
1102 MediaStore.Audio.Media.DATA,
1103 MediaStore.Audio.Media.TITLE
1106 String where = MediaStore.Audio.Media._ID + "=" + id;
1107 Cursor cursor = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1108 cols, where , null, null);
1110 if (cursor != null && cursor.getCount() == 1) {
1111 // Set the system setting to make this the current ringtone
1112 cursor.moveToFirst();
1113 Settings.System.putString(resolver, Settings.System.RINGTONE, ringUri.toString());
1114 String message = context.getString(R.string.ringtone_set, cursor.getString(2));
1115 Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
1118 if (cursor != null) {
1124 static int sActiveTabIndex = -1;
1126 static boolean updateButtonBar(Activity a, int highlight) {
1127 final TabWidget ll = (TabWidget) a.findViewById(R.id.buttonbar);
1128 boolean withtabs = false;
1129 Intent intent = a.getIntent();
1130 if (intent != null) {
1131 withtabs = intent.getBooleanExtra("withtabs", false);
1134 if (highlight == 0 || !withtabs) {
1135 ll.setVisibility(View.GONE);
1137 } else if (withtabs) {
1138 ll.setVisibility(View.VISIBLE);
1140 for (int i = ll.getChildCount() - 1; i >= 0; i--) {
1142 View v = ll.getChildAt(i);
1143 boolean isActive = (v.getId() == highlight);
1145 ll.setCurrentTab(i);
1146 sActiveTabIndex = i;
1149 v.setOnFocusChangeListener(new View.OnFocusChangeListener() {
1151 public void onFocusChange(View v, boolean hasFocus) {
1153 for (int i = 0; i < ll.getTabCount(); i++) {
1154 if (ll.getChildTabViewAt(i) == v) {
1155 ll.setCurrentTab(i);
1156 processTabClick((Activity)ll.getContext(), v, ll.getChildAt(sActiveTabIndex).getId());
1163 v.setOnClickListener(new View.OnClickListener() {
1165 public void onClick(View v) {
1166 processTabClick((Activity)ll.getContext(), v, ll.getChildAt(sActiveTabIndex).getId());
1172 static void processTabClick(Activity a, View v, int current) {
1174 if (id == current) {
1178 final TabWidget ll = (TabWidget) a.findViewById(R.id.buttonbar);
1179 ll.setCurrentTab((Integer) v.getTag());
1182 if (id != R.id.nowplayingtab) {
1183 setIntPref(a, "activetab", id);
1187 static void activateTab(Activity a, int id) {
1188 Intent intent = new Intent(Intent.ACTION_PICK);
1190 case R.id.artisttab:
1191 intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/artistalbum");
1194 intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/album");
1197 intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track");
1199 case R.id.playlisttab:
1200 intent.setDataAndType(Uri.EMPTY, MediaStore.Audio.Playlists.CONTENT_TYPE);
1202 case R.id.nowplayingtab:
1203 intent = new Intent(a, MediaPlaybackActivity.class);
1204 a.startActivity(intent);
1205 // fall through and return
1209 intent.putExtra("withtabs", true);
1210 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
1211 a.startActivity(intent);
1213 a.overridePendingTransition(0, 0);
1216 static void updateNowPlaying(Activity a) {
1217 View nowPlayingView = a.findViewById(R.id.nowplaying);
1218 if (nowPlayingView == null) {
1222 boolean withtabs = false;
1223 Intent intent = a.getIntent();
1224 if (intent != null) {
1225 withtabs = intent.getBooleanExtra("withtabs", false);
1227 if (true && MusicUtils.sService != null && MusicUtils.sService.getAudioId() != -1) {
1228 TextView title = (TextView) nowPlayingView.findViewById(R.id.title);
1229 TextView artist = (TextView) nowPlayingView.findViewById(R.id.artist);
1230 title.setText(MusicUtils.sService.getTrackName());
1231 String artistName = MusicUtils.sService.getArtistName();
1232 if (MediaStore.UNKNOWN_STRING.equals(artistName)) {
1233 artistName = a.getString(R.string.unknown_artist_name);
1235 artist.setText(artistName);
1236 //mNowPlayingView.setOnFocusChangeListener(mFocuser);
1237 //mNowPlayingView.setOnClickListener(this);
1238 nowPlayingView.setVisibility(View.VISIBLE);
1239 nowPlayingView.setOnClickListener(new View.OnClickListener() {
1241 public void onClick(View v) {
1242 Context c = v.getContext();
1243 c.startActivity(new Intent(c, MediaPlaybackActivity.class));
1247 } catch (RemoteException ex) {
1249 nowPlayingView.setVisibility(View.GONE);
1252 static void setBackground(View v, Bitmap bm) {
1255 v.setBackgroundResource(0);
1259 int vwidth = v.getWidth();
1260 int vheight = v.getHeight();
1261 int bwidth = bm.getWidth();
1262 int bheight = bm.getHeight();
1263 float scalex = (float) vwidth / bwidth;
1264 float scaley = (float) vheight / bheight;
1265 float scale = Math.max(scalex, scaley) * 1.3f;
1267 Bitmap.Config config = Bitmap.Config.ARGB_8888;
1268 Bitmap bg = Bitmap.createBitmap(vwidth, vheight, config);
1269 Canvas c = new Canvas(bg);
1270 Paint paint = new Paint();
1271 paint.setAntiAlias(true);
1272 paint.setFilterBitmap(true);
1273 ColorMatrix greymatrix = new ColorMatrix();
1274 greymatrix.setSaturation(0);
1275 ColorMatrix darkmatrix = new ColorMatrix();
1276 darkmatrix.setScale(.3f, .3f, .3f, 1.0f);
1277 greymatrix.postConcat(darkmatrix);
1278 ColorFilter filter = new ColorMatrixColorFilter(greymatrix);
1279 paint.setColorFilter(filter);
1280 Matrix matrix = new Matrix();
1281 matrix.setTranslate(-bwidth/2, -bheight/2); // move bitmap center to origin
1282 matrix.postRotate(10);
1283 matrix.postScale(scale, scale);
1284 matrix.postTranslate(vwidth/2, vheight/2); // Move bitmap center to view center
1285 c.drawBitmap(bm, matrix, paint);
1286 v.setBackgroundDrawable(new BitmapDrawable(bg));
1289 static int getCardId(Context context) {
1290 ContentResolver res = context.getContentResolver();
1291 Cursor c = res.query(Uri.parse("content://media/external/fs_id"), null, null, null, null);