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