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