OSDN Git Service

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