OSDN Git Service

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