OSDN Git Service

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