OSDN Git Service

resolved conflicts for merge of 7790a210 to master
[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 android.app.Activity;
20 import android.content.ComponentName;
21 import android.content.ContentResolver;
22 import android.content.ContentUris;
23 import android.content.ContentValues;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.ServiceConnection;
27 import android.content.SharedPreferences;
28 import android.content.SharedPreferences.Editor;
29 import android.content.res.Resources;
30 import android.database.Cursor;
31 import android.graphics.Bitmap;
32 import android.graphics.BitmapFactory;
33 import android.graphics.Canvas;
34 import android.graphics.ColorFilter;
35 import android.graphics.PixelFormat;
36 import android.graphics.drawable.BitmapDrawable;
37 import android.graphics.drawable.Drawable;
38 import android.net.Uri;
39 import android.os.Environment;
40 import android.os.ParcelFileDescriptor;
41 import android.os.RemoteException;
42 import android.provider.MediaStore;
43 import android.provider.Settings;
44 import android.util.Log;
45 import android.view.SubMenu;
46 import android.view.View;
47 import android.view.Window;
48 import android.widget.TextView;
49 import android.widget.Toast;
50
51 import java.io.File;
52 import java.io.FileDescriptor;
53 import java.io.FileNotFoundException;
54 import java.io.IOException;
55 import java.io.InputStream;
56 import java.util.Arrays;
57 import java.util.Formatter;
58 import java.util.HashMap;
59 import java.util.Locale;
60
61 public class MusicUtils {
62
63     private static final String TAG = "MusicUtils";
64
65     public interface Defs {
66         public final static int OPEN_URL = 0;
67         public final static int ADD_TO_PLAYLIST = 1;
68         public final static int USE_AS_RINGTONE = 2;
69         public final static int PLAYLIST_SELECTED = 3;
70         public final static int NEW_PLAYLIST = 4;
71         public final static int PLAY_SELECTION = 5;
72         public final static int GOTO_START = 6;
73         public final static int GOTO_PLAYBACK = 7;
74         public final static int PARTY_SHUFFLE = 8;
75         public final static int SHUFFLE_ALL = 9;
76         public final static int DELETE_ITEM = 10;
77         public final static int SCAN_DONE = 11;
78         public final static int QUEUE = 12;
79         public final static int CHILD_MENU_BASE = 13; // this should be the last item
80     }
81
82     public static String makeAlbumsLabel(Context context, int numalbums, int numsongs, boolean isUnknown) {
83         // There are two formats for the albums/songs information:
84         // "N Song(s)"  - used for unknown artist/album
85         // "N Album(s)" - used for known albums
86         
87         StringBuilder songs_albums = new StringBuilder();
88
89         Resources r = context.getResources();
90         if (isUnknown) {
91             if (numsongs == 1) {
92                 songs_albums.append(context.getString(R.string.onesong));
93             } else {
94                 String f = r.getQuantityText(R.plurals.Nsongs, numsongs).toString();
95                 sFormatBuilder.setLength(0);
96                 sFormatter.format(f, Integer.valueOf(numsongs));
97                 songs_albums.append(sFormatBuilder);
98             }
99         } else {
100             String f = r.getQuantityText(R.plurals.Nalbums, numalbums).toString();
101             sFormatBuilder.setLength(0);
102             sFormatter.format(f, Integer.valueOf(numalbums));
103             songs_albums.append(sFormatBuilder);
104             songs_albums.append(context.getString(R.string.albumsongseparator));
105         }
106         return songs_albums.toString();
107     }
108
109     /**
110      * This is now only used for the query screen
111      */
112     public static String makeAlbumsSongsLabel(Context context, int numalbums, int numsongs, boolean isUnknown) {
113         // There are several formats for the albums/songs information:
114         // "1 Song"   - used if there is only 1 song
115         // "N Songs" - used for the "unknown artist" item
116         // "1 Album"/"N Songs" 
117         // "N Album"/"M Songs"
118         // Depending on locale, these may need to be further subdivided
119         
120         StringBuilder songs_albums = new StringBuilder();
121
122         if (numsongs == 1) {
123             songs_albums.append(context.getString(R.string.onesong));
124         } else {
125             Resources r = context.getResources();
126             if (! isUnknown) {
127                 String f = r.getQuantityText(R.plurals.Nalbums, numalbums).toString();
128                 sFormatBuilder.setLength(0);
129                 sFormatter.format(f, Integer.valueOf(numalbums));
130                 songs_albums.append(sFormatBuilder);
131                 songs_albums.append(context.getString(R.string.albumsongseparator));
132             }
133             String f = r.getQuantityText(R.plurals.Nsongs, numsongs).toString();
134             sFormatBuilder.setLength(0);
135             sFormatter.format(f, Integer.valueOf(numsongs));
136             songs_albums.append(sFormatBuilder);
137         }
138         return songs_albums.toString();
139     }
140     
141     public static IMediaPlaybackService sService = null;
142     private static HashMap<Context, ServiceBinder> sConnectionMap = new HashMap<Context, ServiceBinder>();
143
144     public static boolean bindToService(Context context) {
145         return bindToService(context, null);
146     }
147
148     public static boolean bindToService(Context context, ServiceConnection callback) {
149         context.startService(new Intent(context, MediaPlaybackService.class));
150         ServiceBinder sb = new ServiceBinder(callback);
151         sConnectionMap.put(context, sb);
152         return context.bindService((new Intent()).setClass(context,
153                 MediaPlaybackService.class), sb, 0);
154     }
155     
156     public static void unbindFromService(Context context) {
157         ServiceBinder sb = (ServiceBinder) sConnectionMap.remove(context);
158         if (sb == null) {
159             Log.e("MusicUtils", "Trying to unbind for unknown Context");
160             return;
161         }
162         context.unbindService(sb);
163         if (sConnectionMap.isEmpty()) {
164             // presumably there is nobody interested in the service at this point,
165             // so don't hang on to the ServiceConnection
166             sService = null;
167         }
168     }
169
170     private static class ServiceBinder implements ServiceConnection {
171         ServiceConnection mCallback;
172         ServiceBinder(ServiceConnection callback) {
173             mCallback = callback;
174         }
175         
176         public void onServiceConnected(ComponentName className, android.os.IBinder service) {
177             sService = IMediaPlaybackService.Stub.asInterface(service);
178             initAlbumArtCache();
179             if (mCallback != null) {
180                 mCallback.onServiceConnected(className, service);
181             }
182         }
183         
184         public void onServiceDisconnected(ComponentName className) {
185             if (mCallback != null) {
186                 mCallback.onServiceDisconnected(className);
187             }
188             sService = null;
189         }
190     }
191     
192     public static long getCurrentAlbumId() {
193         if (sService != null) {
194             try {
195                 return sService.getAlbumId();
196             } catch (RemoteException ex) {
197             }
198         }
199         return -1;
200     }
201
202     public static long getCurrentArtistId() {
203         if (MusicUtils.sService != null) {
204             try {
205                 return sService.getArtistId();
206             } catch (RemoteException ex) {
207             }
208         }
209         return -1;
210     }
211
212     public static long getCurrentAudioId() {
213         if (MusicUtils.sService != null) {
214             try {
215                 return sService.getAudioId();
216             } catch (RemoteException ex) {
217             }
218         }
219         return -1;
220     }
221     
222     public static int getCurrentShuffleMode() {
223         int mode = MediaPlaybackService.SHUFFLE_NONE;
224         if (sService != null) {
225             try {
226                 mode = sService.getShuffleMode();
227             } catch (RemoteException ex) {
228             }
229         }
230         return mode;
231     }
232     
233     /*
234      * Returns true if a file is currently opened for playback (regardless
235      * of whether it's playing or paused).
236      */
237     public static boolean isMusicLoaded() {
238         if (MusicUtils.sService != null) {
239             try {
240                 return sService.getPath() != null;
241             } catch (RemoteException ex) {
242             }
243         }
244         return false;
245     }
246
247     private final static long [] sEmptyList = new long[0];
248     
249     public static long [] getSongListForCursor(Cursor cursor) {
250         if (cursor == null) {
251             return sEmptyList;
252         }
253         int len = cursor.getCount();
254         long [] list = new long[len];
255         cursor.moveToFirst();
256         int colidx = -1;
257         try {
258             colidx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.Members.AUDIO_ID);
259         } catch (IllegalArgumentException ex) {
260             colidx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID);
261         }
262         for (int i = 0; i < len; i++) {
263             list[i] = cursor.getLong(colidx);
264             cursor.moveToNext();
265         }
266         return list;
267     }
268
269     public static long [] getSongListForArtist(Context context, long id) {
270         final String[] ccols = new String[] { MediaStore.Audio.Media._ID };
271         String where = MediaStore.Audio.Media.ARTIST_ID + "=" + id + " AND " + 
272         MediaStore.Audio.Media.IS_MUSIC + "=1";
273         Cursor cursor = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
274                 ccols, where, null,
275                 MediaStore.Audio.Media.ALBUM_KEY + ","  + MediaStore.Audio.Media.TRACK);
276         
277         if (cursor != null) {
278             long [] list = getSongListForCursor(cursor);
279             cursor.close();
280             return list;
281         }
282         return sEmptyList;
283     }
284
285     public static long [] getSongListForAlbum(Context context, long id) {
286         final String[] ccols = new String[] { MediaStore.Audio.Media._ID };
287         String where = MediaStore.Audio.Media.ALBUM_ID + "=" + id + " AND " + 
288                 MediaStore.Audio.Media.IS_MUSIC + "=1";
289         Cursor cursor = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
290                 ccols, where, null, MediaStore.Audio.Media.TRACK);
291
292         if (cursor != null) {
293             long [] list = getSongListForCursor(cursor);
294             cursor.close();
295             return list;
296         }
297         return sEmptyList;
298     }
299
300     public static long [] getSongListForPlaylist(Context context, long plid) {
301         final String[] ccols = new String[] { MediaStore.Audio.Playlists.Members.AUDIO_ID };
302         Cursor cursor = query(context, MediaStore.Audio.Playlists.Members.getContentUri("external", plid),
303                 ccols, null, null, MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER);
304         
305         if (cursor != null) {
306             long [] list = getSongListForCursor(cursor);
307             cursor.close();
308             return list;
309         }
310         return sEmptyList;
311     }
312     
313     public static void playPlaylist(Context context, long plid) {
314         long [] list = getSongListForPlaylist(context, plid);
315         if (list != null) {
316             playAll(context, list, -1, false);
317         }
318     }
319
320     public static long [] getAllSongs(Context context) {
321         Cursor c = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
322                 new String[] {MediaStore.Audio.Media._ID}, MediaStore.Audio.Media.IS_MUSIC + "=1",
323                 null, null);
324         try {
325             if (c == null || c.getCount() == 0) {
326                 return null;
327             }
328             int len = c.getCount();
329             long [] list = new long[len];
330             for (int i = 0; i < len; i++) {
331                 c.moveToNext();
332                 list[i] = c.getLong(0);
333             }
334
335             return list;
336         } finally {
337             if (c != null) {
338                 c.close();
339             }
340         }
341     }
342
343     /**
344      * Fills out the given submenu with items for "new playlist" and
345      * any existing playlists. When the user selects an item, the
346      * application will receive PLAYLIST_SELECTED with the Uri of
347      * the selected playlist, NEW_PLAYLIST if a new playlist
348      * should be created, and QUEUE if the "current playlist" was
349      * selected.
350      * @param context The context to use for creating the menu items
351      * @param sub The submenu to add the items to.
352      */
353     public static void makePlaylistMenu(Context context, SubMenu sub) {
354         String[] cols = new String[] {
355                 MediaStore.Audio.Playlists._ID,
356                 MediaStore.Audio.Playlists.NAME
357         };
358         ContentResolver resolver = context.getContentResolver();
359         if (resolver == null) {
360             System.out.println("resolver = null");
361         } else {
362             String whereclause = MediaStore.Audio.Playlists.NAME + " != ''";
363             Cursor cur = resolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
364                 cols, whereclause, null,
365                 MediaStore.Audio.Playlists.NAME);
366             sub.clear();
367             sub.add(1, Defs.QUEUE, 0, R.string.queue);
368             sub.add(1, Defs.NEW_PLAYLIST, 0, R.string.new_playlist);
369             if (cur != null && cur.getCount() > 0) {
370                 //sub.addSeparator(1, 0);
371                 cur.moveToFirst();
372                 while (! cur.isAfterLast()) {
373                     Intent intent = new Intent();
374                     intent.putExtra("playlist", cur.getLong(0));
375 //                    if (cur.getInt(0) == mLastPlaylistSelected) {
376 //                        sub.add(0, MusicBaseActivity.PLAYLIST_SELECTED, cur.getString(1)).setIntent(intent);
377 //                    } else {
378                         sub.add(1, Defs.PLAYLIST_SELECTED, 0, cur.getString(1)).setIntent(intent);
379 //                    }
380                     cur.moveToNext();
381                 }
382             }
383             if (cur != null) {
384                 cur.close();
385             }
386         }
387     }
388
389     public static void clearPlaylist(Context context, int plid) {
390         
391         Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", plid);
392         context.getContentResolver().delete(uri, null, null);
393         return;
394     }
395     
396     public static void deleteTracks(Context context, long [] list) {
397         
398         String [] cols = new String [] { MediaStore.Audio.Media._ID, 
399                 MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.ALBUM_ID };
400         StringBuilder where = new StringBuilder();
401         where.append(MediaStore.Audio.Media._ID + " IN (");
402         for (int i = 0; i < list.length; i++) {
403             where.append(list[i]);
404             if (i < list.length - 1) {
405                 where.append(",");
406             }
407         }
408         where.append(")");
409         Cursor c = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, cols,
410                 where.toString(), null, null);
411
412         if (c != null) {
413
414             // step 1: remove selected tracks from the current playlist, as well
415             // as from the album art cache
416             try {
417                 c.moveToFirst();
418                 while (! c.isAfterLast()) {
419                     // remove from current playlist
420                     long id = c.getLong(0);
421                     sService.removeTrack(id);
422                     // remove from album art cache
423                     long artIndex = c.getLong(2);
424                     synchronized(sArtCache) {
425                         sArtCache.remove(artIndex);
426                     }
427                     c.moveToNext();
428                 }
429             } catch (RemoteException ex) {
430             }
431
432             // step 2: remove selected tracks from the database
433             context.getContentResolver().delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, where.toString(), null);
434
435             // step 3: remove files from card
436             c.moveToFirst();
437             while (! c.isAfterLast()) {
438                 String name = c.getString(1);
439                 File f = new File(name);
440                 try {  // File.delete can throw a security exception
441                     if (!f.delete()) {
442                         // I'm not sure if we'd ever get here (deletion would
443                         // have to fail, but no exception thrown)
444                         Log.e("MusicUtils", "Failed to delete file " + name);
445                     }
446                     c.moveToNext();
447                 } catch (SecurityException ex) {
448                     c.moveToNext();
449                 }
450             }
451             c.close();
452         }
453
454         String message = context.getResources().getQuantityString(
455                 R.plurals.NNNtracksdeleted, list.length, Integer.valueOf(list.length));
456         
457         Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
458         // We deleted a number of tracks, which could affect any number of things
459         // in the media content domain, so update everything.
460         context.getContentResolver().notifyChange(Uri.parse("content://media"), null);
461     }
462     
463     public static void addToCurrentPlaylist(Context context, long [] list) {
464         if (sService == null) {
465             return;
466         }
467         try {
468             sService.enqueue(list, MediaPlaybackService.LAST);
469             String message = context.getResources().getQuantityString(
470                     R.plurals.NNNtrackstoplaylist, list.length, Integer.valueOf(list.length));
471             Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
472         } catch (RemoteException ex) {
473         }
474     }
475     
476     public static void addToPlaylist(Context context, long [] ids, long playlistid) {
477         if (ids == null) {
478             // this shouldn't happen (the menuitems shouldn't be visible
479             // unless the selected item represents something playable
480             Log.e("MusicBase", "ListSelection null");
481         } else {
482             int size = ids.length;
483             ContentValues values [] = new ContentValues[size];
484             ContentResolver resolver = context.getContentResolver();
485             // need to determine the number of items currently in the playlist,
486             // so the play_order field can be maintained.
487             String[] cols = new String[] {
488                     "count(*)"
489             };
490             Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistid);
491             Cursor cur = resolver.query(uri, cols, null, null, null);
492             cur.moveToFirst();
493             int base = cur.getInt(0);
494             cur.close();
495
496             for (int i = 0; i < size; i++) {
497                 values[i] = new ContentValues();
498                 values[i].put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(base + i));
499                 values[i].put(MediaStore.Audio.Playlists.Members.AUDIO_ID, ids[i]);
500             }
501             resolver.bulkInsert(uri, values);
502             String message = context.getResources().getQuantityString(
503                     R.plurals.NNNtrackstoplaylist, size, Integer.valueOf(size));
504             Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
505             //mLastPlaylistSelected = playlistid;
506         }
507     }
508
509     public static Cursor query(Context context, Uri uri, String[] projection,
510             String selection, String[] selectionArgs, String sortOrder, int limit) {
511         try {
512             ContentResolver resolver = context.getContentResolver();
513             if (resolver == null) {
514                 return null;
515             }
516             if (limit > 0) {
517                 uri = uri.buildUpon().appendQueryParameter("limit", "" + limit).build();
518             }
519             return resolver.query(uri, projection, selection, selectionArgs, sortOrder);
520          } catch (UnsupportedOperationException ex) {
521             return null;
522         }
523         
524     }
525     public static Cursor query(Context context, Uri uri, String[] projection,
526             String selection, String[] selectionArgs, String sortOrder) {
527         return query(context, uri, projection, selection, selectionArgs, sortOrder, 0);
528     }
529     
530     public static boolean isMediaScannerScanning(Context context) {
531         boolean result = false;
532         Cursor cursor = query(context, MediaStore.getMediaScannerUri(), 
533                 new String [] { MediaStore.MEDIA_SCANNER_VOLUME }, null, null, null);
534         if (cursor != null) {
535             if (cursor.getCount() == 1) {
536                 cursor.moveToFirst();
537                 result = "external".equals(cursor.getString(0));
538             }
539             cursor.close(); 
540         } 
541
542         return result;
543     }
544     
545     public static void setSpinnerState(Activity a) {
546         if (isMediaScannerScanning(a)) {
547             // start the progress spinner
548             a.getWindow().setFeatureInt(
549                     Window.FEATURE_INDETERMINATE_PROGRESS,
550                     Window.PROGRESS_INDETERMINATE_ON);
551
552             a.getWindow().setFeatureInt(
553                     Window.FEATURE_INDETERMINATE_PROGRESS,
554                     Window.PROGRESS_VISIBILITY_ON);
555         } else {
556             // stop the progress spinner
557             a.getWindow().setFeatureInt(
558                     Window.FEATURE_INDETERMINATE_PROGRESS,
559                     Window.PROGRESS_VISIBILITY_OFF);
560         }
561     }
562     
563     public static void displayDatabaseError(Activity a) {
564         String status = Environment.getExternalStorageState();
565         int title = R.string.sdcard_error_title;
566         int message = R.string.sdcard_error_message;
567         
568         if (status.equals(Environment.MEDIA_SHARED) ||
569                 status.equals(Environment.MEDIA_UNMOUNTED)) {
570             title = R.string.sdcard_busy_title;
571             message = R.string.sdcard_busy_message;
572         } else if (status.equals(Environment.MEDIA_REMOVED)) {
573             title = R.string.sdcard_missing_title;
574             message = R.string.sdcard_missing_message;
575         } else if (status.equals(Environment.MEDIA_MOUNTED)){
576             // The card is mounted, but we didn't get a valid cursor.
577             // This probably means the mediascanner hasn't started scanning the
578             // card yet (there is a small window of time during boot where this
579             // will happen).
580             a.setTitle("");
581             Intent intent = new Intent();
582             intent.setClass(a, ScanningProgress.class);
583             a.startActivityForResult(intent, Defs.SCAN_DONE);
584         } else {
585             Log.d(TAG, "sd card: " + status);
586         }
587
588         a.setTitle(title);
589         View v = a.findViewById(R.id.sd_message);
590         if (v != null) {
591             v.setVisibility(View.VISIBLE);
592         }
593         v = a.findViewById(R.id.sd_icon);
594         if (v != null) {
595             v.setVisibility(View.VISIBLE);
596         }
597         v = a.findViewById(android.R.id.list);
598         if (v != null) {
599             v.setVisibility(View.GONE);
600         }
601         TextView tv = (TextView) a.findViewById(R.id.sd_message);
602         tv.setText(message);
603     }
604     
605     public static void hideDatabaseError(Activity a) {
606         View v = a.findViewById(R.id.sd_message);
607         if (v != null) {
608             v.setVisibility(View.GONE);
609         }
610         v = a.findViewById(R.id.sd_icon);
611         if (v != null) {
612             v.setVisibility(View.GONE);
613         }
614         v = a.findViewById(android.R.id.list);
615         if (v != null) {
616             v.setVisibility(View.VISIBLE);
617         }
618     }
619
620     static protected Uri getContentURIForPath(String path) {
621         return Uri.fromFile(new File(path));
622     }
623
624     
625     /*  Try to use String.format() as little as possible, because it creates a
626      *  new Formatter every time you call it, which is very inefficient.
627      *  Reusing an existing Formatter more than tripled the speed of
628      *  makeTimeString().
629      *  This Formatter/StringBuilder are also used by makeAlbumSongsLabel()
630      */
631     private static StringBuilder sFormatBuilder = new StringBuilder();
632     private static Formatter sFormatter = new Formatter(sFormatBuilder, Locale.getDefault());
633     private static final Object[] sTimeArgs = new Object[5];
634
635     public static String makeTimeString(Context context, long secs) {
636         String durationformat = context.getString(R.string.durationformat);
637         
638         /* Provide multiple arguments so the format can be changed easily
639          * by modifying the xml.
640          */
641         sFormatBuilder.setLength(0);
642
643         final Object[] timeArgs = sTimeArgs;
644         timeArgs[0] = secs / 3600;
645         timeArgs[1] = secs / 60;
646         timeArgs[2] = (secs / 60) % 60;
647         timeArgs[3] = secs;
648         timeArgs[4] = secs % 60;
649
650         return sFormatter.format(durationformat, timeArgs).toString();
651     }
652     
653     public static void shuffleAll(Context context, Cursor cursor) {
654         playAll(context, cursor, 0, true);
655     }
656
657     public static void playAll(Context context, Cursor cursor) {
658         playAll(context, cursor, 0, false);
659     }
660     
661     public static void playAll(Context context, Cursor cursor, int position) {
662         playAll(context, cursor, position, false);
663     }
664     
665     public static void playAll(Context context, long [] list, int position) {
666         playAll(context, list, position, false);
667     }
668     
669     private static void playAll(Context context, Cursor cursor, int position, boolean force_shuffle) {
670     
671         long [] list = getSongListForCursor(cursor);
672         playAll(context, list, position, force_shuffle);
673     }
674     
675     private static void playAll(Context context, long [] list, int position, boolean force_shuffle) {
676         if (list.length == 0 || sService == null) {
677             Log.d("MusicUtils", "attempt to play empty song list");
678             // Don't try to play empty playlists. Nothing good will come of it.
679             String message = context.getString(R.string.emptyplaylist, list.length);
680             Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
681             return;
682         }
683         try {
684             if (force_shuffle) {
685                 sService.setShuffleMode(MediaPlaybackService.SHUFFLE_NORMAL);
686             }
687             long curid = sService.getAudioId();
688             int curpos = sService.getQueuePosition();
689             if (position != -1 && curpos == position && curid == list[position]) {
690                 // The selected file is the file that's currently playing;
691                 // figure out if we need to restart with a new playlist,
692                 // or just launch the playback activity.
693                 long [] playlist = sService.getQueue();
694                 if (Arrays.equals(list, playlist)) {
695                     // we don't need to set a new list, but we should resume playback if needed
696                     sService.play();
697                     return; // the 'finally' block will still run
698                 }
699             }
700             if (position < 0) {
701                 position = 0;
702             }
703             sService.open(list, force_shuffle ? -1 : position);
704             sService.play();
705         } catch (RemoteException ex) {
706         } finally {
707             Intent intent = new Intent("com.android.music.PLAYBACK_VIEWER")
708                 .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
709             context.startActivity(intent);
710         }
711     }
712     
713     public static void clearQueue() {
714         try {
715             sService.removeTracks(0, Integer.MAX_VALUE);
716         } catch (RemoteException ex) {
717         }
718     }
719     
720     // A really simple BitmapDrawable-like class, that doesn't do
721     // scaling, dithering or filtering.
722     private static class FastBitmapDrawable extends Drawable {
723         private Bitmap mBitmap;
724         public FastBitmapDrawable(Bitmap b) {
725             mBitmap = b;
726         }
727         @Override
728         public void draw(Canvas canvas) {
729             canvas.drawBitmap(mBitmap, 0, 0, null);
730         }
731         @Override
732         public int getOpacity() {
733             return PixelFormat.OPAQUE;
734         }
735         @Override
736         public void setAlpha(int alpha) {
737         }
738         @Override
739         public void setColorFilter(ColorFilter cf) {
740         }
741     }
742     
743     private static int sArtId = -2;
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<Long, Drawable> sArtCache = new HashMap<Long, 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, long 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, long 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, long song_id, long album_id) {
869
870         if (album_id < 0) {
871             // This is something that is not in the database, so get the album art directly
872             // from the file.
873             if (song_id >= 0) {
874                 Bitmap bm = getArtworkFromFile(context, song_id, -1);
875                 if (bm != null) {
876                     return bm;
877                 }
878             }
879             return getDefaultArtwork(context);
880         }
881
882         ContentResolver res = context.getContentResolver();
883         Uri uri = ContentUris.withAppendedId(sArtworkUri, album_id);
884         if (uri != null) {
885             InputStream in = null;
886             try {
887                 in = res.openInputStream(uri);
888                 return BitmapFactory.decodeStream(in, null, sBitmapOptions);
889             } catch (FileNotFoundException ex) {
890                 // The album art thumbnail does not actually exist. Maybe the user deleted it, or
891                 // maybe it never existed to begin with.
892                 Bitmap bm = getArtworkFromFile(context, song_id, album_id);
893                 if (bm != null) {
894                     if (bm.getConfig() == null) {
895                         bm = bm.copy(Bitmap.Config.RGB_565, false);
896                         if (bm == null) {
897                             return getDefaultArtwork(context);
898                         }
899                     }
900                 } else {
901                     bm = getDefaultArtwork(context);
902                 }
903                 return bm;
904             } finally {
905                 try {
906                     if (in != null) {
907                         in.close();
908                     }
909                 } catch (IOException ex) {
910                 }
911             }
912         }
913         
914         return null;
915     }
916     
917     // get album art for specified file
918     private static final String sExternalMediaUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString();
919     private static Bitmap getArtworkFromFile(Context context, long songid, long albumid) {
920         Bitmap bm = null;
921         byte [] art = null;
922         String path = null;
923
924         if (albumid < 0 && songid < 0) {
925             throw new IllegalArgumentException("Must specify an album or a song id");
926         }
927
928         try {
929             if (albumid < 0) {
930                 Uri uri = Uri.parse("content://media/external/audio/media/" + songid + "/albumart");
931                 ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
932                 if (pfd != null) {
933                     FileDescriptor fd = pfd.getFileDescriptor();
934                     bm = BitmapFactory.decodeFileDescriptor(fd);
935                 }
936             } else {
937                 Uri uri = ContentUris.withAppendedId(sArtworkUri, albumid);
938                 ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
939                 if (pfd != null) {
940                     FileDescriptor fd = pfd.getFileDescriptor();
941                     bm = BitmapFactory.decodeFileDescriptor(fd);
942                 }
943             }
944         } catch (FileNotFoundException ex) {
945             //
946         }
947         if (bm != null) {
948             mCachedBit = bm;
949         }
950         return bm;
951     }
952     
953     private static Bitmap getDefaultArtwork(Context context) {
954         BitmapFactory.Options opts = new BitmapFactory.Options();
955         opts.inPreferredConfig = Bitmap.Config.ARGB_8888;
956         return BitmapFactory.decodeStream(
957                 context.getResources().openRawResource(R.drawable.albumart_mp_unknown), null, opts);
958     }
959     
960     static int getIntPref(Context context, String name, int def) {
961         SharedPreferences prefs =
962             context.getSharedPreferences("com.android.music", Context.MODE_PRIVATE);
963         return prefs.getInt(name, def);
964     }
965     
966     static void setIntPref(Context context, String name, int value) {
967         SharedPreferences prefs =
968             context.getSharedPreferences("com.android.music", Context.MODE_PRIVATE);
969         Editor ed = prefs.edit();
970         ed.putInt(name, value);
971         ed.commit();
972     }
973
974     static void setRingtone(Context context, long id) {
975         ContentResolver resolver = context.getContentResolver();
976         // Set the flag in the database to mark this as a ringtone
977         Uri ringUri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
978         try {
979             ContentValues values = new ContentValues(2);
980             values.put(MediaStore.Audio.Media.IS_RINGTONE, "1");
981             values.put(MediaStore.Audio.Media.IS_ALARM, "1");
982             resolver.update(ringUri, values, null, null);
983         } catch (UnsupportedOperationException ex) {
984             // most likely the card just got unmounted
985             Log.e(TAG, "couldn't set ringtone flag for id " + id);
986             return;
987         }
988
989         String[] cols = new String[] {
990                 MediaStore.Audio.Media._ID,
991                 MediaStore.Audio.Media.DATA,
992                 MediaStore.Audio.Media.TITLE
993         };
994
995         String where = MediaStore.Audio.Media._ID + "=" + id;
996         Cursor cursor = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
997                 cols, where , null, null);
998         try {
999             if (cursor != null && cursor.getCount() == 1) {
1000                 // Set the system setting to make this the current ringtone
1001                 cursor.moveToFirst();
1002                 Settings.System.putString(resolver, Settings.System.RINGTONE, ringUri.toString());
1003                 String message = context.getString(R.string.ringtone_set, cursor.getString(2));
1004                 Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
1005             }
1006         } finally {
1007             if (cursor != null) {
1008                 cursor.close();
1009             }
1010         }
1011     }
1012 }