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