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