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