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