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