OSDN Git Service

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