OSDN Git Service

Eleven: This adds swiping gestures to the music player in the main area
[android-x86/packages-apps-Eleven.git] / src / com / cyngn / eleven / utils / MusicUtils.java
1 /*
2  * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
3  * (the "License"); you may not use this file except in compliance with the
4  * License. You may obtain a copy of the License at
5  * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
6  * or agreed to in writing, software distributed under the License is
7  * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
8  * KIND, either express or implied. See the License for the specific language
9  * governing permissions and limitations under the License.
10  */
11
12 package com.cyngn.eleven.utils;
13
14 import android.app.Activity;
15 import android.content.ComponentName;
16 import android.content.ContentResolver;
17 import android.content.ContentUris;
18 import android.content.ContentValues;
19 import android.content.Context;
20 import android.content.ContextWrapper;
21 import android.content.Intent;
22 import android.content.ServiceConnection;
23 import android.database.Cursor;
24 import android.net.Uri;
25 import android.os.AsyncTask;
26 import android.os.IBinder;
27 import android.os.RemoteException;
28 import android.provider.BaseColumns;
29 import android.provider.MediaStore;
30 import android.provider.MediaStore.Audio.AlbumColumns;
31 import android.provider.MediaStore.Audio.ArtistColumns;
32 import android.provider.MediaStore.Audio.AudioColumns;
33 import android.provider.MediaStore.Audio.Playlists;
34 import android.provider.MediaStore.Audio.PlaylistsColumns;
35 import android.provider.MediaStore.MediaColumns;
36 import android.provider.Settings;
37 import android.util.Log;
38 import android.view.Menu;
39 import android.view.SubMenu;
40
41 import com.cyngn.eleven.IElevenService;
42 import com.cyngn.eleven.MusicPlaybackService;
43 import com.cyngn.eleven.R;
44 import com.cyngn.eleven.loaders.LastAddedLoader;
45 import com.cyngn.eleven.loaders.PlaylistLoader;
46 import com.cyngn.eleven.loaders.SongLoader;
47 import com.cyngn.eleven.menu.FragmentMenuItems;
48 import com.cyngn.eleven.provider.RecentStore;
49 import com.devspark.appmsg.AppMsg;
50
51 import java.io.File;
52 import java.util.Arrays;
53 import java.util.Formatter;
54 import java.util.Locale;
55 import java.util.WeakHashMap;
56
57 /**
58  * A collection of helpers directly related to music or Apollo's service.
59  *
60  * @author Andrew Neal (andrewdneal@gmail.com)
61  */
62 public final class MusicUtils {
63
64     public static IElevenService mService = null;
65
66     private static int sForegroundActivities = 0;
67
68     private static final WeakHashMap<Context, ServiceBinder> mConnectionMap;
69
70     private static final long[] sEmptyList;
71
72     private static ContentValues[] mContentValuesCache = null;
73
74     private static final int MIN_VALID_YEAR = 1900; // used to remove invalid years from metadata
75
76     static {
77         mConnectionMap = new WeakHashMap<Context, ServiceBinder>();
78         sEmptyList = new long[0];
79     }
80
81     /* This class is never initiated */
82     public MusicUtils() {
83     }
84
85     /**
86      * @param context The {@link Context} to use
87      * @param callback The {@link ServiceConnection} to use
88      * @return The new instance of {@link ServiceToken}
89      */
90     public static final ServiceToken bindToService(final Context context,
91             final ServiceConnection callback) {
92         Activity realActivity = ((Activity)context).getParent();
93         if (realActivity == null) {
94             realActivity = (Activity)context;
95         }
96         final ContextWrapper contextWrapper = new ContextWrapper(realActivity);
97         contextWrapper.startService(new Intent(contextWrapper, MusicPlaybackService.class));
98         final ServiceBinder binder = new ServiceBinder(callback);
99         if (contextWrapper.bindService(
100                 new Intent().setClass(contextWrapper, MusicPlaybackService.class), binder, 0)) {
101             mConnectionMap.put(contextWrapper, binder);
102             return new ServiceToken(contextWrapper);
103         }
104         return null;
105     }
106
107     /**
108      * @param token The {@link ServiceToken} to unbind from
109      */
110     public static void unbindFromService(final ServiceToken token) {
111         if (token == null) {
112             return;
113         }
114         final ContextWrapper mContextWrapper = token.mWrappedContext;
115         final ServiceBinder mBinder = mConnectionMap.remove(mContextWrapper);
116         if (mBinder == null) {
117             return;
118         }
119         mContextWrapper.unbindService(mBinder);
120         if (mConnectionMap.isEmpty()) {
121             mService = null;
122         }
123     }
124
125     public static final class ServiceBinder implements ServiceConnection {
126         private final ServiceConnection mCallback;
127
128         /**
129          * Constructor of <code>ServiceBinder</code>
130          *
131          * @param context The {@link ServiceConnection} to use
132          */
133         public ServiceBinder(final ServiceConnection callback) {
134             mCallback = callback;
135         }
136
137         @Override
138         public void onServiceConnected(final ComponentName className, final IBinder service) {
139             mService = IElevenService.Stub.asInterface(service);
140             if (mCallback != null) {
141                 mCallback.onServiceConnected(className, service);
142             }
143         }
144
145         @Override
146         public void onServiceDisconnected(final ComponentName className) {
147             if (mCallback != null) {
148                 mCallback.onServiceDisconnected(className);
149             }
150             mService = null;
151         }
152     }
153
154     public static final class ServiceToken {
155         public ContextWrapper mWrappedContext;
156
157         /**
158          * Constructor of <code>ServiceToken</code>
159          *
160          * @param context The {@link ContextWrapper} to use
161          */
162         public ServiceToken(final ContextWrapper context) {
163             mWrappedContext = context;
164         }
165     }
166
167     /**
168      * Used to make number of labels for the number of artists, albums, songs,
169      * genres, and playlists.
170      *
171      * @param context The {@link Context} to use.
172      * @param pluralInt The ID of the plural string to use.
173      * @param number The number of artists, albums, songs, genres, or playlists.
174      * @return A {@link String} used as a label for the number of artists,
175      *         albums, songs, genres, and playlists.
176      */
177     public static final String makeLabel(final Context context, final int pluralInt,
178             final int number) {
179         return context.getResources().getQuantityString(pluralInt, number, number);
180     }
181
182     /**
183      * * Used to create a formatted time string for the duration of tracks.
184      *
185      * @param context The {@link Context} to use.
186      * @param secs The track in seconds.
187      * @return Duration of a track that's properly formatted.
188      */
189     public static final String makeTimeString(final Context context, long secs) {
190         long hours, mins;
191
192         hours = secs / 3600;
193         secs -= hours * 3600;
194         mins = secs / 60;
195         secs -= mins * 60;
196
197         final String durationFormat = context.getResources().getString(
198                 hours == 0 ? R.string.durationformatshort : R.string.durationformatlong);
199         return String.format(durationFormat, hours, mins, secs);
200     }
201
202     /**
203      * Changes to the next track
204      */
205     public static void next() {
206         try {
207             if (mService != null) {
208                 mService.next();
209             }
210         } catch (final RemoteException ignored) {
211         }
212     }
213
214     /**
215      * Changes to the next track asynchronously
216      */
217     public static void asyncNext(final Context context) {
218         final Intent previous = new Intent(context, MusicPlaybackService.class);
219         previous.setAction(MusicPlaybackService.NEXT_ACTION);
220         context.startService(previous);
221     }
222
223     /**
224      * Changes to the previous track.
225      *
226      * @NOTE The AIDL isn't used here in order to properly use the previous
227      *       action. When the user is shuffling, because {@link
228      *       MusicPlaybackService.#openCurrentAndNext()} is used, the user won't
229      *       be able to travel to the previously skipped track. To remedy this,
230      *       {@link MusicPlaybackService.#openCurrent()} is called in {@link
231      *       MusicPlaybackService.#prev()}. {@code #startService(Intent intent)}
232      *       is called here to specifically invoke the onStartCommand used by
233      *       {@link MusicPlaybackService}, which states if the current position
234      *       less than 2000 ms, start the track over, otherwise move to the
235      *       previously listened track.
236      */
237     public static void previous(final Context context, final boolean force) {
238         final Intent previous = new Intent(context, MusicPlaybackService.class);
239         if (force) {
240             previous.setAction(MusicPlaybackService.PREVIOUS_FORCE_ACTION);
241         } else {
242             previous.setAction(MusicPlaybackService.PREVIOUS_ACTION);
243         }
244         context.startService(previous);
245     }
246
247     /**
248      * Plays or pauses the music.
249      */
250     public static void playOrPause() {
251         try {
252             if (mService != null) {
253                 if (mService.isPlaying()) {
254                     mService.pause();
255                 } else {
256                     mService.play();
257                 }
258             }
259         } catch (final Exception ignored) {
260         }
261     }
262
263     /**
264      * Cycles through the repeat options.
265      */
266     public static void cycleRepeat() {
267         try {
268             if (mService != null) {
269                 switch (mService.getRepeatMode()) {
270                     case MusicPlaybackService.REPEAT_NONE:
271                         mService.setRepeatMode(MusicPlaybackService.REPEAT_ALL);
272                         break;
273                     case MusicPlaybackService.REPEAT_ALL:
274                         mService.setRepeatMode(MusicPlaybackService.REPEAT_CURRENT);
275                         if (mService.getShuffleMode() != MusicPlaybackService.SHUFFLE_NONE) {
276                             mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
277                         }
278                         break;
279                     default:
280                         mService.setRepeatMode(MusicPlaybackService.REPEAT_NONE);
281                         break;
282                 }
283             }
284         } catch (final RemoteException ignored) {
285         }
286     }
287
288     /**
289      * Cycles through the shuffle options.
290      */
291     public static void cycleShuffle() {
292         try {
293             if (mService != null) {
294                 switch (mService.getShuffleMode()) {
295                     case MusicPlaybackService.SHUFFLE_NONE:
296                         mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
297                         if (mService.getRepeatMode() == MusicPlaybackService.REPEAT_CURRENT) {
298                             mService.setRepeatMode(MusicPlaybackService.REPEAT_ALL);
299                         }
300                         break;
301                     case MusicPlaybackService.SHUFFLE_NORMAL:
302                         mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
303                         break;
304                     case MusicPlaybackService.SHUFFLE_AUTO:
305                         mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
306                         break;
307                     default:
308                         break;
309                 }
310             }
311         } catch (final RemoteException ignored) {
312         }
313     }
314
315     /**
316      * @return True if we're playing music, false otherwise.
317      */
318     public static final boolean isPlaying() {
319         if (mService != null) {
320             try {
321                 return mService.isPlaying();
322             } catch (final RemoteException ignored) {
323             }
324         }
325         return false;
326     }
327
328     /**
329      * @return The current shuffle mode.
330      */
331     public static final int getShuffleMode() {
332         if (mService != null) {
333             try {
334                 return mService.getShuffleMode();
335             } catch (final RemoteException ignored) {
336             }
337         }
338         return 0;
339     }
340
341     /**
342      * @return The current repeat mode.
343      */
344     public static final int getRepeatMode() {
345         if (mService != null) {
346             try {
347                 return mService.getRepeatMode();
348             } catch (final RemoteException ignored) {
349             }
350         }
351         return 0;
352     }
353
354     /**
355      * @return The current track name.
356      */
357     public static final String getTrackName() {
358         if (mService != null) {
359             try {
360                 return mService.getTrackName();
361             } catch (final RemoteException ignored) {
362             }
363         }
364         return null;
365     }
366
367     /**
368      * @return The current artist name.
369      */
370     public static final String getArtistName() {
371         if (mService != null) {
372             try {
373                 return mService.getArtistName();
374             } catch (final RemoteException ignored) {
375             }
376         }
377         return null;
378     }
379
380     /**
381      * @return The current album name.
382      */
383     public static final String getAlbumName() {
384         if (mService != null) {
385             try {
386                 return mService.getAlbumName();
387             } catch (final RemoteException ignored) {
388             }
389         }
390         return null;
391     }
392
393     /**
394      * @return The current album Id.
395      */
396     public static final long getCurrentAlbumId() {
397         if (mService != null) {
398             try {
399                 return mService.getAlbumId();
400             } catch (final RemoteException ignored) {
401             }
402         }
403         return -1;
404     }
405
406     /**
407      * @return The current song Id.
408      */
409     public static final long getCurrentAudioId() {
410         if (mService != null) {
411             try {
412                 return mService.getAudioId();
413             } catch (final RemoteException ignored) {
414             }
415         }
416         return -1;
417     }
418
419     /**
420      * @return The next song Id.
421      */
422     public static final long getNextAudioId() {
423         if (mService != null) {
424             try {
425                 return mService.getNextAudioId();
426             } catch (final RemoteException ignored) {
427             }
428         }
429         return -1;
430     }
431
432     /**
433      * @return The previous song Id.
434      */
435     public static final long getPreviousAudioId() {
436         if (mService != null) {
437             try {
438                 return mService.getPreviousAudioId();
439             } catch (final RemoteException ignored) {
440             }
441         }
442         return -1;
443     }
444
445     /**
446      * @return The current artist Id.
447      */
448     public static final long getCurrentArtistId() {
449         if (mService != null) {
450             try {
451                 return mService.getArtistId();
452             } catch (final RemoteException ignored) {
453             }
454         }
455         return -1;
456     }
457
458     /**
459      * @return The audio session Id.
460      */
461     public static final int getAudioSessionId() {
462         if (mService != null) {
463             try {
464                 return mService.getAudioSessionId();
465             } catch (final RemoteException ignored) {
466             }
467         }
468         return -1;
469     }
470
471     /**
472      * @return The queue.
473      */
474     public static final long[] getQueue() {
475         try {
476             if (mService != null) {
477                 return mService.getQueue();
478             } else {
479             }
480         } catch (final RemoteException ignored) {
481         }
482         return sEmptyList;
483     }
484
485     /**
486      * @return The position of the current track in the queue.
487      */
488     public static final int getQueuePosition() {
489         try {
490             if (mService != null) {
491                 return mService.getQueuePosition();
492             }
493         } catch (final RemoteException ignored) {
494         }
495         return 0;
496     }
497
498     /**
499      * @return The queue history size
500      */
501     public static final int getQueueHistorySize() {
502         if (mService != null) {
503             try {
504                 return mService.getQueueHistorySize();
505             } catch (final RemoteException ignored) {
506             }
507         }
508         return 0;
509     }
510
511     /**
512      * @return The queue history
513      */
514     public static final int[] getQueueHistoryList() {
515         if (mService != null) {
516             try {
517                 return mService.getQueueHistoryList();
518             } catch (final RemoteException ignored) {
519             }
520         }
521         return null;
522     }
523
524     /**
525      * @param id The ID of the track to remove.
526      * @return removes track from a playlist or the queue.
527      */
528     public static final int removeTrack(final long id) {
529         try {
530             if (mService != null) {
531                 return mService.removeTrack(id);
532             }
533         } catch (final RemoteException ingored) {
534         }
535         return 0;
536     }
537
538     /**
539      * @param cursor The {@link Cursor} used to perform our query.
540      * @return The song list for a MIME type.
541      */
542     public static final long[] getSongListForCursor(Cursor cursor) {
543         if (cursor == null) {
544             return sEmptyList;
545         }
546         final int len = cursor.getCount();
547         final long[] list = new long[len];
548         cursor.moveToFirst();
549         int columnIndex = -1;
550         try {
551             columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.Members.AUDIO_ID);
552         } catch (final IllegalArgumentException notaplaylist) {
553             columnIndex = cursor.getColumnIndexOrThrow(BaseColumns._ID);
554         }
555         for (int i = 0; i < len; i++) {
556             list[i] = cursor.getLong(columnIndex);
557             cursor.moveToNext();
558         }
559         cursor.close();
560         cursor = null;
561         return list;
562     }
563
564     /**
565      * @param context The {@link Context} to use.
566      * @param id The ID of the artist.
567      * @return The song list for an artist.
568      */
569     public static final long[] getSongListForArtist(final Context context, final long id) {
570         final String[] projection = new String[] {
571             BaseColumns._ID
572         };
573         final String selection = AudioColumns.ARTIST_ID + "=" + id + " AND "
574                 + AudioColumns.IS_MUSIC + "=1";
575         Cursor cursor = context.getContentResolver().query(
576                 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null,
577                 AudioColumns.ALBUM_KEY + "," + AudioColumns.TRACK);
578         if (cursor != null) {
579             final long[] mList = getSongListForCursor(cursor);
580             cursor.close();
581             cursor = null;
582             return mList;
583         }
584         return sEmptyList;
585     }
586
587     /**
588      * @param context The {@link Context} to use.
589      * @param id The ID of the album.
590      * @return The song list for an album.
591      */
592     public static final long[] getSongListForAlbum(final Context context, final long id) {
593         final String[] projection = new String[] {
594             BaseColumns._ID
595         };
596         final String selection = AudioColumns.ALBUM_ID + "=" + id + " AND " + AudioColumns.IS_MUSIC
597                 + "=1";
598         Cursor cursor = context.getContentResolver().query(
599                 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null,
600                 AudioColumns.TRACK + ", " + MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
601         if (cursor != null) {
602             final long[] mList = getSongListForCursor(cursor);
603             cursor.close();
604             cursor = null;
605             return mList;
606         }
607         return sEmptyList;
608     }
609
610     /**
611      * Plays songs by an artist.
612      *
613      * @param context The {@link Context} to use.
614      * @param artistId The artist Id.
615      * @param position Specify where to start.
616      */
617     public static void playArtist(final Context context, final long artistId, int position) {
618         final long[] artistList = getSongListForArtist(context, artistId);
619         if (artistList != null) {
620             playAll(context, artistList, position, false);
621         }
622     }
623
624     /**
625      * @param context The {@link Context} to use.
626      * @param id The ID of the genre.
627      * @return The song list for an genre.
628      */
629     public static final long[] getSongListForGenre(final Context context, final long id) {
630         final String[] projection = new String[] {
631             BaseColumns._ID
632         };
633         final StringBuilder selection = new StringBuilder();
634         selection.append(AudioColumns.IS_MUSIC + "=1");
635         selection.append(" AND " + MediaColumns.TITLE + "!=''");
636         final Uri uri = MediaStore.Audio.Genres.Members.getContentUri("external", Long.valueOf(id));
637         Cursor cursor = context.getContentResolver().query(uri, projection, selection.toString(),
638                 null, null);
639         if (cursor != null) {
640             final long[] mList = getSongListForCursor(cursor);
641             cursor.close();
642             cursor = null;
643             return mList;
644         }
645         return sEmptyList;
646     }
647
648     /**
649      * @param context The {@link Context} to use
650      * @param uri The source of the file
651      */
652     public static void playFile(final Context context, final Uri uri) {
653         if (uri == null || mService == null) {
654             return;
655         }
656
657         // If this is a file:// URI, just use the path directly instead
658         // of going through the open-from-filedescriptor codepath.
659         String filename;
660         String scheme = uri.getScheme();
661         if ("file".equals(scheme)) {
662             filename = uri.getPath();
663         } else {
664             filename = uri.toString();
665         }
666
667         try {
668             mService.stop();
669             mService.openFile(filename);
670             mService.play();
671         } catch (final RemoteException ignored) {
672         }
673     }
674
675     /**
676      * @param context The {@link Context} to use.
677      * @param list The list of songs to play.
678      * @param position Specify where to start.
679      * @param forceShuffle True to force a shuffle, false otherwise.
680      */
681     public static void playAll(final Context context, final long[] list, int position,
682             final boolean forceShuffle) {
683         if (list.length == 0 || mService == null) {
684             return;
685         }
686         try {
687             if (forceShuffle) {
688                 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
689             } else {
690                 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
691             }
692             final long currentId = mService.getAudioId();
693             final int currentQueuePosition = getQueuePosition();
694             if (position != -1 && currentQueuePosition == position && currentId == list[position]) {
695                 final long[] playlist = getQueue();
696                 if (Arrays.equals(list, playlist)) {
697                     mService.play();
698                     return;
699                 }
700             }
701             if (position < 0) {
702                 position = 0;
703             }
704             mService.open(list, forceShuffle ? -1 : position);
705             mService.play();
706         } catch (final RemoteException ignored) {
707         }
708     }
709
710     /**
711      * @param list The list to enqueue.
712      */
713     public static void playNext(final long[] list) {
714         if (mService == null) {
715             return;
716         }
717         try {
718             mService.enqueue(list, MusicPlaybackService.NEXT);
719         } catch (final RemoteException ignored) {
720         }
721     }
722
723     /**
724      * @param context The {@link Context} to use.
725      */
726     public static void shuffleAll(final Context context) {
727         Cursor cursor = SongLoader.makeSongCursor(context);
728         final long[] mTrackList = getSongListForCursor(cursor);
729         final int position = 0;
730         if (mTrackList.length == 0 || mService == null) {
731             return;
732         }
733         try {
734             mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
735             final long mCurrentId = mService.getAudioId();
736             final int mCurrentQueuePosition = getQueuePosition();
737             if (position != -1 && mCurrentQueuePosition == position
738                     && mCurrentId == mTrackList[position]) {
739                 final long[] mPlaylist = getQueue();
740                 if (Arrays.equals(mTrackList, mPlaylist)) {
741                     mService.play();
742                     return;
743                 }
744             }
745             mService.open(mTrackList, -1);
746             mService.play();
747             cursor.close();
748             cursor = null;
749         } catch (final RemoteException ignored) {
750         }
751     }
752
753     /**
754      * Returns The ID for a playlist.
755      *
756      * @param context The {@link Context} to use.
757      * @param name The name of the playlist.
758      * @return The ID for a playlist.
759      */
760     public static final long getIdForPlaylist(final Context context, final String name) {
761         Cursor cursor = context.getContentResolver().query(
762                 MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, new String[] {
763                     BaseColumns._ID
764                 }, PlaylistsColumns.NAME + "=?", new String[] {
765                     name
766                 }, PlaylistsColumns.NAME);
767         int id = -1;
768         if (cursor != null) {
769             cursor.moveToFirst();
770             if (!cursor.isAfterLast()) {
771                 id = cursor.getInt(0);
772             }
773             cursor.close();
774             cursor = null;
775         }
776         return id;
777     }
778
779     /**
780      * Returns the Id for an artist.
781      *
782      * @param context The {@link Context} to use.
783      * @param name The name of the artist.
784      * @return The ID for an artist.
785      */
786     public static final long getIdForArtist(final Context context, final String name) {
787         Cursor cursor = context.getContentResolver().query(
788                 MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI, new String[] {
789                     BaseColumns._ID
790                 }, ArtistColumns.ARTIST + "=?", new String[] {
791                     name
792                 }, ArtistColumns.ARTIST);
793         int id = -1;
794         if (cursor != null) {
795             cursor.moveToFirst();
796             if (!cursor.isAfterLast()) {
797                 id = cursor.getInt(0);
798             }
799             cursor.close();
800             cursor = null;
801         }
802         return id;
803     }
804
805     /**
806      * Returns the ID for an album.
807      *
808      * @param context The {@link Context} to use.
809      * @param albumName The name of the album.
810      * @param artistName The name of the artist
811      * @return The ID for an album.
812      */
813     public static final long getIdForAlbum(final Context context, final String albumName,
814             final String artistName) {
815         Cursor cursor = context.getContentResolver().query(
816                 MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, new String[] {
817                     BaseColumns._ID
818                 }, AlbumColumns.ALBUM + "=? AND " + AlbumColumns.ARTIST + "=?", new String[] {
819                     albumName, artistName
820                 }, AlbumColumns.ALBUM);
821         int id = -1;
822         if (cursor != null) {
823             cursor.moveToFirst();
824             if (!cursor.isAfterLast()) {
825                 id = cursor.getInt(0);
826             }
827             cursor.close();
828             cursor = null;
829         }
830         return id;
831     }
832
833     /**
834      * Plays songs from an album.
835      *
836      * @param context The {@link Context} to use.
837      * @param albumId The album Id.
838      * @param position Specify where to start.
839      */
840     public static void playAlbum(final Context context, final long albumId, int position) {
841         final long[] albumList = getSongListForAlbum(context, albumId);
842         if (albumList != null) {
843             playAll(context, albumList, position, false);
844         }
845     }
846
847     /*  */
848     public static void makeInsertItems(final long[] ids, final int offset, int len, final int base) {
849         if (offset + len > ids.length) {
850             len = ids.length - offset;
851         }
852
853         if (mContentValuesCache == null || mContentValuesCache.length != len) {
854             mContentValuesCache = new ContentValues[len];
855         }
856         for (int i = 0; i < len; i++) {
857             if (mContentValuesCache[i] == null) {
858                 mContentValuesCache[i] = new ContentValues();
859             }
860             mContentValuesCache[i].put(Playlists.Members.PLAY_ORDER, base + offset + i);
861             mContentValuesCache[i].put(Playlists.Members.AUDIO_ID, ids[offset + i]);
862         }
863     }
864
865     /**
866      * @param context The {@link Context} to use.
867      * @param name The name of the new playlist.
868      * @return A new playlist ID.
869      */
870     public static final long createPlaylist(final Context context, final String name) {
871         if (name != null && name.length() > 0) {
872             final ContentResolver resolver = context.getContentResolver();
873             final String[] projection = new String[] {
874                 PlaylistsColumns.NAME
875             };
876             final String selection = PlaylistsColumns.NAME + " = '" + name + "'";
877             Cursor cursor = resolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
878                     projection, selection, null, null);
879             if (cursor.getCount() <= 0) {
880                 final ContentValues values = new ContentValues(1);
881                 values.put(PlaylistsColumns.NAME, name);
882                 final Uri uri = resolver.insert(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
883                         values);
884                 return Long.parseLong(uri.getLastPathSegment());
885             }
886             if (cursor != null) {
887                 cursor.close();
888                 cursor = null;
889             }
890             return -1;
891         }
892         return -1;
893     }
894
895     /**
896      * @param context The {@link Context} to use.
897      * @param playlistId The playlist ID.
898      */
899     public static void clearPlaylist(final Context context, final int playlistId) {
900         final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
901         context.getContentResolver().delete(uri, null, null);
902         return;
903     }
904
905     /**
906      * @param context The {@link Context} to use.
907      * @param ids The id of the song(s) to add.
908      * @param playlistid The id of the playlist being added to.
909      */
910     public static void addToPlaylist(final Context context, final long[] ids, final long playlistid) {
911         final int size = ids.length;
912         final ContentResolver resolver = context.getContentResolver();
913         final String[] projection = new String[] {
914             "count(*)"
915         };
916         final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistid);
917         Cursor cursor = resolver.query(uri, projection, null, null, null);
918         cursor.moveToFirst();
919         final int base = cursor.getInt(0);
920         cursor.close();
921         cursor = null;
922         int numinserted = 0;
923         for (int offSet = 0; offSet < size; offSet += 1000) {
924             makeInsertItems(ids, offSet, 1000, base);
925             numinserted += resolver.bulkInsert(uri, mContentValuesCache);
926         }
927         final String message = context.getResources().getQuantityString(
928                 R.plurals.NNNtrackstoplaylist, numinserted, numinserted);
929         AppMsg.makeText((Activity)context, message, AppMsg.STYLE_CONFIRM).show();
930     }
931
932     /**
933      * Removes a single track from a given playlist
934      * @param context The {@link Context} to use.
935      * @param id The id of the song to remove.
936      * @param playlistId The id of the playlist being removed from.
937      */
938     public static void removeFromPlaylist(final Context context, final long id,
939             final long playlistId) {
940         final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
941         final ContentResolver resolver = context.getContentResolver();
942         resolver.delete(uri, Playlists.Members.AUDIO_ID + " = ? ", new String[] {
943             Long.toString(id)
944         });
945         final String message = context.getResources().getQuantityString(
946                 R.plurals.NNNtracksfromplaylist, 1, 1);
947         AppMsg.makeText((Activity)context, message, AppMsg.STYLE_CONFIRM).show();
948     }
949
950     /**
951      * @param context The {@link Context} to use.
952      * @param list The list to enqueue.
953      */
954     public static void addToQueue(final Context context, final long[] list) {
955         if (mService == null) {
956             return;
957         }
958         try {
959             mService.enqueue(list, MusicPlaybackService.LAST);
960             final String message = makeLabel(context, R.plurals.NNNtrackstoqueue, list.length);
961             AppMsg.makeText((Activity)context, message, AppMsg.STYLE_CONFIRM).show();
962         } catch (final RemoteException ignored) {
963         }
964     }
965
966     /**
967      * @param context The {@link Context} to use
968      * @param id The song ID.
969      */
970     public static void setRingtone(final Context context, final long id) {
971         final ContentResolver resolver = context.getContentResolver();
972         final Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
973         try {
974             final ContentValues values = new ContentValues(2);
975             values.put(AudioColumns.IS_RINGTONE, "1");
976             values.put(AudioColumns.IS_ALARM, "1");
977             resolver.update(uri, values, null, null);
978         } catch (final UnsupportedOperationException ingored) {
979             return;
980         }
981
982         final String[] projection = new String[] {
983                 BaseColumns._ID, MediaColumns.DATA, MediaColumns.TITLE
984         };
985
986         final String selection = BaseColumns._ID + "=" + id;
987         Cursor cursor = resolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection,
988                 selection, null, null);
989         try {
990             if (cursor != null && cursor.getCount() == 1) {
991                 cursor.moveToFirst();
992                 Settings.System.putString(resolver, Settings.System.RINGTONE, uri.toString());
993                 final String message = context.getString(R.string.set_as_ringtone,
994                         cursor.getString(2));
995                 AppMsg.makeText((Activity)context, message, AppMsg.STYLE_CONFIRM).show();
996             }
997         } finally {
998             if (cursor != null) {
999                 cursor.close();
1000                 cursor = null;
1001             }
1002         }
1003     }
1004
1005     /**
1006      * @param context The {@link Context} to use.
1007      * @param id The id of the album.
1008      * @return The song count for an album.
1009      */
1010     public static final String getSongCountForAlbum(final Context context, final long id) {
1011         if (id == -1) {
1012             return null;
1013         }
1014         Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, id);
1015         Cursor cursor = context.getContentResolver().query(uri, new String[] {
1016                     AlbumColumns.NUMBER_OF_SONGS
1017                 }, null, null, null);
1018         String songCount = null;
1019         if (cursor != null) {
1020             cursor.moveToFirst();
1021             if (!cursor.isAfterLast()) {
1022                 songCount = cursor.getString(0);
1023             }
1024             cursor.close();
1025             cursor = null;
1026         }
1027         return songCount;
1028     }
1029
1030     /**
1031      * @param context The {@link Context} to use.
1032      * @param id The id of the album.
1033      * @return The release date for an album.
1034      */
1035     public static final String getReleaseDateForAlbum(final Context context, final long id) {
1036         if (id == -1) {
1037             return null;
1038         }
1039         Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, id);
1040         Cursor cursor = context.getContentResolver().query(uri, new String[] {
1041                     AlbumColumns.FIRST_YEAR
1042                 }, null, null, null);
1043         String releaseDate = null;
1044         if (cursor != null) {
1045             cursor.moveToFirst();
1046             if (!cursor.isAfterLast()) {
1047                 releaseDate = cursor.getString(0);
1048             }
1049             cursor.close();
1050             cursor = null;
1051         }
1052         return releaseDate;
1053     }
1054
1055     /**
1056      * @return The path to the currently playing file as {@link String}
1057      */
1058     public static final String getFilePath() {
1059         try {
1060             if (mService != null) {
1061                 return mService.getPath();
1062             }
1063         } catch (final RemoteException ignored) {
1064         }
1065         return null;
1066     }
1067
1068     /**
1069      * @param from The index the item is currently at.
1070      * @param to The index the item is moving to.
1071      */
1072     public static void moveQueueItem(final int from, final int to) {
1073         try {
1074             if (mService != null) {
1075                 mService.moveQueueItem(from, to);
1076             } else {
1077             }
1078         } catch (final RemoteException ignored) {
1079         }
1080     }
1081
1082     /**
1083      * @param context The {@link Context} to sue
1084      * @param playlistId The playlist Id
1085      * @return The track list for a playlist
1086      */
1087     public static final long[] getSongListForPlaylist(final Context context, final long playlistId) {
1088         final String[] projection = new String[] {
1089             MediaStore.Audio.Playlists.Members.AUDIO_ID
1090         };
1091         Cursor cursor = context.getContentResolver().query(
1092                 MediaStore.Audio.Playlists.Members.getContentUri("external",
1093                         Long.valueOf(playlistId)), projection, null, null,
1094                 MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER);
1095
1096         if (cursor != null) {
1097             final long[] list = getSongListForCursor(cursor);
1098             cursor.close();
1099             cursor = null;
1100             return list;
1101         }
1102         return sEmptyList;
1103     }
1104
1105     /**
1106      * Plays a user created playlist.
1107      *
1108      * @param context The {@link Context} to use.
1109      * @param playlistId The playlist Id.
1110      */
1111     public static void playPlaylist(final Context context, final long playlistId) {
1112         final long[] playlistList = getSongListForPlaylist(context, playlistId);
1113         if (playlistList != null) {
1114             playAll(context, playlistList, -1, false);
1115         }
1116     }
1117
1118     /**
1119      * @param context The {@link Context} to use
1120      * @return The song list for the last added playlist
1121      */
1122     public static final long[] getSongListForLastAdded(final Context context) {
1123         final Cursor cursor = LastAddedLoader.makeLastAddedCursor(context);
1124         if (cursor != null) {
1125             final int count = cursor.getCount();
1126             final long[] list = new long[count];
1127             for (int i = 0; i < count; i++) {
1128                 cursor.moveToNext();
1129                 list[i] = cursor.getLong(0);
1130             }
1131             return list;
1132         }
1133         return sEmptyList;
1134     }
1135
1136     /**
1137      * Plays the last added songs from the past two weeks.
1138      *
1139      * @param context The {@link Context} to use
1140      */
1141     public static void playLastAdded(final Context context) {
1142         playAll(context, getSongListForLastAdded(context), 0, false);
1143     }
1144
1145     /**
1146      * Creates a sub menu used to add items to a new playlist or an existsing
1147      * one.
1148      *
1149      * @param context The {@link Context} to use.
1150      * @param groupId The group Id of the menu.
1151      * @param subMenu The {@link SubMenu} to add to.
1152      */
1153     public static void makePlaylistMenu(final Context context, final int groupId,
1154             final SubMenu subMenu) {
1155         subMenu.clear();
1156         subMenu.add(groupId, FragmentMenuItems.NEW_PLAYLIST, Menu.NONE, R.string.new_playlist);
1157         Cursor cursor = PlaylistLoader.makePlaylistCursor(context);
1158         if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) {
1159             while (!cursor.isAfterLast()) {
1160                 final Intent intent = new Intent();
1161                 String name = cursor.getString(1);
1162                 if (name != null) {
1163                     intent.putExtra("playlist", getIdForPlaylist(context, name));
1164                     subMenu.add(groupId, FragmentMenuItems.PLAYLIST_SELECTED, Menu.NONE,
1165                             name).setIntent(intent);
1166                 }
1167                 cursor.moveToNext();
1168             }
1169         }
1170         if (cursor != null) {
1171             cursor.close();
1172             cursor = null;
1173         }
1174     }
1175
1176     /**
1177      * Called when one of the lists should refresh or requery.
1178      */
1179     public static void refresh() {
1180         try {
1181             if (mService != null) {
1182                 mService.refresh();
1183             }
1184         } catch (final RemoteException ignored) {
1185         }
1186     }
1187
1188     /**
1189      * Queries {@link RecentStore} for the last album played by an artist
1190      *
1191      * @param context The {@link Context} to use
1192      * @param artistName The artist name
1193      * @return The last album name played by an artist
1194      */
1195     public static final String getLastAlbumForArtist(final Context context, final String artistName) {
1196         return RecentStore.getInstance(context).getAlbumName(artistName);
1197     }
1198
1199     /**
1200      * Seeks the current track to a desired position
1201      *
1202      * @param position The position to seek to
1203      */
1204     public static void seek(final long position) {
1205         if (mService != null) {
1206             try {
1207                 mService.seek(position);
1208             } catch (final RemoteException ignored) {
1209             }
1210         }
1211     }
1212
1213     /**
1214      * @return The current position time of the track
1215      */
1216     public static final long position() {
1217         if (mService != null) {
1218             try {
1219                 return mService.position();
1220             } catch (final RemoteException ignored) {
1221             }
1222         }
1223         return 0;
1224     }
1225
1226     /**
1227      * @return The total length of the current track
1228      */
1229     public static final long duration() {
1230         if (mService != null) {
1231             try {
1232                 return mService.duration();
1233             } catch (final RemoteException ignored) {
1234             }
1235         }
1236         return 0;
1237     }
1238
1239     /**
1240      * @param position The position to move the queue to
1241      */
1242     public static void setQueuePosition(final int position) {
1243         if (mService != null) {
1244             try {
1245                 mService.setQueuePosition(position);
1246             } catch (final RemoteException ignored) {
1247             }
1248         }
1249     }
1250
1251     /**
1252      * Clears the qeueue
1253      */
1254     public static void clearQueue() {
1255         try {
1256             mService.removeTracks(0, Integer.MAX_VALUE);
1257         } catch (final RemoteException ignored) {
1258         }
1259     }
1260
1261     /**
1262      * Used to build and show a notification when Apollo is sent into the
1263      * background
1264      *
1265      * @param context The {@link Context} to use.
1266      */
1267     public static void notifyForegroundStateChanged(final Context context, boolean inForeground) {
1268         int old = sForegroundActivities;
1269         if (inForeground) {
1270             sForegroundActivities++;
1271         } else {
1272             sForegroundActivities--;
1273         }
1274
1275         if (old == 0 || sForegroundActivities == 0) {
1276             final Intent intent = new Intent(context, MusicPlaybackService.class);
1277             intent.setAction(MusicPlaybackService.FOREGROUND_STATE_CHANGED);
1278             intent.putExtra(MusicPlaybackService.NOW_IN_FOREGROUND, sForegroundActivities != 0);
1279             context.startService(intent);
1280         }
1281     }
1282
1283     /**
1284      * Perminately deletes item(s) from the user's device
1285      *
1286      * @param context The {@link Context} to use.
1287      * @param list The item(s) to delete.
1288      */
1289     public static void deleteTracks(final Context context, final long[] list) {
1290         final String[] projection = new String[] {
1291                 BaseColumns._ID, MediaColumns.DATA, AudioColumns.ALBUM_ID
1292         };
1293         final StringBuilder selection = new StringBuilder();
1294         selection.append(BaseColumns._ID + " IN (");
1295         for (int i = 0; i < list.length; i++) {
1296             selection.append(list[i]);
1297             if (i < list.length - 1) {
1298                 selection.append(",");
1299             }
1300         }
1301         selection.append(")");
1302         final Cursor c = context.getContentResolver().query(
1303                 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection.toString(),
1304                 null, null);
1305         if (c != null) {
1306             // Step 1: Remove selected tracks from the current playlist, as well
1307             // as from the album art cache
1308             c.moveToFirst();
1309             while (!c.isAfterLast()) {
1310                 // Remove from current playlist
1311                 final long id = c.getLong(0);
1312                 removeTrack(id);
1313                 // Remove any items in the recents database
1314                 RecentStore.getInstance(context).removeItem(c.getLong(2));
1315                 c.moveToNext();
1316             }
1317
1318             // Step 2: Remove selected tracks from the database
1319             context.getContentResolver().delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1320                     selection.toString(), null);
1321
1322             // Step 3: Remove files from card
1323             c.moveToFirst();
1324             while (!c.isAfterLast()) {
1325                 final String name = c.getString(1);
1326                 final File f = new File(name);
1327                 try { // File.delete can throw a security exception
1328                     if (!f.delete()) {
1329                         // I'm not sure if we'd ever get here (deletion would
1330                         // have to fail, but no exception thrown)
1331                         Log.e("MusicUtils", "Failed to delete file " + name);
1332                     }
1333                     c.moveToNext();
1334                 } catch (final SecurityException ex) {
1335                     c.moveToNext();
1336                 }
1337             }
1338             c.close();
1339         }
1340
1341         final String message = makeLabel(context, R.plurals.NNNtracksdeleted, list.length);
1342
1343         AppMsg.makeText((Activity)context, message, AppMsg.STYLE_CONFIRM).show();
1344         // We deleted a number of tracks, which could affect any number of
1345         // things
1346         // in the media content domain, so update everything.
1347         context.getContentResolver().notifyChange(Uri.parse("content://media"), null);
1348         // Notify the lists to update
1349         refresh();
1350     }
1351
1352     /**
1353      * Simple function used to determine if the song/album year is invalid
1354      * @param year value to test
1355      * @return true if the app considers it valid
1356      */
1357     public static boolean isInvalidYear(int year) {
1358         return year < MIN_VALID_YEAR;
1359     }
1360
1361     /**
1362      * A snippet is taken from MediaStore.Audio.keyFor method
1363      * This will take a name, removes things like "the", "an", etc
1364      * as well as special characters, then find the localized label
1365      * @param name Name to get the label of
1366      * @param trimName boolean flag to run the trimmer on the name
1367      * @return the localized label of the bucket that the name falls into
1368      */
1369     public static String getLocalizedBucketLetter(String name, boolean trimName) {
1370         if (trimName) {
1371             name = name.trim().toLowerCase();
1372             if (name.startsWith("the ")) {
1373                 name = name.substring(4);
1374             }
1375             if (name.startsWith("an ")) {
1376                 name = name.substring(3);
1377             }
1378             if (name.startsWith("a ")) {
1379                 name = name.substring(2);
1380             }
1381             if (name.endsWith(", the") || name.endsWith(",the") ||
1382                     name.endsWith(", an") || name.endsWith(",an") ||
1383                     name.endsWith(", a") || name.endsWith(",a")) {
1384                 name = name.substring(0, name.lastIndexOf(','));
1385             }
1386             name = name.replaceAll("[\\[\\]\\(\\)\"'.,?!]", "").trim();
1387         }
1388
1389         if (name.length() > 0) {
1390             return LocaleUtils.getInstance().getLabel(name);
1391         }
1392
1393         return null;
1394     }
1395 }