OSDN Git Service

Eleven: remove an unnecessary check when playing music to improve perf
[android-x86/packages-apps-Eleven.git] / src / com / cyanogenmod / eleven / utils / MusicUtils.java
1 /*
2  * Copyright (C) 2012 Andrew Neal
3  * Copyright (C) 2014 The CyanogenMod Project
4  * Licensed under the Apache License, Version 2.0
5  * (the "License"); you may not use this file except in compliance with the
6  * License. You may obtain a copy of the License at
7  * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
8  * or agreed to in writing, software distributed under the License is
9  * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
10  * KIND, either express or implied. See the License for the specific language
11  * governing permissions and limitations under the License.
12  */
13
14 package com.cyanogenmod.eleven.utils;
15
16 import android.app.Activity;
17 import android.content.ComponentName;
18 import android.content.ContentResolver;
19 import android.content.ContentUris;
20 import android.content.ContentValues;
21 import android.content.Context;
22 import android.content.ContextWrapper;
23 import android.content.Intent;
24 import android.content.ServiceConnection;
25 import android.database.Cursor;
26 import android.net.Uri;
27 import android.os.AsyncTask;
28 import android.os.IBinder;
29 import android.os.RemoteException;
30 import android.os.SystemClock;
31 import android.provider.BaseColumns;
32 import android.provider.MediaStore;
33 import android.provider.MediaStore.Audio.AlbumColumns;
34 import android.provider.MediaStore.Audio.ArtistColumns;
35 import android.provider.MediaStore.Audio.AudioColumns;
36 import android.provider.MediaStore.Audio.Playlists;
37 import android.provider.MediaStore.Audio.PlaylistsColumns;
38 import android.provider.MediaStore.MediaColumns;
39 import android.provider.Settings;
40 import android.util.Log;
41 import android.view.Menu;
42
43 import com.cyanogenmod.eleven.Config.IdType;
44 import com.cyanogenmod.eleven.Config.SmartPlaylistType;
45 import com.cyanogenmod.eleven.IElevenService;
46 import com.cyanogenmod.eleven.MusicPlaybackService;
47 import com.cyanogenmod.eleven.R;
48 import com.cyanogenmod.eleven.cache.ImageFetcher;
49 import com.cyanogenmod.eleven.loaders.LastAddedLoader;
50 import com.cyanogenmod.eleven.loaders.PlaylistLoader;
51 import com.cyanogenmod.eleven.loaders.PlaylistSongLoader;
52 import com.cyanogenmod.eleven.loaders.SongLoader;
53 import com.cyanogenmod.eleven.loaders.TopTracksLoader;
54 import com.cyanogenmod.eleven.locale.LocaleUtils;
55 import com.cyanogenmod.eleven.menu.FragmentMenuItems;
56 import com.cyanogenmod.eleven.model.Album;
57 import com.cyanogenmod.eleven.model.AlbumArtistDetails;
58 import com.cyanogenmod.eleven.model.Artist;
59 import com.cyanogenmod.eleven.model.Song;
60 import com.cyanogenmod.eleven.provider.RecentStore;
61 import com.cyanogenmod.eleven.provider.SongPlayCount;
62 import com.cyanogenmod.eleven.service.MusicPlaybackTrack;
63
64 import java.io.File;
65 import java.util.Arrays;
66 import java.util.Collection;
67 import java.util.Iterator;
68 import java.util.WeakHashMap;
69
70 /**
71  * A collection of helpers directly related to music or Apollo's service.
72  *
73  * @author Andrew Neal (andrewdneal@gmail.com)
74  */
75 public final class MusicUtils {
76
77     public static IElevenService mService = null;
78
79     private static final WeakHashMap<Context, ServiceBinder> mConnectionMap;
80
81     private static final long[] sEmptyList;
82
83     private static ContentValues[] mContentValuesCache = null;
84
85     private static final int MIN_VALID_YEAR = 1900; // used to remove invalid years from metadata
86
87     public static final String MUSIC_ONLY_SELECTION = MediaStore.Audio.AudioColumns.IS_MUSIC + "=1"
88                     + " AND " + MediaStore.Audio.AudioColumns.TITLE + " != ''"; //$NON-NLS-2$
89
90     static {
91         mConnectionMap = new WeakHashMap<Context, ServiceBinder>();
92         sEmptyList = new long[0];
93     }
94
95     /* This class is never initiated */
96     public MusicUtils() {
97     }
98
99     /**
100      * @param context The {@link Context} to use
101      * @param callback The {@link ServiceConnection} to use
102      * @return The new instance of {@link ServiceToken}
103      */
104     public static final ServiceToken bindToService(final Context context,
105             final ServiceConnection callback) {
106         Activity realActivity = ((Activity)context).getParent();
107         if (realActivity == null) {
108             realActivity = (Activity)context;
109         }
110         final ContextWrapper contextWrapper = new ContextWrapper(realActivity);
111         contextWrapper.startService(new Intent(contextWrapper, MusicPlaybackService.class));
112         final ServiceBinder binder = new ServiceBinder(callback,
113                 contextWrapper.getApplicationContext());
114         if (contextWrapper.bindService(
115                 new Intent().setClass(contextWrapper, MusicPlaybackService.class), binder, 0)) {
116             mConnectionMap.put(contextWrapper, binder);
117             return new ServiceToken(contextWrapper);
118         }
119         return null;
120     }
121
122     /**
123      * @param token The {@link ServiceToken} to unbind from
124      */
125     public static void unbindFromService(final ServiceToken token) {
126         if (token == null) {
127             return;
128         }
129         final ContextWrapper mContextWrapper = token.mWrappedContext;
130         final ServiceBinder mBinder = mConnectionMap.remove(mContextWrapper);
131         if (mBinder == null) {
132             return;
133         }
134         mContextWrapper.unbindService(mBinder);
135         if (mConnectionMap.isEmpty()) {
136             mService = null;
137         }
138     }
139
140     public static final class ServiceBinder implements ServiceConnection {
141         private final ServiceConnection mCallback;
142         private final Context mContext;
143
144         /**
145          * Constructor of <code>ServiceBinder</code>
146          *
147          * @param context The {@link ServiceConnection} to use
148          */
149         public ServiceBinder(final ServiceConnection callback, final Context context) {
150             mCallback = callback;
151             mContext = context;
152         }
153
154         @Override
155         public void onServiceConnected(final ComponentName className, final IBinder service) {
156             mService = IElevenService.Stub.asInterface(service);
157             if (mCallback != null) {
158                 mCallback.onServiceConnected(className, service);
159             }
160             MusicUtils.initPlaybackServiceWithSettings(mContext);
161         }
162
163         @Override
164         public void onServiceDisconnected(final ComponentName className) {
165             if (mCallback != null) {
166                 mCallback.onServiceDisconnected(className);
167             }
168             mService = null;
169         }
170     }
171
172     public static final class ServiceToken {
173         public ContextWrapper mWrappedContext;
174
175         /**
176          * Constructor of <code>ServiceToken</code>
177          *
178          * @param context The {@link ContextWrapper} to use
179          */
180         public ServiceToken(final ContextWrapper context) {
181             mWrappedContext = context;
182         }
183     }
184
185     public static final boolean isPlaybackServiceConnected() {
186         return mService != null;
187     }
188
189     /**
190      * Used to make number of labels for the number of artists, albums, songs,
191      * genres, and playlists.
192      *
193      * @param context The {@link Context} to use.
194      * @param pluralInt The ID of the plural string to use.
195      * @param number The number of artists, albums, songs, genres, or playlists.
196      * @return A {@link String} used as a label for the number of artists,
197      *         albums, songs, genres, and playlists.
198      */
199     public static final String makeLabel(final Context context, final int pluralInt,
200             final int number) {
201         return context.getResources().getQuantityString(pluralInt, number, number);
202     }
203
204     /**
205      * * Used to create a formatted time string for the duration of tracks.
206      *
207      * @param context The {@link Context} to use.
208      * @param secs The track in seconds.
209      * @return Duration of a track that's properly formatted.
210      */
211     public static final String makeShortTimeString(final Context context, long secs) {
212         long hours, mins;
213
214         hours = secs / 3600;
215         secs %= 3600;
216         mins = secs / 60;
217         secs %= 60;
218
219         final String durationFormat = context.getResources().getString(
220                 hours == 0 ? R.string.durationformatshort : R.string.durationformatlong);
221         return String.format(durationFormat, hours, mins, secs);
222     }
223
224     /**
225      * Used to create a formatted time string in the format of #h #m or #m if there is only minutes
226      *
227      * @param context The {@link Context} to use.
228      * @param secs The duration seconds.
229      * @return Duration properly formatted in #h #m format
230      */
231     public static final String makeLongTimeString(final Context context, long secs) {
232         long hours, mins;
233
234         hours = secs / 3600;
235         secs %= 3600;
236         mins = secs / 60;
237
238         String hoursString = MusicUtils.makeLabel(context, R.plurals.Nhours, (int)hours);
239         String minutesString = MusicUtils.makeLabel(context, R.plurals.Nminutes, (int)mins);
240
241         if (hours == 0) {
242             return minutesString;
243         } else if (mins == 0) {
244             return hoursString;
245         }
246
247         final String durationFormat = context.getResources().getString(R.string.duration_format);
248         return String.format(durationFormat, hoursString, minutesString);
249     }
250
251     /**
252      * Used to combine two strings with some kind of separator in between
253      *
254      * @param context The {@link Context} to use.
255      * @param first string to combine
256      * @param second string to combine
257      * @return the combined string
258      */
259     public static final String makeCombinedString(final Context context, final String first,
260                                                   final String second) {
261         final String formatter = context.getResources().getString(R.string.combine_two_strings);
262         return String.format(formatter, first, second);
263     }
264
265     /**
266      * Changes to the next track
267      */
268     public static void next() {
269         try {
270             if (mService != null) {
271                 mService.next();
272             }
273         } catch (final RemoteException ignored) {
274         }
275     }
276
277     /**
278      * Initialize playback service with values from Settings
279      */
280     public static void initPlaybackServiceWithSettings(final Context context) {
281         MusicUtils.setShakeToPlayEnabled(
282                 PreferenceUtils.getInstance(context).getShakeToPlay());
283         MusicUtils.setShowAlbumArtOnLockscreen(
284                 PreferenceUtils.getInstance(context).getShowAlbumArtOnLockscreen());
285     }
286
287     /**
288      * Set shake to play status
289      */
290     public static void setShakeToPlayEnabled(final boolean enabled) {
291         try {
292             if (mService != null) {
293                 mService.setShakeToPlayEnabled(enabled);
294             }
295         } catch (final RemoteException ignored) {
296         }
297     }
298
299     /**
300      * Set show album art on lockscreen
301      */
302     public static void setShowAlbumArtOnLockscreen(final boolean enabled) {
303         try {
304             if (mService != null) {
305                 mService.setLockscreenAlbumArt(enabled);
306             }
307         } catch (final RemoteException ignored) {
308         }
309     }
310
311     /**
312      * Changes to the next track asynchronously
313      */
314     public static void asyncNext(final Context context) {
315         final Intent previous = new Intent(context, MusicPlaybackService.class);
316         previous.setAction(MusicPlaybackService.NEXT_ACTION);
317         context.startService(previous);
318     }
319
320     /**
321      * Changes to the previous track.
322      *
323      * @NOTE The AIDL isn't used here in order to properly use the previous
324      *       action. When the user is shuffling, because {@link
325      *       MusicPlaybackService.#openCurrentAndNext()} is used, the user won't
326      *       be able to travel to the previously skipped track. To remedy this,
327      *       {@link MusicPlaybackService.#openCurrent()} is called in {@link
328      *       MusicPlaybackService.#prev()}. {@code #startService(Intent intent)}
329      *       is called here to specifically invoke the onStartCommand used by
330      *       {@link MusicPlaybackService}, which states if the current position
331      *       less than 2000 ms, start the track over, otherwise move to the
332      *       previously listened track.
333      */
334     public static void previous(final Context context, final boolean force) {
335         final Intent previous = new Intent(context, MusicPlaybackService.class);
336         if (force) {
337             previous.setAction(MusicPlaybackService.PREVIOUS_FORCE_ACTION);
338         } else {
339             previous.setAction(MusicPlaybackService.PREVIOUS_ACTION);
340         }
341         context.startService(previous);
342     }
343
344     /**
345      * Plays or pauses the music.
346      */
347     public static void playOrPause() {
348         try {
349             if (mService != null) {
350                 if (mService.isPlaying()) {
351                     mService.pause();
352                 } else {
353                     mService.play();
354                 }
355             }
356         } catch (final Exception ignored) {
357         }
358     }
359
360     /**
361      * Cycles through the repeat options.
362      */
363     public static void cycleRepeat() {
364         try {
365             if (mService != null) {
366                 switch (mService.getRepeatMode()) {
367                     case MusicPlaybackService.REPEAT_NONE:
368                         mService.setRepeatMode(MusicPlaybackService.REPEAT_ALL);
369                         break;
370                     case MusicPlaybackService.REPEAT_ALL:
371                         mService.setRepeatMode(MusicPlaybackService.REPEAT_CURRENT);
372                         if (mService.getShuffleMode() != MusicPlaybackService.SHUFFLE_NONE) {
373                             mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
374                         }
375                         break;
376                     default:
377                         mService.setRepeatMode(MusicPlaybackService.REPEAT_NONE);
378                         break;
379                 }
380             }
381         } catch (final RemoteException ignored) {
382         }
383     }
384
385     /**
386      * Cycles through the shuffle options.
387      */
388     public static void cycleShuffle() {
389         try {
390             if (mService != null) {
391                 switch (mService.getShuffleMode()) {
392                     case MusicPlaybackService.SHUFFLE_NONE:
393                         mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
394                         if (mService.getRepeatMode() == MusicPlaybackService.REPEAT_CURRENT) {
395                             mService.setRepeatMode(MusicPlaybackService.REPEAT_ALL);
396                         }
397                         break;
398                     case MusicPlaybackService.SHUFFLE_NORMAL:
399                         mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
400                         break;
401                     case MusicPlaybackService.SHUFFLE_AUTO:
402                         mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
403                         break;
404                     default:
405                         break;
406                 }
407             }
408         } catch (final RemoteException ignored) {
409         }
410     }
411
412     /**
413      * @return True if we're playing music, false otherwise.
414      */
415     public static final boolean isPlaying() {
416         if (mService != null) {
417             try {
418                 return mService.isPlaying();
419             } catch (final RemoteException ignored) {
420             }
421         }
422         return false;
423     }
424
425     /**
426      * @return The current shuffle mode.
427      */
428     public static final int getShuffleMode() {
429         if (mService != null) {
430             try {
431                 return mService.getShuffleMode();
432             } catch (final RemoteException ignored) {
433             }
434         }
435         return 0;
436     }
437
438     /**
439      * @return The current repeat mode.
440      */
441     public static final int getRepeatMode() {
442         if (mService != null) {
443             try {
444                 return mService.getRepeatMode();
445             } catch (final RemoteException ignored) {
446             }
447         }
448         return 0;
449     }
450
451     /**
452      * @return The current track name.
453      */
454     public static final String getTrackName() {
455         if (mService != null) {
456             try {
457                 return mService.getTrackName();
458             } catch (final RemoteException ignored) {
459             }
460         }
461         return null;
462     }
463
464     /**
465      * @return The current artist name.
466      */
467     public static final String getArtistName() {
468         if (mService != null) {
469             try {
470                 return mService.getArtistName();
471             } catch (final RemoteException ignored) {
472             }
473         }
474         return null;
475     }
476
477     /**
478      * @return The current album name.
479      */
480     public static final String getAlbumName() {
481         if (mService != null) {
482             try {
483                 return mService.getAlbumName();
484             } catch (final RemoteException ignored) {
485             }
486         }
487         return null;
488     }
489
490     /**
491      * @return The current album Id.
492      */
493     public static final long getCurrentAlbumId() {
494         if (mService != null) {
495             try {
496                 return mService.getAlbumId();
497             } catch (final RemoteException ignored) {
498             }
499         }
500         return -1;
501     }
502
503     /**
504      * @return The current song Id.
505      */
506     public static final long getCurrentAudioId() {
507         if (mService != null) {
508             try {
509                 return mService.getAudioId();
510             } catch (final RemoteException ignored) {
511             }
512         }
513         return -1;
514     }
515
516     /**
517      * @return The current Music Playback Track
518      */
519     public static final MusicPlaybackTrack getCurrentTrack() {
520         if (mService != null) {
521             try {
522                 return mService.getCurrentTrack();
523             } catch (final RemoteException ignored) {
524             }
525         }
526         return null;
527     }
528
529     /**
530      * @return The Music Playback Track at the specified index
531      */
532     public static final MusicPlaybackTrack getTrack(int index) {
533         if (mService != null) {
534             try {
535                 return mService.getTrack(index);
536             } catch (final RemoteException ignored) {
537             }
538         }
539         return null;
540     }
541
542     /**
543      * @return The next song Id.
544      */
545     public static final long getNextAudioId() {
546         if (mService != null) {
547             try {
548                 return mService.getNextAudioId();
549             } catch (final RemoteException ignored) {
550             }
551         }
552         return -1;
553     }
554
555     /**
556      * @return The previous song Id.
557      */
558     public static final long getPreviousAudioId() {
559         if (mService != null) {
560             try {
561                 return mService.getPreviousAudioId();
562             } catch (final RemoteException ignored) {
563             }
564         }
565         return -1;
566     }
567
568     /**
569      * @return The current artist Id.
570      */
571     public static final long getCurrentArtistId() {
572         if (mService != null) {
573             try {
574                 return mService.getArtistId();
575             } catch (final RemoteException ignored) {
576             }
577         }
578         return -1;
579     }
580
581     /**
582      * @return The audio session Id.
583      */
584     public static final int getAudioSessionId() {
585         if (mService != null) {
586             try {
587                 return mService.getAudioSessionId();
588             } catch (final RemoteException ignored) {
589             }
590         }
591         return -1;
592     }
593
594     /**
595      * @return The queue.
596      */
597     public static final long[] getQueue() {
598         try {
599             if (mService != null) {
600                 return mService.getQueue();
601             } else {
602             }
603         } catch (final RemoteException ignored) {
604         }
605         return sEmptyList;
606     }
607
608     /**
609      * @param position
610      * @return the id of the track in the queue at the given position
611      */
612     public static final long getQueueItemAtPosition(int position) {
613         try {
614             if (mService != null) {
615                 return mService.getQueueItemAtPosition(position);
616             } else {
617             }
618         } catch (final RemoteException ignored) {
619         }
620         return -1;
621     }
622
623     /**
624      * @return the current queue size
625      */
626     public static final int getQueueSize() {
627         try {
628             if (mService != null) {
629                 return mService.getQueueSize();
630             } else {
631             }
632         } catch (final RemoteException ignored) {
633         }
634         return 0;
635     }
636
637     /**
638      * @return The position of the current track in the queue.
639      */
640     public static final int getQueuePosition() {
641         try {
642             if (mService != null) {
643                 return mService.getQueuePosition();
644             }
645         } catch (final RemoteException ignored) {
646         }
647         return 0;
648     }
649
650     /**
651      * @return The queue history size
652      */
653     public static final int getQueueHistorySize() {
654         if (mService != null) {
655             try {
656                 return mService.getQueueHistorySize();
657             } catch (final RemoteException ignored) {
658             }
659         }
660         return 0;
661     }
662
663     /**
664      * @return The queue history position at the position
665      */
666     public static final int getQueueHistoryPosition(int position) {
667         if (mService != null) {
668             try {
669                 return mService.getQueueHistoryPosition(position);
670             } catch (final RemoteException ignored) {
671             }
672         }
673         return -1;
674     }
675
676     /**
677      * @return The queue history
678      */
679     public static final int[] getQueueHistoryList() {
680         if (mService != null) {
681             try {
682                 return mService.getQueueHistoryList();
683             } catch (final RemoteException ignored) {
684             }
685         }
686         return null;
687     }
688
689     /**
690      * @param id The ID of the track to remove.
691      * @return removes track from a playlist or the queue.
692      */
693     public static final int removeTrack(final long id) {
694         try {
695             if (mService != null) {
696                 return mService.removeTrack(id);
697             }
698         } catch (final RemoteException ingored) {
699         }
700         return 0;
701     }
702
703     /**
704      * Remove song at a specified position in the list
705      *
706      * @param id The ID of the track to remove
707      * @param position The position of the song
708      *
709      * @return true if successful, false otherwise
710      */
711     public static final boolean removeTrackAtPosition(final long id, final int position) {
712         try {
713             if (mService != null) {
714                 return mService.removeTrackAtPosition(id, position);
715             }
716         } catch (final RemoteException ingored) {
717         }
718         return false;
719     }
720
721     /**
722      * @param cursor The {@link Cursor} used to perform our query.
723      * @return The song list for a MIME type.
724      */
725     public static final long[] getSongListForCursor(Cursor cursor) {
726         if (cursor == null) {
727             return sEmptyList;
728         }
729         final int len = cursor.getCount();
730         final long[] list = new long[len];
731         cursor.moveToFirst();
732         int columnIndex = -1;
733         try {
734             columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.Members.AUDIO_ID);
735         } catch (final IllegalArgumentException notaplaylist) {
736             columnIndex = cursor.getColumnIndexOrThrow(BaseColumns._ID);
737         }
738         for (int i = 0; i < len; i++) {
739             list[i] = cursor.getLong(columnIndex);
740             cursor.moveToNext();
741         }
742         cursor.close();
743         cursor = null;
744         return list;
745     }
746
747     /**
748      * @param context The {@link Context} to use.
749      * @param id The ID of the artist.
750      * @return The song list for an artist.
751      */
752     public static final long[] getSongListForArtist(final Context context, final long id) {
753         final String[] projection = new String[] {
754             BaseColumns._ID
755         };
756         final String selection = AudioColumns.ARTIST_ID + "=" + id + " AND "
757                 + AudioColumns.IS_MUSIC + "=1";
758         Cursor cursor = context.getContentResolver().query(
759                 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null,
760                 AudioColumns.ALBUM_KEY + "," + AudioColumns.TRACK);
761         if (cursor != null) {
762             final long[] mList = getSongListForCursor(cursor);
763             cursor.close();
764             cursor = null;
765             return mList;
766         }
767         return sEmptyList;
768     }
769
770     /**
771      * @param context The {@link Context} to use.
772      * @param id The ID of the album.
773      * @return The song list for an album.
774      */
775     public static final long[] getSongListForAlbum(final Context context, final long id) {
776         final String[] projection = new String[] {
777             BaseColumns._ID
778         };
779         final String selection = AudioColumns.ALBUM_ID + "=" + id + " AND " + AudioColumns.IS_MUSIC
780                 + "=1";
781         Cursor cursor = context.getContentResolver().query(
782                 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null,
783                 AudioColumns.TRACK + ", " + MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
784         if (cursor != null) {
785             final long[] mList = getSongListForCursor(cursor);
786             cursor.close();
787             cursor = null;
788             return mList;
789         }
790         return sEmptyList;
791     }
792
793     /**
794      * Plays songs by an artist.
795      *
796      * @param context The {@link Context} to use.
797      * @param artistId The artist Id.
798      * @param position Specify where to start.
799      */
800     public static void playArtist(final Context context, final long artistId, int position, boolean shuffle) {
801         final long[] artistList = getSongListForArtist(context, artistId);
802         if (artistList != null) {
803             playAll(context, artistList, position, artistId, IdType.Artist, shuffle);
804         }
805     }
806
807     /**
808      * @param context The {@link Context} to use.
809      * @param id The ID of the genre.
810      * @return The song list for an genre.
811      */
812     public static final long[] getSongListForGenre(final Context context, final long id) {
813         final String[] projection = new String[] {
814             BaseColumns._ID
815         };
816         final StringBuilder selection = new StringBuilder();
817         selection.append(AudioColumns.IS_MUSIC + "=1");
818         selection.append(" AND " + MediaColumns.TITLE + "!=''");
819         final Uri uri = MediaStore.Audio.Genres.Members.getContentUri("external", Long.valueOf(id));
820         Cursor cursor = context.getContentResolver().query(uri, projection, selection.toString(),
821                 null, null);
822         if (cursor != null) {
823             final long[] mList = getSongListForCursor(cursor);
824             cursor.close();
825             cursor = null;
826             return mList;
827         }
828         return sEmptyList;
829     }
830
831     /**
832      * @param context The {@link Context} to use
833      * @param uri The source of the file
834      */
835     public static void playFile(final Context context, final Uri uri) {
836         if (uri == null || mService == null) {
837             return;
838         }
839
840         // If this is a file:// URI, just use the path directly instead
841         // of going through the open-from-filedescriptor codepath.
842         String filename;
843         String scheme = uri.getScheme();
844         if ("file".equals(scheme)) {
845             filename = uri.getPath();
846         } else {
847             filename = uri.toString();
848         }
849
850         try {
851             mService.stop();
852             mService.openFile(filename);
853             mService.play();
854         } catch (final RemoteException ignored) {
855         }
856     }
857
858     /**
859      * @param context The {@link Context} to use.
860      * @param list The list of songs to play.
861      * @param position Specify where to start.
862      * @param forceShuffle True to force a shuffle, false otherwise.
863      */
864     public static void playAll(final Context context, final long[] list, int position,
865                                final long sourceId, final IdType sourceType,
866                                final boolean forceShuffle) {
867         if (list == null || list.length == 0 || mService == null) {
868             return;
869         }
870         try {
871             if (forceShuffle) {
872                 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
873             }
874             if (position < 0) {
875                 position = 0;
876             }
877             mService.open(list, forceShuffle ? -1 : position, sourceId, sourceType.mId);
878             mService.play();
879         } catch (final RemoteException ignored) {
880         }
881     }
882
883     /**
884      * @param list The list to enqueue.
885      */
886     public static void playNext(final long[] list, final long sourceId, final IdType sourceType) {
887         if (mService == null) {
888             return;
889         }
890         try {
891             mService.enqueue(list, MusicPlaybackService.NEXT, sourceId, sourceType.mId);
892         } catch (final RemoteException ignored) {
893         }
894     }
895
896     /**
897      * @param context The {@link Context} to use.
898      */
899     public static void shuffleAll(final Context context) {
900         Cursor cursor = SongLoader.makeSongCursor(context, null);
901         final long[] mTrackList = getSongListForCursor(cursor);
902         final int position = 0;
903         if (mTrackList.length == 0 || mService == null) {
904             return;
905         }
906         try {
907             mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
908             final long mCurrentId = mService.getAudioId();
909             final int mCurrentQueuePosition = getQueuePosition();
910             if (position != -1 && mCurrentQueuePosition == position
911                     && mCurrentId == mTrackList[position]) {
912                 final long[] mPlaylist = getQueue();
913                 if (Arrays.equals(mTrackList, mPlaylist)) {
914                     mService.play();
915                     return;
916                 }
917             }
918             mService.open(mTrackList, -1, -1, IdType.NA.mId);
919             mService.play();
920             cursor.close();
921             cursor = null;
922         } catch (final RemoteException ignored) {
923         }
924     }
925
926     /**
927      * Returns The ID for a playlist.
928      *
929      * @param context The {@link Context} to use.
930      * @param name The name of the playlist.
931      * @return The ID for a playlist.
932      */
933     public static final long getIdForPlaylist(final Context context, final String name) {
934         Cursor cursor = context.getContentResolver().query(
935                 MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, new String[]{
936                         BaseColumns._ID
937                 }, PlaylistsColumns.NAME + "=?", new String[]{
938                         name
939                 }, PlaylistsColumns.NAME);
940         int id = -1;
941         if (cursor != null) {
942             cursor.moveToFirst();
943             if (!cursor.isAfterLast()) {
944                 id = cursor.getInt(0);
945             }
946             cursor.close();
947             cursor = null;
948         }
949         return id;
950     }
951
952     /** @param context The {@link Context} to use.
953      *  @param id The id of the playlist.
954      *  @return The name for a playlist. */
955     public static final String getNameForPlaylist(final Context context, final long id) {
956         Cursor cursor = context.getContentResolver().query(
957                 MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
958                 new String[] { PlaylistsColumns.NAME },
959                 BaseColumns._ID + "=?",
960                 new String[] { Long.toString(id) },
961                 null);
962         if (cursor != null) {
963             try {
964                 if(cursor.moveToFirst()) { return cursor.getString(0); }
965             } finally { cursor.close(); }
966         }
967         // nothing found
968         return null;
969     }
970
971     /**
972      * Returns the Id for an artist.
973      *
974      * @param context The {@link Context} to use.
975      * @param name The name of the artist.
976      * @return The ID for an artist.
977      */
978     public static final long getIdForArtist(final Context context, final String name) {
979         Cursor cursor = context.getContentResolver().query(
980                 MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI, new String[]{
981                         BaseColumns._ID
982                 }, ArtistColumns.ARTIST + "=?", new String[]{
983                         name
984                 }, ArtistColumns.ARTIST);
985         int id = -1;
986         if (cursor != null) {
987             cursor.moveToFirst();
988             if (!cursor.isAfterLast()) {
989                 id = cursor.getInt(0);
990             }
991             cursor.close();
992             cursor = null;
993         }
994         return id;
995     }
996
997     /**
998      * Returns the ID for an album.
999      *
1000      * @param context The {@link Context} to use.
1001      * @param albumName The name of the album.
1002      * @param artistName The name of the artist
1003      * @return The ID for an album.
1004      */
1005     public static final long getIdForAlbum(final Context context, final String albumName,
1006             final String artistName) {
1007         Cursor cursor = context.getContentResolver().query(
1008                 MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, new String[] {
1009                     BaseColumns._ID
1010                 }, AlbumColumns.ALBUM + "=? AND " + AlbumColumns.ARTIST + "=?", new String[] {
1011                     albumName, artistName
1012                 }, AlbumColumns.ALBUM);
1013         int id = -1;
1014         if (cursor != null) {
1015             cursor.moveToFirst();
1016             if (!cursor.isAfterLast()) {
1017                 id = cursor.getInt(0);
1018             }
1019             cursor.close();
1020             cursor = null;
1021         }
1022         return id;
1023     }
1024
1025     /**
1026      * Plays songs from an album.
1027      *
1028      * @param context The {@link Context} to use.
1029      * @param albumId The album Id.
1030      * @param position Specify where to start.
1031      */
1032     public static void playAlbum(final Context context, final long albumId, int position, boolean shuffle) {
1033         final long[] albumList = getSongListForAlbum(context, albumId);
1034         if (albumList != null) {
1035             playAll(context, albumList, position, albumId, IdType.Album, shuffle);
1036         }
1037     }
1038
1039     /*  */
1040     public static void makeInsertItems(final long[] ids, final int offset, int len, final int base) {
1041         if (offset + len > ids.length) {
1042             len = ids.length - offset;
1043         }
1044
1045         if (mContentValuesCache == null || mContentValuesCache.length != len) {
1046             mContentValuesCache = new ContentValues[len];
1047         }
1048         for (int i = 0; i < len; i++) {
1049             if (mContentValuesCache[i] == null) {
1050                 mContentValuesCache[i] = new ContentValues();
1051             }
1052             mContentValuesCache[i].put(Playlists.Members.PLAY_ORDER, base + offset + i);
1053             mContentValuesCache[i].put(Playlists.Members.AUDIO_ID, ids[offset + i]);
1054         }
1055     }
1056
1057     /**
1058      * @param context The {@link Context} to use.
1059      * @param name The name of the new playlist.
1060      * @return A new playlist ID.
1061      */
1062     public static final long createPlaylist(final Context context, final String name) {
1063         if (name != null && name.length() > 0) {
1064             final ContentResolver resolver = context.getContentResolver();
1065             final String[] projection = new String[] {
1066                 PlaylistsColumns.NAME
1067             };
1068             final String selection = PlaylistsColumns.NAME + " = '" + name + "'";
1069             Cursor cursor = resolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
1070                     projection, selection, null, null);
1071             if (cursor.getCount() <= 0) {
1072                 final ContentValues values = new ContentValues(1);
1073                 values.put(PlaylistsColumns.NAME, name);
1074                 final Uri uri = resolver.insert(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
1075                         values);
1076                 return Long.parseLong(uri.getLastPathSegment());
1077             }
1078             if (cursor != null) {
1079                 cursor.close();
1080                 cursor = null;
1081             }
1082             return -1;
1083         }
1084         return -1;
1085     }
1086
1087     /**
1088      * @param context The {@link Context} to use.
1089      * @param playlistId The playlist ID.
1090      */
1091     public static void clearPlaylist(final Context context, final int playlistId) {
1092         final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
1093         context.getContentResolver().delete(uri, null, null);
1094         return;
1095     }
1096
1097     /** remove all backing data for top tracks playlist */
1098     public static void clearTopTracks(Context context) {
1099         SongPlayCount.getInstance(context).deleteAll();
1100     }
1101
1102     /** remove all backing data for top tracks playlist */
1103     public static void clearRecent(Context context) {
1104         RecentStore.getInstance(context).deleteAll();
1105     }
1106
1107     /** move up cutoff for last added songs so playlist will be cleared */
1108     public static void clearLastAdded(Context context) {
1109         PreferenceUtils.getInstance(context)
1110             .setLastAddedCutoff(System.currentTimeMillis());
1111     }
1112
1113     /**
1114      * @param context The {@link Context} to use.
1115      * @param ids The id of the song(s) to add.
1116      * @param playlistid The id of the playlist being added to.
1117      */
1118     public static void addToPlaylist(final Context context, final long[] ids, final long playlistid) {
1119         final int size = ids.length;
1120         final ContentResolver resolver = context.getContentResolver();
1121         final String[] projection = new String[] {
1122             "max(" + Playlists.Members.PLAY_ORDER + ")",
1123         };
1124         final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistid);
1125         Cursor cursor = null;
1126         int base = 0;
1127
1128         try {
1129             cursor = resolver.query(uri, projection, null, null, null);
1130
1131             if (cursor != null && cursor.moveToFirst()) {
1132                 base = cursor.getInt(0) + 1;
1133             }
1134         } finally {
1135             if (cursor != null) {
1136                 cursor.close();
1137                 cursor = null;
1138             }
1139         }
1140
1141         int numinserted = 0;
1142         for (int offSet = 0; offSet < size; offSet += 1000) {
1143             makeInsertItems(ids, offSet, 1000, base);
1144             numinserted += resolver.bulkInsert(uri, mContentValuesCache);
1145         }
1146         final String message = context.getResources().getQuantityString(
1147                 R.plurals.NNNtrackstoplaylist, numinserted, numinserted);
1148         CustomToast.makeText((Activity)context, message, CustomToast.LENGTH_SHORT).show();
1149         playlistChanged();
1150     }
1151
1152     /**
1153      * Removes a single track from a given playlist
1154      * @param context The {@link Context} to use.
1155      * @param id The id of the song to remove.
1156      * @param playlistId The id of the playlist being removed from.
1157      */
1158     public static void removeFromPlaylist(final Context context, final long id,
1159             final long playlistId) {
1160         final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
1161         final ContentResolver resolver = context.getContentResolver();
1162         resolver.delete(uri, Playlists.Members.AUDIO_ID + " = ? ", new String[] {
1163             Long.toString(id)
1164         });
1165         final String message = context.getResources().getQuantityString(
1166                 R.plurals.NNNtracksfromplaylist, 1, 1);
1167         CustomToast.makeText((Activity)context, message, CustomToast.LENGTH_SHORT).show();
1168         playlistChanged();
1169     }
1170
1171     /**
1172      * @param context The {@link Context} to use.
1173      * @param list The list to enqueue.
1174      */
1175     public static void addToQueue(final Context context, final long[] list, long sourceId,
1176                                   IdType sourceType) {
1177         if (mService == null) {
1178             return;
1179         }
1180         try {
1181             mService.enqueue(list, MusicPlaybackService.LAST, sourceId, sourceType.mId);
1182             final String message = makeLabel(context, R.plurals.NNNtrackstoqueue, list.length);
1183             CustomToast.makeText((Activity) context, message, CustomToast.LENGTH_SHORT).show();
1184         } catch (final RemoteException ignored) {
1185         }
1186     }
1187
1188     /**
1189      * @param context The {@link Context} to use
1190      * @param id The song ID.
1191      */
1192     public static void setRingtone(final Context context, final long id) {
1193         final ContentResolver resolver = context.getContentResolver();
1194         final Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
1195         try {
1196             final ContentValues values = new ContentValues(2);
1197             values.put(AudioColumns.IS_RINGTONE, "1");
1198             values.put(AudioColumns.IS_ALARM, "1");
1199             resolver.update(uri, values, null, null);
1200         } catch (final UnsupportedOperationException ingored) {
1201             return;
1202         }
1203
1204         final String[] projection = new String[] {
1205                 BaseColumns._ID, MediaColumns.DATA, MediaColumns.TITLE
1206         };
1207
1208         final String selection = BaseColumns._ID + "=" + id;
1209         Cursor cursor = resolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection,
1210                 selection, null, null);
1211         try {
1212             if (cursor != null && cursor.getCount() == 1) {
1213                 cursor.moveToFirst();
1214                 Settings.System.putString(resolver, Settings.System.RINGTONE, uri.toString());
1215                 final String message = context.getString(R.string.set_as_ringtone,
1216                         cursor.getString(2));
1217                 CustomToast.makeText((Activity)context, message, CustomToast.LENGTH_SHORT).show();
1218             }
1219         } finally {
1220             if (cursor != null) {
1221                 cursor.close();
1222                 cursor = null;
1223             }
1224         }
1225     }
1226
1227     /**
1228      * @param context The {@link Context} to use.
1229      * @param id The id of the album.
1230      * @return The song count for an album.
1231      */
1232     public static final int getSongCountForAlbumInt(final Context context, final long id) {
1233         int songCount = 0;
1234         if (id == -1) { return songCount; }
1235
1236         Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, id);
1237         Cursor cursor = context.getContentResolver().query(uri,
1238                 new String[] { AlbumColumns.NUMBER_OF_SONGS }, null, null, null);
1239         if (cursor != null) {
1240             cursor.moveToFirst();
1241             if (!cursor.isAfterLast()) {
1242                 if(!cursor.isNull(0)) {
1243                     songCount = cursor.getInt(0);
1244                 }
1245             }
1246             cursor.close();
1247             cursor = null;
1248         }
1249
1250         return songCount;
1251     }
1252
1253     /**
1254      * Gets the number of songs for a playlist
1255      * @param context The {@link Context} to use.
1256      * @param playlistId the id of the playlist
1257      * @return the # of songs in the playlist
1258      */
1259     public static final int getSongCountForPlaylist(final Context context, final long playlistId) {
1260         Cursor c = context.getContentResolver().query(
1261                 MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId),
1262                 new String[]{BaseColumns._ID}, MusicUtils.MUSIC_ONLY_SELECTION, null, null);
1263
1264         if (c != null) {
1265             int count = 0;
1266             if (c.moveToFirst()) {
1267                 count = c.getCount();
1268             }
1269             c.close();
1270             c = null;
1271             return count;
1272         }
1273
1274         return 0;
1275     }
1276
1277     public static final AlbumArtistDetails getAlbumArtDetails(final Context context, final long trackId) {
1278         final StringBuilder selection = new StringBuilder();
1279         selection.append(MediaStore.Audio.AudioColumns.IS_MUSIC + "=1");
1280         selection.append(" AND " + BaseColumns._ID + " = '" + trackId + "'");
1281
1282         Cursor cursor = context.getContentResolver().query(
1283             MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1284             new String[] {
1285                     /* 0 */
1286                 MediaStore.Audio.AudioColumns.ALBUM_ID,
1287                     /* 1 */
1288                 MediaStore.Audio.AudioColumns.ALBUM,
1289                     /* 2 */
1290                 MediaStore.Audio.AlbumColumns.ARTIST,
1291             }, selection.toString(), null, null
1292         );
1293
1294         if (!cursor.moveToFirst()) {
1295             cursor.close();
1296             return null;
1297         }
1298
1299         AlbumArtistDetails result = new AlbumArtistDetails();
1300         result.mAudioId = trackId;
1301         result.mAlbumId = cursor.getLong(0);
1302         result.mAlbumName = cursor.getString(1);
1303         result.mArtistName = cursor.getString(2);
1304         cursor.close();
1305
1306         return result;
1307     }
1308
1309     /**
1310      * @param context The {@link Context} to use.
1311      * @param id The id of the album.
1312      * @return The release date for an album.
1313      */
1314     public static final String getReleaseDateForAlbum(final Context context, final long id) {
1315         if (id == -1) {
1316             return null;
1317         }
1318         Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, id);
1319         Cursor cursor = context.getContentResolver().query(uri, new String[] {
1320                     AlbumColumns.FIRST_YEAR
1321                 }, null, null, null);
1322         String releaseDate = null;
1323         if (cursor != null) {
1324             cursor.moveToFirst();
1325             if (!cursor.isAfterLast()) {
1326                 releaseDate = cursor.getString(0);
1327             }
1328             cursor.close();
1329             cursor = null;
1330         }
1331         return releaseDate;
1332     }
1333
1334     /**
1335      * @return The path to the currently playing file as {@link String}
1336      */
1337     public static final String getFilePath() {
1338         try {
1339             if (mService != null) {
1340                 return mService.getPath();
1341             }
1342         } catch (final RemoteException ignored) {
1343         }
1344         return null;
1345     }
1346
1347     /**
1348      * @param from The index the item is currently at.
1349      * @param to The index the item is moving to.
1350      */
1351     public static void moveQueueItem(final int from, final int to) {
1352         try {
1353             if (mService != null) {
1354                 mService.moveQueueItem(from, to);
1355             } else {
1356             }
1357         } catch (final RemoteException ignored) {
1358         }
1359     }
1360
1361     /**
1362      * @param context The {@link Context} to sue
1363      * @param playlistId The playlist Id
1364      * @return The track list for a playlist
1365      */
1366     public static final long[] getSongListForPlaylist(final Context context, final long playlistId) {
1367         Cursor cursor = PlaylistSongLoader.makePlaylistSongCursor(context, playlistId);
1368
1369         if (cursor != null) {
1370             final long[] list = getSongListForCursor(cursor);
1371             cursor.close();
1372             cursor = null;
1373             return list;
1374         }
1375         return sEmptyList;
1376     }
1377
1378     /**
1379      * Plays a user created playlist.
1380      *
1381      * @param context The {@link Context} to use.
1382      * @param playlistId The playlist Id.
1383      */
1384     public static void playPlaylist(final Context context, final long playlistId, boolean shuffle) {
1385         final long[] playlistList = getSongListForPlaylist(context, playlistId);
1386         if (playlistList != null) {
1387             playAll(context, playlistList, -1, playlistId, IdType.Playlist, shuffle);
1388         }
1389     }
1390
1391     /**
1392      * @param context The {@link Context} to use
1393      * @param type The Smart Playlist Type
1394      * @return The song list for the last added playlist
1395      */
1396     public static final long[] getSongListForSmartPlaylist(final Context context,
1397                                                            final SmartPlaylistType type) {
1398         Cursor cursor = null;
1399         try {
1400             switch (type) {
1401                 case LastAdded:
1402                     cursor = LastAddedLoader.makeLastAddedCursor(context);
1403                     break;
1404                 case RecentlyPlayed:
1405                     cursor = TopTracksLoader.makeRecentTracksCursor(context);
1406                     break;
1407                 case TopTracks:
1408                     cursor = TopTracksLoader.makeTopTracksCursor(context);
1409                     break;
1410             }
1411             return MusicUtils.getSongListForCursor(cursor);
1412         } finally {
1413             if (cursor != null) {
1414                 cursor.close();
1415                 cursor = null;
1416             }
1417         }
1418     }
1419
1420     /**
1421      * Plays the smart playlist
1422      * @param context The {@link Context} to use
1423      * @param position the position to start playing from
1424      * @param type The Smart Playlist Type
1425      */
1426     public static void playSmartPlaylist(final Context context, final int position,
1427                                          final SmartPlaylistType type, final boolean shuffle) {
1428         final long[] list = getSongListForSmartPlaylist(context, type);
1429         MusicUtils.playAll(context, list, position, type.mId, IdType.Playlist, shuffle);
1430     }
1431
1432     /**
1433      * Creates a sub menu used to add items to a new playlist or an existsing
1434      * one.
1435      *
1436      * @param context The {@link Context} to use.
1437      * @param groupId The group Id of the menu.
1438      * @param menu The {@link Menu} to add to.
1439      */
1440     public static void makePlaylistMenu(final Context context, final int groupId,
1441             final Menu menu) {
1442         menu.clear();
1443         menu.add(groupId, FragmentMenuItems.NEW_PLAYLIST, Menu.NONE, R.string.new_playlist);
1444         Cursor cursor = PlaylistLoader.makePlaylistCursor(context);
1445         if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) {
1446             while (!cursor.isAfterLast()) {
1447                 final Intent intent = new Intent();
1448                 String name = cursor.getString(1);
1449                 if (name != null) {
1450                     intent.putExtra("playlist", getIdForPlaylist(context, name));
1451                     menu.add(groupId, FragmentMenuItems.PLAYLIST_SELECTED, Menu.NONE,
1452                             name).setIntent(intent);
1453                 }
1454                 cursor.moveToNext();
1455             }
1456         }
1457         if (cursor != null) {
1458             cursor.close();
1459             cursor = null;
1460         }
1461     }
1462
1463     /**
1464      * Called when one of the lists should refresh or requery.
1465      */
1466     public static void refresh() {
1467         try {
1468             if (mService != null) {
1469                 mService.refresh();
1470             }
1471         } catch (final RemoteException ignored) {
1472         }
1473     }
1474
1475     /**
1476      * Called when one of playlists have changed
1477      */
1478     public static void playlistChanged() {
1479         try {
1480             if (mService != null) {
1481                 mService.playlistChanged();
1482             }
1483         } catch (final RemoteException ignored) {
1484         }
1485     }
1486
1487     /**
1488      * Seeks the current track to a desired position
1489      *
1490      * @param position The position to seek to
1491      */
1492     public static void seek(final long position) {
1493         if (mService != null) {
1494             try {
1495                 mService.seek(position);
1496             } catch (final RemoteException ignored) {
1497             }
1498         }
1499     }
1500
1501     /**
1502      * Seeks the current track to a desired relative position.  This can be used
1503      * to simulate fastforward and rewind
1504      *
1505      * @param deltaInMs The delta in ms to seek from the current position
1506      */
1507     public static void seekRelative(final long deltaInMs) {
1508         if (mService != null) {
1509             try {
1510                 mService.seekRelative(deltaInMs);
1511             } catch (final RemoteException ignored) {
1512             } catch (final IllegalStateException ignored) {
1513                 // Illegal State Exception message is empty so logging will actually throw an
1514                 // exception.  We should come back and figure out why we get an exception in the
1515                 // first place and make sure we understand it completely.  I will use
1516                 // https://cyanogen.atlassian.net/browse/MUSIC-125 to track investigating this more
1517             }
1518         }
1519     }
1520
1521     /**
1522      * @return The current position time of the track
1523      */
1524     public static final long position() {
1525         if (mService != null) {
1526             try {
1527                 return mService.position();
1528             } catch (final RemoteException ignored) {
1529             } catch (final IllegalStateException ex) {
1530                 // Illegal State Exception message is empty so logging will actually throw an
1531                 // exception.  We should come back and figure out why we get an exception in the
1532                 // first place and make sure we understand it completely.  I will use
1533                 // https://cyanogen.atlassian.net/browse/MUSIC-125 to track investigating this more
1534             }
1535         }
1536         return 0;
1537     }
1538
1539     /**
1540      * @return The total length of the current track
1541      */
1542     public static final long duration() {
1543         if (mService != null) {
1544             try {
1545                 return mService.duration();
1546             } catch (final RemoteException ignored) {
1547             } catch (final IllegalStateException ignored) {
1548                 // Illegal State Exception message is empty so logging will actually throw an
1549                 // exception.  We should come back and figure out why we get an exception in the
1550                 // first place and make sure we understand it completely.  I will use
1551                 // https://cyanogen.atlassian.net/browse/MUSIC-125 to track investigating this more
1552             }
1553         }
1554         return 0;
1555     }
1556
1557     /**
1558      * @param position The position to move the queue to
1559      */
1560     public static void setQueuePosition(final int position) {
1561         if (mService != null) {
1562             try {
1563                 mService.setQueuePosition(position);
1564             } catch (final RemoteException ignored) {
1565             }
1566         }
1567     }
1568
1569     /**
1570      * Clears the qeueue
1571      */
1572     public static void clearQueue() {
1573         try {
1574             mService.removeTracks(0, Integer.MAX_VALUE);
1575         } catch (final RemoteException ignored) {
1576         }
1577     }
1578
1579     /**
1580      * Perminately deletes item(s) from the user's device
1581      *
1582      * @param context The {@link Context} to use.
1583      * @param list The item(s) to delete.
1584      */
1585     public static void deleteTracks(final Context context, final long[] list) {
1586         final String[] projection = new String[] {
1587                 BaseColumns._ID, MediaColumns.DATA, AudioColumns.ALBUM_ID
1588         };
1589         final StringBuilder selection = new StringBuilder();
1590         selection.append(BaseColumns._ID + " IN (");
1591         for (int i = 0; i < list.length; i++) {
1592             selection.append(list[i]);
1593             if (i < list.length - 1) {
1594                 selection.append(",");
1595             }
1596         }
1597         selection.append(")");
1598         final Cursor c = context.getContentResolver().query(
1599                 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection.toString(),
1600                 null, null);
1601         if (c != null) {
1602             // Step 1: Remove selected tracks from the current playlist, as well
1603             // as from the album art cache
1604             c.moveToFirst();
1605             while (!c.isAfterLast()) {
1606                 // Remove from current playlist
1607                 final long id = c.getLong(0);
1608                 removeTrack(id);
1609                 // Remove the track from the play count
1610                 SongPlayCount.getInstance(context).removeItem(id);
1611                 // Remove any items in the recents database
1612                 RecentStore.getInstance(context).removeItem(id);
1613                 c.moveToNext();
1614             }
1615
1616             // Step 2: Remove selected tracks from the database
1617             context.getContentResolver().delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1618                     selection.toString(), null);
1619
1620             // Step 3: Remove files from card
1621             c.moveToFirst();
1622             while (!c.isAfterLast()) {
1623                 final String name = c.getString(1);
1624                 final File f = new File(name);
1625                 try { // File.delete can throw a security exception
1626                     if (!f.delete()) {
1627                         // I'm not sure if we'd ever get here (deletion would
1628                         // have to fail, but no exception thrown)
1629                         Log.e("MusicUtils", "Failed to delete file " + name);
1630                     }
1631                     c.moveToNext();
1632                 } catch (final SecurityException ex) {
1633                     c.moveToNext();
1634                 }
1635             }
1636             c.close();
1637         }
1638
1639         final String message = makeLabel(context, R.plurals.NNNtracksdeleted, list.length);
1640
1641         CustomToast.makeText((Activity)context, message, CustomToast.LENGTH_SHORT).show();
1642         // We deleted a number of tracks, which could affect any number of
1643         // things
1644         // in the media content domain, so update everything.
1645         context.getContentResolver().notifyChange(Uri.parse("content://media"), null);
1646         // Notify the lists to update
1647         refresh();
1648     }
1649
1650     /**
1651      * Simple function used to determine if the song/album year is invalid
1652      * @param year value to test
1653      * @return true if the app considers it valid
1654      */
1655     public static boolean isInvalidYear(int year) {
1656         return year < MIN_VALID_YEAR;
1657     }
1658
1659     /**
1660      * A snippet is taken from MediaStore.Audio.keyFor method
1661      * This will take a name, removes things like "the", "an", etc
1662      * as well as special characters and return it
1663      * @param name the string to trim
1664      * @return the trimmed name
1665      */
1666     public static String getTrimmedName(String name) {
1667         if (name == null || name.length() == 0) {
1668             return name;
1669         }
1670
1671         name = name.trim().toLowerCase();
1672         if (name.startsWith("the ")) {
1673             name = name.substring(4);
1674         }
1675         if (name.startsWith("an ")) {
1676             name = name.substring(3);
1677         }
1678         if (name.startsWith("a ")) {
1679             name = name.substring(2);
1680         }
1681         if (name.endsWith(", the") || name.endsWith(",the") ||
1682                 name.endsWith(", an") || name.endsWith(",an") ||
1683                 name.endsWith(", a") || name.endsWith(",a")) {
1684             name = name.substring(0, name.lastIndexOf(','));
1685         }
1686         name = name.replaceAll("[\\[\\]\\(\\)\"'.,?!]", "").trim();
1687
1688         return name;
1689     }
1690
1691     /**
1692      * A snippet is taken from MediaStore.Audio.keyFor method
1693      * This will take a name, removes things like "the", "an", etc
1694      * as well as special characters, then find the localized label
1695      * @param name Name to get the label of
1696      * @return the localized label of the bucket that the name falls into
1697      */
1698     public static String getLocalizedBucketLetter(String name) {
1699         if (name == null || name.length() == 0) {
1700             return null;
1701         }
1702
1703         name = getTrimmedName(name);
1704
1705         if (name.length() > 0) {
1706             return LocaleUtils.getInstance().getLabel(name);
1707         }
1708
1709         return null;
1710     }
1711
1712     /** @return true if a string is null, empty, or contains only whitespace */
1713     public static boolean isBlank(String s) {
1714         if(s == null) { return true; }
1715         if(s.isEmpty()) { return true; }
1716         for(int i = 0; i < s.length(); i++) {
1717             char c = s.charAt(i);
1718             if(!Character.isWhitespace(c)) { return false; }
1719         }
1720         return true;
1721     }
1722
1723     /**
1724      * Removes the header image from the cache.
1725      */
1726     public static void removeFromCache(Activity activity, String key) {
1727         ImageFetcher imageFetcher = ApolloUtils.getImageFetcher(activity);
1728         imageFetcher.removeFromCache(key);
1729         // Give the disk cache a little time before requesting a new image.
1730         SystemClock.sleep(80);
1731     }
1732
1733     /**
1734      * Removes image from cache so that the stock image is retrieved on reload
1735      */
1736     public static void selectOldPhoto(Activity activity, String key) {
1737         // First remove the old image
1738         removeFromCache(activity, key);
1739         MusicUtils.refresh();
1740     }
1741
1742     /**
1743      *
1744      * @param sortOrder values are mostly derived from SortOrder.class or could also be any sql
1745      *                  order clause
1746      * @return
1747      */
1748     public static boolean isSortOrderDesending(String sortOrder) {
1749         return sortOrder.endsWith(" DESC");
1750     }
1751
1752     /**
1753      * Takes a collection of items and builds a comma-separated list of them
1754      * @param items collection of items
1755      * @return comma-separted list of items
1756      */
1757     public static final <E> String buildCollectionAsString(Collection<E> items) {
1758         Iterator<E> iterator = items.iterator();
1759         StringBuilder str = new StringBuilder();
1760         if (iterator.hasNext()) {
1761             str.append(iterator.next());
1762             while (iterator.hasNext()) {
1763                 str.append(",");
1764                 str.append(iterator.next());
1765             }
1766         }
1767
1768         return str.toString();
1769     }
1770 }