OSDN Git Service

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