OSDN Git Service

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