OSDN Git Service

auto import from //depot/cupcake/@135843
[android-x86/packages-apps-Music.git] / src / com / android / music / MusicUtils.java
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
17 package com.android.music;
18
19 import java.io.File;
20 import java.io.FileDescriptor;
21 import java.io.FileInputStream;
22 import java.io.FileNotFoundException;
23 import java.io.FileOutputStream;
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.io.OutputStream;
27 import java.util.Arrays;
28 import java.util.Formatter;
29 import java.util.HashMap;
30 import java.util.Locale;
31
32 import android.app.Activity;
33 import android.app.ExpandableListActivity;
34 import android.content.ComponentName;
35 import android.content.ContentResolver;
36 import android.content.ContentUris;
37 import android.content.ContentValues;
38 import android.content.Context;
39 import android.content.Intent;
40 import android.content.ServiceConnection;
41 import android.content.SharedPreferences;
42 import android.content.SharedPreferences.Editor;
43 import android.content.res.Resources;
44 import android.database.Cursor;
45 import android.graphics.Bitmap;
46 import android.graphics.BitmapFactory;
47 import android.graphics.Canvas;
48 import android.graphics.ColorFilter;
49 import android.graphics.PixelFormat;
50 import android.graphics.drawable.BitmapDrawable;
51 import android.graphics.drawable.Drawable;
52 import android.media.MediaFile;
53 import android.media.MediaScanner;
54 import android.net.Uri;
55 import android.os.RemoteException;
56 import android.os.Environment;
57 import android.os.ParcelFileDescriptor;
58 import android.provider.MediaStore;
59 import android.provider.Settings;
60 import android.util.Log;
61 import android.view.SubMenu;
62 import android.view.View;
63 import android.view.Window;
64 import android.widget.TextView;
65 import android.widget.Toast;
66
67 public 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 }