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