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