OSDN Git Service

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