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