OSDN Git Service

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