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