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