MusicUtils.java revision 6cb8bc92e0ca524a76a6fa3f6814b43ea9a3b30d
1/* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 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 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 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. 15 */ 16 17package com.android.music; 18 19import java.io.File; 20import java.io.FileDescriptor; 21import java.io.FileInputStream; 22import java.io.FileNotFoundException; 23import java.io.FileOutputStream; 24import java.io.IOException; 25import java.io.InputStream; 26import java.io.OutputStream; 27import java.util.Arrays; 28import java.util.Formatter; 29import java.util.HashMap; 30import java.util.Locale; 31 32import android.app.Activity; 33import android.app.ExpandableListActivity; 34import android.content.ComponentName; 35import android.content.ContentResolver; 36import android.content.ContentUris; 37import android.content.ContentValues; 38import android.content.Context; 39import android.content.Intent; 40import android.content.ServiceConnection; 41import android.content.SharedPreferences; 42import android.content.SharedPreferences.Editor; 43import android.content.res.Resources; 44import android.database.Cursor; 45import android.graphics.Bitmap; 46import android.graphics.BitmapFactory; 47import android.graphics.Canvas; 48import android.graphics.ColorFilter; 49import android.graphics.PixelFormat; 50import android.graphics.drawable.BitmapDrawable; 51import android.graphics.drawable.Drawable; 52import android.media.MediaFile; 53import android.media.MediaScanner; 54import android.net.Uri; 55import android.os.RemoteException; 56import android.os.Environment; 57import android.os.ParcelFileDescriptor; 58import android.provider.MediaStore; 59import android.provider.Settings; 60import android.util.Log; 61import android.view.SubMenu; 62import android.view.Window; 63import android.widget.TextView; 64import android.widget.Toast; 65 66public class MusicUtils { 67 68 private static final String TAG = "MusicUtils"; 69 70 public interface Defs { 71 public final static int OPEN_URL = 0; 72 public final static int ADD_TO_PLAYLIST = 1; 73 public final static int USE_AS_RINGTONE = 2; 74 public final static int PLAYLIST_SELECTED = 3; 75 public final static int NEW_PLAYLIST = 4; 76 public final static int PLAY_SELECTION = 5; 77 public final static int GOTO_START = 6; 78 public final static int GOTO_PLAYBACK = 7; 79 public final static int PARTY_SHUFFLE = 8; 80 public final static int SHUFFLE_ALL = 9; 81 public final static int DELETE_ITEM = 10; 82 public final static int SCAN_DONE = 11; 83 public final static int CHILD_MENU_BASE = 12; 84 public final static int QUEUE = 13; 85 } 86 87 public static String makeAlbumsSongsLabel(Context context, int numalbums, int numsongs, boolean isUnknown) { 88 // There are several formats for the albums/songs information: 89 // "1 Song" - used if there is only 1 song 90 // "N Songs" - used for the "unknown artist" item 91 // "1 Album"/"N Songs" 92 // "N Album"/"M Songs" 93 // Depending on locale, these may need to be further subdivided 94 95 StringBuilder songs_albums = new StringBuilder(); 96 97 if (numsongs == 1) { 98 songs_albums.append(context.getString(R.string.onesong)); 99 } else { 100 Resources r = context.getResources(); 101 if (! isUnknown) { 102 String f = r.getQuantityText(R.plurals.Nalbums, numalbums).toString(); 103 sFormatBuilder.setLength(0); 104 sFormatter.format(f, Integer.valueOf(numalbums)); 105 songs_albums.append(sFormatBuilder); 106 songs_albums.append(context.getString(R.string.albumsongseparator)); 107 } 108 String f = r.getQuantityText(R.plurals.Nsongs, numsongs).toString(); 109 sFormatBuilder.setLength(0); 110 sFormatter.format(f, Integer.valueOf(numsongs)); 111 songs_albums.append(sFormatBuilder); 112 } 113 return songs_albums.toString(); 114 } 115 116 public static IMediaPlaybackService sService = null; 117 private static HashMap<Context, ServiceBinder> sConnectionMap = new HashMap<Context, ServiceBinder>(); 118 119 public static boolean bindToService(Context context) { 120 return bindToService(context, null); 121 } 122 123 public static boolean bindToService(Context context, ServiceConnection callback) { 124 context.startService(new Intent(context, MediaPlaybackService.class)); 125 ServiceBinder sb = new ServiceBinder(callback); 126 sConnectionMap.put(context, sb); 127 return context.bindService((new Intent()).setClass(context, 128 MediaPlaybackService.class), sb, 0); 129 } 130 131 public static void unbindFromService(Context context) { 132 ServiceBinder sb = (ServiceBinder) sConnectionMap.remove(context); 133 if (sb == null) { 134 Log.e("MusicUtils", "Trying to unbind for unknown Context"); 135 return; 136 } 137 context.unbindService(sb); 138 } 139 140 private static class ServiceBinder implements ServiceConnection { 141 ServiceConnection mCallback; 142 ServiceBinder(ServiceConnection callback) { 143 mCallback = callback; 144 } 145 146 public void onServiceConnected(ComponentName className, android.os.IBinder service) { 147 sService = IMediaPlaybackService.Stub.asInterface(service); 148 initAlbumArtCache(); 149 if (mCallback != null) { 150 mCallback.onServiceConnected(className, service); 151 } 152 } 153 154 public void onServiceDisconnected(ComponentName className) { 155 if (mCallback != null) { 156 mCallback.onServiceDisconnected(className); 157 } 158 sService = null; 159 } 160 } 161 162 public static int getCurrentAlbumId() { 163 if (sService != null) { 164 try { 165 return sService.getAlbumId(); 166 } catch (RemoteException ex) { 167 } 168 } 169 return -1; 170 } 171 172 public static int getCurrentArtistId() { 173 if (MusicUtils.sService != null) { 174 try { 175 return sService.getArtistId(); 176 } catch (RemoteException ex) { 177 } 178 } 179 return -1; 180 } 181 182 public static int getCurrentAudioId() { 183 if (MusicUtils.sService != null) { 184 try { 185 return sService.getAudioId(); 186 } catch (RemoteException ex) { 187 } 188 } 189 return -1; 190 } 191 192 public static int getCurrentShuffleMode() { 193 int mode = MediaPlaybackService.SHUFFLE_NONE; 194 if (sService != null) { 195 try { 196 mode = sService.getShuffleMode(); 197 } catch (RemoteException ex) { 198 } 199 } 200 return mode; 201 } 202 203 /* 204 * Returns true if a file is currently opened for playback (regardless 205 * of whether it's playing or paused). 206 */ 207 public static boolean isMusicLoaded() { 208 if (MusicUtils.sService != null) { 209 try { 210 return sService.getPath() != null; 211 } catch (RemoteException ex) { 212 } 213 } 214 return false; 215 } 216 217 private final static int [] sEmptyList = new int[0]; 218 219 public static int [] getSongListForCursor(Cursor cursor) { 220 if (cursor == null) { 221 return sEmptyList; 222 } 223 int len = cursor.getCount(); 224 int [] list = new int[len]; 225 cursor.moveToFirst(); 226 int colidx = cursor.getColumnIndex(MediaStore.Audio.Playlists.Members.AUDIO_ID); 227 if (colidx < 0) { 228 colidx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID); 229 } 230 for (int i = 0; i < len; i++) { 231 list[i] = cursor.getInt(colidx); 232 cursor.moveToNext(); 233 } 234 return list; 235 } 236 237 public static int [] getSongListForArtist(Context context, int id) { 238 final String[] ccols = new String[] { MediaStore.Audio.Media._ID }; 239 String where = MediaStore.Audio.Media.ARTIST_ID + "=" + id + " AND " + 240 MediaStore.Audio.Media.IS_MUSIC + "=1"; 241 Cursor cursor = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 242 ccols, where, null, 243 MediaStore.Audio.Media.ALBUM_KEY + "," + MediaStore.Audio.Media.TRACK); 244 245 if (cursor != null) { 246 int [] list = getSongListForCursor(cursor); 247 cursor.close(); 248 return list; 249 } 250 return sEmptyList; 251 } 252 253 public static int [] getSongListForAlbum(Context context, int id) { 254 final String[] ccols = new String[] { MediaStore.Audio.Media._ID }; 255 String where = MediaStore.Audio.Media.ALBUM_ID + "=" + id + " AND " + 256 MediaStore.Audio.Media.IS_MUSIC + "=1"; 257 Cursor cursor = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 258 ccols, where, null, MediaStore.Audio.Media.TRACK); 259 260 if (cursor != null) { 261 int [] list = getSongListForCursor(cursor); 262 cursor.close(); 263 return list; 264 } 265 return sEmptyList; 266 } 267 268 public static int [] getSongListForPlaylist(Context context, long plid) { 269 final String[] ccols = new String[] { MediaStore.Audio.Playlists.Members.AUDIO_ID }; 270 Cursor cursor = query(context, MediaStore.Audio.Playlists.Members.getContentUri("external", plid), 271 ccols, null, null, MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER); 272 273 if (cursor != null) { 274 int [] list = getSongListForCursor(cursor); 275 cursor.close(); 276 return list; 277 } 278 return sEmptyList; 279 } 280 281 public static void playPlaylist(Context context, long plid) { 282 int [] list = getSongListForPlaylist(context, plid); 283 if (list != null) { 284 playAll(context, list, -1, false); 285 } 286 } 287 288 public static int [] getAllSongs(Context context) { 289 Cursor c = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 290 new String[] {MediaStore.Audio.Media._ID}, MediaStore.Audio.Media.IS_MUSIC + "=1", 291 null, null); 292 try { 293 if (c == null || c.getCount() == 0) { 294 return null; 295 } 296 int len = c.getCount(); 297 int[] list = new int[len]; 298 for (int i = 0; i < len; i++) { 299 c.moveToNext(); 300 list[i] = c.getInt(0); 301 } 302 303 return list; 304 } finally { 305 if (c != null) { 306 c.close(); 307 } 308 } 309 } 310 311 /** 312 * Fills out the given submenu with items for "new playlist" and 313 * any existing playlists. When the user selects an item, the 314 * application will receive PLAYLIST_SELECTED with the Uri of 315 * the selected playlist, NEW_PLAYLIST if a new playlist 316 * should be created, and QUEUE if the "current playlist" was 317 * selected. 318 * @param context The context to use for creating the menu items 319 * @param sub The submenu to add the items to. 320 */ 321 public static void makePlaylistMenu(Context context, SubMenu sub) { 322 String[] cols = new String[] { 323 MediaStore.Audio.Playlists._ID, 324 MediaStore.Audio.Playlists.NAME 325 }; 326 ContentResolver resolver = context.getContentResolver(); 327 if (resolver == null) { 328 System.out.println("resolver = null"); 329 } else { 330 String whereclause = MediaStore.Audio.Playlists.NAME + " != ''"; 331 Cursor cur = resolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, 332 cols, whereclause, null, 333 MediaStore.Audio.Playlists.NAME); 334 sub.clear(); 335 sub.add(1, Defs.QUEUE, 0, R.string.queue); 336 sub.add(1, Defs.NEW_PLAYLIST, 0, R.string.new_playlist); 337 if (cur != null && cur.getCount() > 0) { 338 //sub.addSeparator(1, 0); 339 cur.moveToFirst(); 340 while (! cur.isAfterLast()) { 341 Intent intent = new Intent(); 342 intent.putExtra("playlist", cur.getInt(0)); 343// if (cur.getInt(0) == mLastPlaylistSelected) { 344// sub.add(0, MusicBaseActivity.PLAYLIST_SELECTED, cur.getString(1)).setIntent(intent); 345// } else { 346 sub.add(1, Defs.PLAYLIST_SELECTED, 0, cur.getString(1)).setIntent(intent); 347// } 348 cur.moveToNext(); 349 } 350 } 351 if (cur != null) { 352 cur.close(); 353 } 354 } 355 } 356 357 public static void clearPlaylist(Context context, int plid) { 358 final String[] ccols = new String[] { MediaStore.Audio.Playlists.Members._ID }; 359 Cursor cursor = query(context, MediaStore.Audio.Playlists.Members.getContentUri("external", plid), 360 ccols, null, null, MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER); 361 362 if (cursor == null) { 363 return; 364 } 365 cursor.moveToFirst(); 366 while (!cursor.isAfterLast()) { 367 cursor.deleteRow(); 368 } 369 cursor.commitUpdates(); 370 cursor.close(); 371 return; 372 } 373 374 public static void deleteTracks(Context context, int [] list) { 375 376 String [] cols = new String [] { MediaStore.Audio.Media._ID, 377 MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.ALBUM_ID }; 378 StringBuilder where = new StringBuilder(); 379 where.append(MediaStore.Audio.Media._ID + " IN ("); 380 for (int i = 0; i < list.length; i++) { 381 where.append(list[i]); 382 if (i < list.length - 1) { 383 where.append(","); 384 } 385 } 386 where.append(")"); 387 Cursor c = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, cols, 388 where.toString(), null, null); 389 390 if (c != null) { 391 392 // step 1: remove selected tracks from the current playlist, as well 393 // as from the album art cache 394 try { 395 c.moveToFirst(); 396 while (! c.isAfterLast()) { 397 // remove from current playlist 398 int id = c.getInt(0); 399 sService.removeTrack(id); 400 // remove from album art cache 401 int artIndex = c.getInt(2); 402 synchronized(sArtCache) { 403 sArtCache.remove(artIndex); 404 } 405 c.moveToNext(); 406 } 407 } catch (RemoteException ex) { 408 } 409 410 // step 2: remove selected tracks from the database 411 context.getContentResolver().delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, where.toString(), null); 412 413 // step 3: remove files from card 414 c.moveToFirst(); 415 while (! c.isAfterLast()) { 416 String name = c.getString(1); 417 File f = new File(name); 418 try { // File.delete can throw a security exception 419 if (!f.delete()) { 420 // I'm not sure if we'd ever get here (deletion would 421 // have to fail, but no exception thrown) 422 Log.e("MusicUtils", "Failed to delete file " + name); 423 } 424 c.moveToNext(); 425 } catch (SecurityException ex) { 426 c.moveToNext(); 427 } 428 } 429 c.commitUpdates(); 430 c.close(); 431 } 432 433 String message = context.getResources().getQuantityString( 434 R.plurals.NNNtracksdeleted, list.length, Integer.valueOf(list.length)); 435 436 Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); 437 // We deleted a number of tracks, which could affect any number of things 438 // in the media content domain, so update everything. 439 context.getContentResolver().notifyChange(Uri.parse("content://media"), null); 440 } 441 442 public static void addToCurrentPlaylist(Context context, int [] list) { 443 if (sService == null) { 444 return; 445 } 446 try { 447 sService.enqueue(list, MediaPlaybackService.LAST); 448 String message = context.getResources().getQuantityString( 449 R.plurals.NNNtrackstoplaylist, list.length, Integer.valueOf(list.length)); 450 Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); 451 } catch (RemoteException ex) { 452 } 453 } 454 455 public static void addToPlaylist(Context context, int [] ids, long playlistid) { 456 if (ids == null) { 457 // this shouldn't happen (the menuitems shouldn't be visible 458 // unless the selected item represents something playable 459 Log.e("MusicBase", "ListSelection null"); 460 } else { 461 int size = ids.length; 462 ContentValues values [] = new ContentValues[size]; 463 ContentResolver resolver = context.getContentResolver(); 464 // need to determine the number of items currently in the playlist, 465 // so the play_order field can be maintained. 466 String[] cols = new String[] { 467 "count(*)" 468 }; 469 Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistid); 470 Cursor cur = resolver.query(uri, cols, null, null, null); 471 cur.moveToFirst(); 472 int base = cur.getInt(0); 473 cur.close(); 474 475 for (int i = 0; i < size; i++) { 476 values[i] = new ContentValues(); 477 values[i].put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(base + i)); 478 values[i].put(MediaStore.Audio.Playlists.Members.AUDIO_ID, ids[i]); 479 } 480 resolver.bulkInsert(uri, values); 481 String message = context.getResources().getQuantityString( 482 R.plurals.NNNtrackstoplaylist, size, Integer.valueOf(size)); 483 Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); 484 //mLastPlaylistSelected = playlistid; 485 } 486 } 487 488 public static Cursor query(Context context, Uri uri, String[] projection, 489 String selection, String[] selectionArgs, String sortOrder) { 490 try { 491 ContentResolver resolver = context.getContentResolver(); 492 if (resolver == null) { 493 return null; 494 } 495 return resolver.query(uri, projection, selection, selectionArgs, sortOrder); 496 } catch (UnsupportedOperationException ex) { 497 return null; 498 } 499 500 } 501 502 public static boolean isMediaScannerScanning(Context context) { 503 boolean result = false; 504 Uri uri = MediaStore.getMediaScannerUri(); 505 Cursor cursor = query(context, MediaStore.getMediaScannerUri(), 506 new String [] { MediaStore.MEDIA_SCANNER_VOLUME }, null, null, null); 507 if (cursor != null) { 508 if (cursor.getCount() == 1) { 509 cursor.moveToFirst(); 510 result = "external".equals(cursor.getString(0)); 511 } 512 cursor.close(); 513 } 514 515 return result; 516 } 517 518 public static void setSpinnerState(Activity a) { 519 if (isMediaScannerScanning(a)) { 520 // start the progress spinner 521 a.getWindow().setFeatureInt( 522 Window.FEATURE_INDETERMINATE_PROGRESS, 523 Window.PROGRESS_INDETERMINATE_ON); 524 525 a.getWindow().setFeatureInt( 526 Window.FEATURE_INDETERMINATE_PROGRESS, 527 Window.PROGRESS_VISIBILITY_ON); 528 } else { 529 // stop the progress spinner 530 a.getWindow().setFeatureInt( 531 Window.FEATURE_INDETERMINATE_PROGRESS, 532 Window.PROGRESS_VISIBILITY_OFF); 533 } 534 } 535 536 public static void displayDatabaseError(Activity a) { 537 String status = Environment.getExternalStorageState(); 538 int title = R.string.sdcard_error_title; 539 int message = R.string.sdcard_error_message; 540 541 if (status.equals(Environment.MEDIA_SHARED)) { 542 title = R.string.sdcard_busy_title; 543 message = R.string.sdcard_busy_message; 544 } else if (status.equals(Environment.MEDIA_REMOVED)) { 545 title = R.string.sdcard_missing_title; 546 message = R.string.sdcard_missing_message; 547 } else if (status.equals(Environment.MEDIA_MOUNTED)){ 548 // The card is mounted, but we didn't get a valid cursor. 549 // This probably means the mediascanner hasn't started scanning the 550 // card yet (there is a small window of time during boot where this 551 // will happen). 552 a.setTitle(""); 553 Intent intent = new Intent(); 554 intent.setClass(a, ScanningProgress.class); 555 a.startActivityForResult(intent, Defs.SCAN_DONE); 556 } else { 557 Log.d(TAG, "sd card: " + status); 558 } 559 560 a.setTitle(title); 561 if (a instanceof ExpandableListActivity) { 562 a.setContentView(R.layout.no_sd_card_expanding); 563 } else { 564 a.setContentView(R.layout.no_sd_card); 565 } 566 TextView tv = (TextView) a.findViewById(R.id.sd_message); 567 tv.setText(message); 568 } 569 570 static protected Uri getContentURIForPath(String path) { 571 return Uri.fromFile(new File(path)); 572 } 573 574 575 /* Try to use String.format() as little as possible, because it creates a 576 * new Formatter every time you call it, which is very inefficient. 577 * Reusing an existing Formatter more than tripled the speed of 578 * makeTimeString(). 579 * This Formatter/StringBuilder are also used by makeAlbumSongsLabel() 580 */ 581 private static StringBuilder sFormatBuilder = new StringBuilder(); 582 private static Formatter sFormatter = new Formatter(sFormatBuilder, Locale.getDefault()); 583 private static final Object[] sTimeArgs = new Object[5]; 584 585 public static String makeTimeString(Context context, long secs) { 586 String durationformat = context.getString(R.string.durationformat); 587 588 /* Provide multiple arguments so the format can be changed easily 589 * by modifying the xml. 590 */ 591 sFormatBuilder.setLength(0); 592 593 final Object[] timeArgs = sTimeArgs; 594 timeArgs[0] = secs / 3600; 595 timeArgs[1] = secs / 60; 596 timeArgs[2] = (secs / 60) % 60; 597 timeArgs[3] = secs; 598 timeArgs[4] = secs % 60; 599 600 return sFormatter.format(durationformat, timeArgs).toString(); 601 } 602 603 public static void shuffleAll(Context context, Cursor cursor) { 604 playAll(context, cursor, 0, true); 605 } 606 607 public static void playAll(Context context, Cursor cursor) { 608 playAll(context, cursor, 0, false); 609 } 610 611 public static void playAll(Context context, Cursor cursor, int position) { 612 playAll(context, cursor, position, false); 613 } 614 615 public static void playAll(Context context, int [] list, int position) { 616 playAll(context, list, position, false); 617 } 618 619 private static void playAll(Context context, Cursor cursor, int position, boolean force_shuffle) { 620 621 int [] list = getSongListForCursor(cursor); 622 playAll(context, list, position, force_shuffle); 623 } 624 625 private static void playAll(Context context, int [] list, int position, boolean force_shuffle) { 626 if (list.length == 0 || sService == null) { 627 Log.d("MusicUtils", "attempt to play empty song list"); 628 // Don't try to play empty playlists. Nothing good will come of it. 629 String message = context.getString(R.string.emptyplaylist, list.length); 630 Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); 631 return; 632 } 633 try { 634 if (force_shuffle) { 635 sService.setShuffleMode(MediaPlaybackService.SHUFFLE_NORMAL); 636 } 637 int curid = sService.getAudioId(); 638 int curpos = sService.getQueuePosition(); 639 if (position != -1 && curpos == position && curid == list[position]) { 640 // The selected file is the file that's currently playing; 641 // figure out if we need to restart with a new playlist, 642 // or just launch the playback activity. 643 int [] playlist = sService.getQueue(); 644 if (Arrays.equals(list, playlist)) { 645 // we don't need to set a new list, but we should resume playback if needed 646 sService.play(); 647 return; // the 'finally' block will still run 648 } 649 } 650 if (position < 0) { 651 position = 0; 652 } 653 sService.open(list, position); 654 sService.play(); 655 } catch (RemoteException ex) { 656 } finally { 657 Intent intent = new Intent("com.android.music.PLAYBACK_VIEWER") 658 .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 659 context.startActivity(intent); 660 } 661 } 662 663 public static void clearQueue() { 664 try { 665 sService.removeTracks(0, Integer.MAX_VALUE); 666 } catch (RemoteException ex) { 667 } 668 } 669 670 // A really simple BitmapDrawable-like class, that doesn't do 671 // scaling, dithering or filtering. 672 private static class FastBitmapDrawable extends Drawable { 673 private Bitmap mBitmap; 674 public FastBitmapDrawable(Bitmap b) { 675 mBitmap = b; 676 } 677 @Override 678 public void draw(Canvas canvas) { 679 canvas.drawBitmap(mBitmap, 0, 0, null); 680 } 681 @Override 682 public int getOpacity() { 683 return PixelFormat.OPAQUE; 684 } 685 @Override 686 public void setAlpha(int alpha) { 687 } 688 @Override 689 public void setColorFilter(ColorFilter cf) { 690 } 691 } 692 693 private static int sArtId = -2; 694 private static byte [] mCachedArt; 695 private static Bitmap mCachedBit = null; 696 private static final BitmapFactory.Options sBitmapOptionsCache = new BitmapFactory.Options(); 697 private static final BitmapFactory.Options sBitmapOptions = new BitmapFactory.Options(); 698 private static final Uri sArtworkUri = Uri.parse("content://media/external/audio/albumart"); 699 private static final HashMap<Integer, Drawable> sArtCache = new HashMap<Integer, Drawable>(); 700 private static int sArtCacheId = -1; 701 702 static { 703 // for the cache, 704 // 565 is faster to decode and display 705 // and we don't want to dither here because the image will be scaled down later 706 sBitmapOptionsCache.inPreferredConfig = Bitmap.Config.RGB_565; 707 sBitmapOptionsCache.inDither = false; 708 709 sBitmapOptions.inPreferredConfig = Bitmap.Config.RGB_565; 710 sBitmapOptions.inDither = false; 711 } 712 713 public static void initAlbumArtCache() { 714 try { 715 int id = sService.getMediaMountedCount(); 716 if (id != sArtCacheId) { 717 clearAlbumArtCache(); 718 sArtCacheId = id; 719 } 720 } catch (RemoteException e) { 721 e.printStackTrace(); 722 } 723 } 724 725 public static void clearAlbumArtCache() { 726 synchronized(sArtCache) { 727 sArtCache.clear(); 728 } 729 } 730 731 public static Drawable getCachedArtwork(Context context, int artIndex, BitmapDrawable defaultArtwork) { 732 Drawable d = null; 733 synchronized(sArtCache) { 734 d = sArtCache.get(artIndex); 735 } 736 if (d == null) { 737 d = defaultArtwork; 738 final Bitmap icon = defaultArtwork.getBitmap(); 739 int w = icon.getWidth(); 740 int h = icon.getHeight(); 741 Bitmap b = MusicUtils.getArtworkQuick(context, artIndex, w, h); 742 if (b != null) { 743 d = new FastBitmapDrawable(b); 744 synchronized(sArtCache) { 745 // the cache may have changed since we checked 746 Drawable value = sArtCache.get(artIndex); 747 if (value == null) { 748 sArtCache.put(artIndex, d); 749 } else { 750 d = value; 751 } 752 } 753 } 754 } 755 return d; 756 } 757 758 // Get album art for specified album. This method will not try to 759 // fall back to getting artwork directly from the file, nor will 760 // it attempt to repair the database. 761 private static Bitmap getArtworkQuick(Context context, int album_id, int w, int h) { 762 // NOTE: There is in fact a 1 pixel frame in the ImageView used to 763 // display this drawable. Take it into account now, so we don't have to 764 // scale later. 765 w -= 2; 766 h -= 2; 767 ContentResolver res = context.getContentResolver(); 768 Uri uri = ContentUris.withAppendedId(sArtworkUri, album_id); 769 if (uri != null) { 770 ParcelFileDescriptor fd = null; 771 try { 772 fd = res.openFileDescriptor(uri, "r"); 773 int sampleSize = 1; 774 775 // Compute the closest power-of-two scale factor 776 // and pass that to sBitmapOptionsCache.inSampleSize, which will 777 // result in faster decoding and better quality 778 sBitmapOptionsCache.inJustDecodeBounds = true; 779 BitmapFactory.decodeFileDescriptor( 780 fd.getFileDescriptor(), null, sBitmapOptionsCache); 781 int nextWidth = sBitmapOptionsCache.outWidth >> 1; 782 int nextHeight = sBitmapOptionsCache.outHeight >> 1; 783 while (nextWidth>w && nextHeight>h) { 784 sampleSize <<= 1; 785 nextWidth >>= 1; 786 nextHeight >>= 1; 787 } 788 789 sBitmapOptionsCache.inSampleSize = sampleSize; 790 sBitmapOptionsCache.inJustDecodeBounds = false; 791 Bitmap b = BitmapFactory.decodeFileDescriptor( 792 fd.getFileDescriptor(), null, sBitmapOptionsCache); 793 794 if (b != null) { 795 // finally rescale to exactly the size we need 796 if (sBitmapOptionsCache.outWidth != w || sBitmapOptionsCache.outHeight != h) { 797 Bitmap tmp = Bitmap.createScaledBitmap(b, w, h, true); 798 b.recycle(); 799 b = tmp; 800 } 801 } 802 803 return b; 804 } catch (FileNotFoundException e) { 805 } finally { 806 try { 807 if (fd != null) 808 fd.close(); 809 } catch (IOException e) { 810 } 811 } 812 } 813 return null; 814 } 815 816 // Get album art for specified album. You should not pass in the album id 817 // for the "unknown" album here (use -1 instead) 818 public static Bitmap getArtwork(Context context, int album_id) { 819 820 if (album_id < 0) { 821 // This is something that is not in the database, so get the album art directly 822 // from the file. 823 Bitmap bm = getArtworkFromFile(context, null, -1); 824 if (bm != null) { 825 return bm; 826 } 827 return getDefaultArtwork(context); 828 } 829 830 ContentResolver res = context.getContentResolver(); 831 Uri uri = ContentUris.withAppendedId(sArtworkUri, album_id); 832 if (uri != null) { 833 InputStream in = null; 834 try { 835 in = res.openInputStream(uri); 836 return BitmapFactory.decodeStream(in, null, sBitmapOptions); 837 } catch (FileNotFoundException ex) { 838 // The album art thumbnail does not actually exist. Maybe the user deleted it, or 839 // maybe it never existed to begin with. 840 Bitmap bm = getArtworkFromFile(context, null, album_id); 841 if (bm != null) { 842 // Put the newly found artwork in the database. 843 // Note that this shouldn't be done for the "unknown" album, 844 // but if this method is called correctly, that won't happen. 845 846 // first write it somewhere 847 String file = Environment.getExternalStorageDirectory() 848 + "/albumthumbs/" + String.valueOf(System.currentTimeMillis()); 849 if (ensureFileExists(file)) { 850 try { 851 OutputStream outstream = new FileOutputStream(file); 852 if (bm.getConfig() == null) { 853 bm = bm.copy(Bitmap.Config.RGB_565, false); 854 if (bm == null) { 855 return getDefaultArtwork(context); 856 } 857 } 858 boolean success = bm.compress(Bitmap.CompressFormat.JPEG, 75, outstream); 859 outstream.close(); 860 if (success) { 861 ContentValues values = new ContentValues(); 862 values.put("album_id", album_id); 863 values.put("_data", file); 864 Uri newuri = res.insert(sArtworkUri, values); 865 if (newuri == null) { 866 // Failed to insert in to the database. The most likely 867 // cause of this is that the item already existed in the 868 // database, and the most likely cause of that is that 869 // the album was scanned before, but the user deleted the 870 // album art from the sd card. 871 // We can ignore that case here, since the media provider 872 // will regenerate the album art for those entries when 873 // it detects this. 874 success = false; 875 } 876 } 877 if (!success) { 878 File f = new File(file); 879 f.delete(); 880 } 881 } catch (FileNotFoundException e) { 882 Log.e(TAG, "error creating file", e); 883 } catch (IOException e) { 884 Log.e(TAG, "error creating file", e); 885 } 886 } 887 } else { 888 bm = getDefaultArtwork(context); 889 } 890 return bm; 891 } finally { 892 try { 893 if (in != null) { 894 in.close(); 895 } 896 } catch (IOException ex) { 897 } 898 } 899 } 900 901 return null; 902 } 903 904 // copied from MediaProvider 905 private static boolean ensureFileExists(String path) { 906 File file = new File(path); 907 if (file.exists()) { 908 return true; 909 } else { 910 // we will not attempt to create the first directory in the path 911 // (for example, do not create /sdcard if the SD card is not mounted) 912 int secondSlash = path.indexOf('/', 1); 913 if (secondSlash < 1) return false; 914 String directoryPath = path.substring(0, secondSlash); 915 File directory = new File(directoryPath); 916 if (!directory.exists()) 917 return false; 918 file.getParentFile().mkdirs(); 919 try { 920 return file.createNewFile(); 921 } catch(IOException ioe) { 922 Log.e(TAG, "File creation failed", ioe); 923 } 924 return false; 925 } 926 } 927 928 // get album art for specified file 929 private static final String sExternalMediaUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString(); 930 private static Bitmap getArtworkFromFile(Context context, Uri uri, int albumid) { 931 Bitmap bm = null; 932 byte [] art = null; 933 String path = null; 934 935 if (sArtId == albumid) { 936 //Log.i("@@@@@@ ", "reusing cached data", new Exception()); 937 if (mCachedBit != null) { 938 return mCachedBit; 939 } 940 art = mCachedArt; 941 } else { 942 // try reading embedded artwork 943 if (uri == null) { 944 try { 945 int curalbum = sService.getAlbumId(); 946 if (curalbum == albumid || albumid < 0) { 947 path = sService.getPath(); 948 if (path != null) { 949 uri = Uri.parse(path); 950 } 951 } 952 } catch (RemoteException ex) { 953 } 954 } 955 if (uri == null) { 956 if (albumid >= 0) { 957 Cursor c = query(context,MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 958 new String[] { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.ALBUM }, 959 MediaStore.Audio.Media.ALBUM_ID + "=?", new String [] {String.valueOf(albumid)}, 960 null); 961 if (c != null) { 962 c.moveToFirst(); 963 if (!c.isAfterLast()) { 964 int trackid = c.getInt(0); 965 uri = ContentUris.withAppendedId( 966 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, trackid); 967 } 968 if (c.getString(1).equals(MediaFile.UNKNOWN_STRING)) { 969 albumid = -1; 970 } 971 c.close(); 972 } 973 } 974 } 975 if (uri != null) { 976 MediaScanner scanner = new MediaScanner(context); 977 ParcelFileDescriptor pfd = null; 978 try { 979 pfd = context.getContentResolver().openFileDescriptor(uri, "r"); 980 if (pfd != null) { 981 FileDescriptor fd = pfd.getFileDescriptor(); 982 art = scanner.extractAlbumArt(fd); 983 } 984 } catch (IOException ex) { 985 } catch (SecurityException ex) { 986 } finally { 987 try { 988 if (pfd != null) { 989 pfd.close(); 990 } 991 } catch (IOException ex) { 992 } 993 } 994 } 995 } 996 // if no embedded art exists, look for AlbumArt.jpg in same directory as the media file 997 if (art == null && path != null) { 998 if (path.startsWith(sExternalMediaUri)) { 999 // get the real path 1000 Cursor c = query(context,Uri.parse(path), 1001 new String[] { MediaStore.Audio.Media.DATA}, 1002 null, null, null); 1003 if (c != null) { 1004 c.moveToFirst(); 1005 if (!c.isAfterLast()) { 1006 path = c.getString(0); 1007 } 1008 c.close(); 1009 } 1010 } 1011 int lastSlash = path.lastIndexOf('/'); 1012 if (lastSlash > 0) { 1013 String artPath = path.substring(0, lastSlash + 1) + "AlbumArt.jpg"; 1014 File file = new File(artPath); 1015 if (file.exists()) { 1016 art = new byte[(int)file.length()]; 1017 FileInputStream stream = null; 1018 try { 1019 stream = new FileInputStream(file); 1020 stream.read(art); 1021 } catch (IOException ex) { 1022 art = null; 1023 } finally { 1024 try { 1025 if (stream != null) { 1026 stream.close(); 1027 } 1028 } catch (IOException ex) { 1029 } 1030 } 1031 } else { 1032 // TODO: try getting album art from the web 1033 } 1034 } 1035 } 1036 1037 if (art != null) { 1038 try { 1039 // get the size of the bitmap 1040 BitmapFactory.Options opts = new BitmapFactory.Options(); 1041 opts.inJustDecodeBounds = true; 1042 opts.inSampleSize = 1; 1043 BitmapFactory.decodeByteArray(art, 0, art.length, opts); 1044 1045 // request a reasonably sized output image 1046 // TODO: don't hardcode the size 1047 while (opts.outHeight > 320 || opts.outWidth > 320) { 1048 opts.outHeight /= 2; 1049 opts.outWidth /= 2; 1050 opts.inSampleSize *= 2; 1051 } 1052 1053 // get the image for real now 1054 opts.inJustDecodeBounds = false; 1055 bm = BitmapFactory.decodeByteArray(art, 0, art.length, opts); 1056 if (albumid != -1) { 1057 sArtId = albumid; 1058 } 1059 mCachedArt = art; 1060 mCachedBit = bm; 1061 } catch (Exception e) { 1062 } 1063 } 1064 return bm; 1065 } 1066 1067 private static Bitmap getDefaultArtwork(Context context) { 1068 BitmapFactory.Options opts = new BitmapFactory.Options(); 1069 opts.inPreferredConfig = Bitmap.Config.ARGB_8888; 1070 return BitmapFactory.decodeStream( 1071 context.getResources().openRawResource(R.drawable.albumart_mp_unknown), null, opts); 1072 } 1073 1074 static int getIntPref(Context context, String name, int def) { 1075 SharedPreferences prefs = 1076 context.getSharedPreferences("com.android.music", Context.MODE_PRIVATE); 1077 return prefs.getInt(name, def); 1078 } 1079 1080 static void setIntPref(Context context, String name, int value) { 1081 SharedPreferences prefs = 1082 context.getSharedPreferences("com.android.music", Context.MODE_PRIVATE); 1083 Editor ed = prefs.edit(); 1084 ed.putInt(name, value); 1085 ed.commit(); 1086 } 1087 1088 static void setRingtone(Context context, long id) { 1089 ContentResolver resolver = context.getContentResolver(); 1090 // Set the flag in the database to mark this as a ringtone 1091 Uri ringUri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id); 1092 try { 1093 ContentValues values = new ContentValues(2); 1094 values.put(MediaStore.Audio.Media.IS_RINGTONE, "1"); 1095 values.put(MediaStore.Audio.Media.IS_ALARM, "1"); 1096 resolver.update(ringUri, values, null, null); 1097 } catch (UnsupportedOperationException ex) { 1098 // most likely the card just got unmounted 1099 Log.e(TAG, "couldn't set ringtone flag for id " + id); 1100 return; 1101 } 1102 1103 String[] cols = new String[] { 1104 MediaStore.Audio.Media._ID, 1105 MediaStore.Audio.Media.DATA, 1106 MediaStore.Audio.Media.TITLE 1107 }; 1108 1109 String where = MediaStore.Audio.Media._ID + "=" + id; 1110 Cursor cursor = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 1111 cols, where , null, null); 1112 try { 1113 if (cursor != null && cursor.getCount() == 1) { 1114 // Set the system setting to make this the current ringtone 1115 cursor.moveToFirst(); 1116 Settings.System.putString(resolver, Settings.System.RINGTONE, ringUri.toString()); 1117 String message = context.getString(R.string.ringtone_set, cursor.getString(2)); 1118 Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); 1119 } 1120 } finally { 1121 if (cursor != null) { 1122 cursor.close(); 1123 } 1124 } 1125 } 1126} 1127