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.
12 package com.cyngn.eleven.utils;
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;
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;
52 import java.util.Arrays;
53 import java.util.Formatter;
54 import java.util.Locale;
55 import java.util.WeakHashMap;
58 * A collection of helpers directly related to music or Apollo's service.
60 * @author Andrew Neal (andrewdneal@gmail.com)
62 public final class MusicUtils {
64 public static IElevenService mService = null;
66 private static int sForegroundActivities = 0;
68 private static final WeakHashMap<Context, ServiceBinder> mConnectionMap;
70 private static final long[] sEmptyList;
72 private static ContentValues[] mContentValuesCache = null;
74 private static final int MIN_VALID_YEAR = 1900; // used to remove invalid years from metadata
77 mConnectionMap = new WeakHashMap<Context, ServiceBinder>();
78 sEmptyList = new long[0];
81 /* This class is never initiated */
86 * @param context The {@link Context} to use
87 * @param callback The {@link ServiceConnection} to use
88 * @return The new instance of {@link ServiceToken}
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;
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);
108 * @param token The {@link ServiceToken} to unbind from
110 public static void unbindFromService(final ServiceToken token) {
114 final ContextWrapper mContextWrapper = token.mWrappedContext;
115 final ServiceBinder mBinder = mConnectionMap.remove(mContextWrapper);
116 if (mBinder == null) {
119 mContextWrapper.unbindService(mBinder);
120 if (mConnectionMap.isEmpty()) {
125 public static final class ServiceBinder implements ServiceConnection {
126 private final ServiceConnection mCallback;
129 * Constructor of <code>ServiceBinder</code>
131 * @param context The {@link ServiceConnection} to use
133 public ServiceBinder(final ServiceConnection callback) {
134 mCallback = callback;
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);
146 public void onServiceDisconnected(final ComponentName className) {
147 if (mCallback != null) {
148 mCallback.onServiceDisconnected(className);
154 public static final class ServiceToken {
155 public ContextWrapper mWrappedContext;
158 * Constructor of <code>ServiceToken</code>
160 * @param context The {@link ContextWrapper} to use
162 public ServiceToken(final ContextWrapper context) {
163 mWrappedContext = context;
168 * Used to make number of labels for the number of artists, albums, songs,
169 * genres, and playlists.
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.
177 public static final String makeLabel(final Context context, final int pluralInt,
179 return context.getResources().getQuantityString(pluralInt, number, number);
183 * * Used to create a formatted time string for the duration of tracks.
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.
189 public static final String makeTimeString(final Context context, long secs) {
193 secs -= hours * 3600;
197 final String durationFormat = context.getResources().getString(
198 hours == 0 ? R.string.durationformatshort : R.string.durationformatlong);
199 return String.format(durationFormat, hours, mins, secs);
203 * Changes to the next track
205 public static void next() {
207 if (mService != null) {
210 } catch (final RemoteException ignored) {
215 * Changes to the next track asynchronously
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);
224 * Changes to the previous track.
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.
237 public static void previous(final Context context, final boolean force) {
238 final Intent previous = new Intent(context, MusicPlaybackService.class);
240 previous.setAction(MusicPlaybackService.PREVIOUS_FORCE_ACTION);
242 previous.setAction(MusicPlaybackService.PREVIOUS_ACTION);
244 context.startService(previous);
248 * Plays or pauses the music.
250 public static void playOrPause() {
252 if (mService != null) {
253 if (mService.isPlaying()) {
259 } catch (final Exception ignored) {
264 * Cycles through the repeat options.
266 public static void cycleRepeat() {
268 if (mService != null) {
269 switch (mService.getRepeatMode()) {
270 case MusicPlaybackService.REPEAT_NONE:
271 mService.setRepeatMode(MusicPlaybackService.REPEAT_ALL);
273 case MusicPlaybackService.REPEAT_ALL:
274 mService.setRepeatMode(MusicPlaybackService.REPEAT_CURRENT);
275 if (mService.getShuffleMode() != MusicPlaybackService.SHUFFLE_NONE) {
276 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
280 mService.setRepeatMode(MusicPlaybackService.REPEAT_NONE);
284 } catch (final RemoteException ignored) {
289 * Cycles through the shuffle options.
291 public static void cycleShuffle() {
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);
301 case MusicPlaybackService.SHUFFLE_NORMAL:
302 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
304 case MusicPlaybackService.SHUFFLE_AUTO:
305 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
311 } catch (final RemoteException ignored) {
316 * @return True if we're playing music, false otherwise.
318 public static final boolean isPlaying() {
319 if (mService != null) {
321 return mService.isPlaying();
322 } catch (final RemoteException ignored) {
329 * @return The current shuffle mode.
331 public static final int getShuffleMode() {
332 if (mService != null) {
334 return mService.getShuffleMode();
335 } catch (final RemoteException ignored) {
342 * @return The current repeat mode.
344 public static final int getRepeatMode() {
345 if (mService != null) {
347 return mService.getRepeatMode();
348 } catch (final RemoteException ignored) {
355 * @return The current track name.
357 public static final String getTrackName() {
358 if (mService != null) {
360 return mService.getTrackName();
361 } catch (final RemoteException ignored) {
368 * @return The current artist name.
370 public static final String getArtistName() {
371 if (mService != null) {
373 return mService.getArtistName();
374 } catch (final RemoteException ignored) {
381 * @return The current album name.
383 public static final String getAlbumName() {
384 if (mService != null) {
386 return mService.getAlbumName();
387 } catch (final RemoteException ignored) {
394 * @return The current album Id.
396 public static final long getCurrentAlbumId() {
397 if (mService != null) {
399 return mService.getAlbumId();
400 } catch (final RemoteException ignored) {
407 * @return The current song Id.
409 public static final long getCurrentAudioId() {
410 if (mService != null) {
412 return mService.getAudioId();
413 } catch (final RemoteException ignored) {
420 * @return The next song Id.
422 public static final long getNextAudioId() {
423 if (mService != null) {
425 return mService.getNextAudioId();
426 } catch (final RemoteException ignored) {
433 * @return The previous song Id.
435 public static final long getPreviousAudioId() {
436 if (mService != null) {
438 return mService.getPreviousAudioId();
439 } catch (final RemoteException ignored) {
446 * @return The current artist Id.
448 public static final long getCurrentArtistId() {
449 if (mService != null) {
451 return mService.getArtistId();
452 } catch (final RemoteException ignored) {
459 * @return The audio session Id.
461 public static final int getAudioSessionId() {
462 if (mService != null) {
464 return mService.getAudioSessionId();
465 } catch (final RemoteException ignored) {
474 public static final long[] getQueue() {
476 if (mService != null) {
477 return mService.getQueue();
480 } catch (final RemoteException ignored) {
486 * @return The position of the current track in the queue.
488 public static final int getQueuePosition() {
490 if (mService != null) {
491 return mService.getQueuePosition();
493 } catch (final RemoteException ignored) {
499 * @return The queue history size
501 public static final int getQueueHistorySize() {
502 if (mService != null) {
504 return mService.getQueueHistorySize();
505 } catch (final RemoteException ignored) {
512 * @return The queue history
514 public static final int[] getQueueHistoryList() {
515 if (mService != null) {
517 return mService.getQueueHistoryList();
518 } catch (final RemoteException ignored) {
525 * @param id The ID of the track to remove.
526 * @return removes track from a playlist or the queue.
528 public static final int removeTrack(final long id) {
530 if (mService != null) {
531 return mService.removeTrack(id);
533 } catch (final RemoteException ingored) {
539 * @param cursor The {@link Cursor} used to perform our query.
540 * @return The song list for a MIME type.
542 public static final long[] getSongListForCursor(Cursor cursor) {
543 if (cursor == null) {
546 final int len = cursor.getCount();
547 final long[] list = new long[len];
548 cursor.moveToFirst();
549 int columnIndex = -1;
551 columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.Members.AUDIO_ID);
552 } catch (final IllegalArgumentException notaplaylist) {
553 columnIndex = cursor.getColumnIndexOrThrow(BaseColumns._ID);
555 for (int i = 0; i < len; i++) {
556 list[i] = cursor.getLong(columnIndex);
565 * @param context The {@link Context} to use.
566 * @param id The ID of the artist.
567 * @return The song list for an artist.
569 public static final long[] getSongListForArtist(final Context context, final long id) {
570 final String[] projection = new String[] {
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);
588 * @param context The {@link Context} to use.
589 * @param id The ID of the album.
590 * @return The song list for an album.
592 public static final long[] getSongListForAlbum(final Context context, final long id) {
593 final String[] projection = new String[] {
596 final String selection = AudioColumns.ALBUM_ID + "=" + id + " AND " + AudioColumns.IS_MUSIC
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);
611 * Plays songs by an artist.
613 * @param context The {@link Context} to use.
614 * @param artistId The artist Id.
615 * @param position Specify where to start.
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);
625 * @param context The {@link Context} to use.
626 * @param id The ID of the genre.
627 * @return The song list for an genre.
629 public static final long[] getSongListForGenre(final Context context, final long id) {
630 final String[] projection = new String[] {
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(),
639 if (cursor != null) {
640 final long[] mList = getSongListForCursor(cursor);
649 * @param context The {@link Context} to use
650 * @param uri The source of the file
652 public static void playFile(final Context context, final Uri uri) {
653 if (uri == null || mService == null) {
657 // If this is a file:// URI, just use the path directly instead
658 // of going through the open-from-filedescriptor codepath.
660 String scheme = uri.getScheme();
661 if ("file".equals(scheme)) {
662 filename = uri.getPath();
664 filename = uri.toString();
669 mService.openFile(filename);
671 } catch (final RemoteException ignored) {
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.
681 public static void playAll(final Context context, final long[] list, int position,
682 final boolean forceShuffle) {
683 if (list.length == 0 || mService == null) {
688 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
690 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
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)) {
704 mService.open(list, forceShuffle ? -1 : position);
706 } catch (final RemoteException ignored) {
711 * @param list The list to enqueue.
713 public static void playNext(final long[] list) {
714 if (mService == null) {
718 mService.enqueue(list, MusicPlaybackService.NEXT);
719 } catch (final RemoteException ignored) {
724 * @param context The {@link Context} to use.
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) {
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)) {
745 mService.open(mTrackList, -1);
749 } catch (final RemoteException ignored) {
754 * Returns The ID for a playlist.
756 * @param context The {@link Context} to use.
757 * @param name The name of the playlist.
758 * @return The ID for a playlist.
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[] {
764 }, PlaylistsColumns.NAME + "=?", new String[] {
766 }, PlaylistsColumns.NAME);
768 if (cursor != null) {
769 cursor.moveToFirst();
770 if (!cursor.isAfterLast()) {
771 id = cursor.getInt(0);
780 * Returns the Id for an artist.
782 * @param context The {@link Context} to use.
783 * @param name The name of the artist.
784 * @return The ID for an artist.
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[] {
790 }, ArtistColumns.ARTIST + "=?", new String[] {
792 }, ArtistColumns.ARTIST);
794 if (cursor != null) {
795 cursor.moveToFirst();
796 if (!cursor.isAfterLast()) {
797 id = cursor.getInt(0);
806 * Returns the ID for an album.
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.
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[] {
818 }, AlbumColumns.ALBUM + "=? AND " + AlbumColumns.ARTIST + "=?", new String[] {
819 albumName, artistName
820 }, AlbumColumns.ALBUM);
822 if (cursor != null) {
823 cursor.moveToFirst();
824 if (!cursor.isAfterLast()) {
825 id = cursor.getInt(0);
834 * Plays songs from an album.
836 * @param context The {@link Context} to use.
837 * @param albumId The album Id.
838 * @param position Specify where to start.
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);
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;
853 if (mContentValuesCache == null || mContentValuesCache.length != len) {
854 mContentValuesCache = new ContentValues[len];
856 for (int i = 0; i < len; i++) {
857 if (mContentValuesCache[i] == null) {
858 mContentValuesCache[i] = new ContentValues();
860 mContentValuesCache[i].put(Playlists.Members.PLAY_ORDER, base + offset + i);
861 mContentValuesCache[i].put(Playlists.Members.AUDIO_ID, ids[offset + i]);
866 * @param context The {@link Context} to use.
867 * @param name The name of the new playlist.
868 * @return A new playlist ID.
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
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,
884 return Long.parseLong(uri.getLastPathSegment());
886 if (cursor != null) {
896 * @param context The {@link Context} to use.
897 * @param playlistId The playlist ID.
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);
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.
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[] {
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);
923 for (int offSet = 0; offSet < size; offSet += 1000) {
924 makeInsertItems(ids, offSet, 1000, base);
925 numinserted += resolver.bulkInsert(uri, mContentValuesCache);
927 final String message = context.getResources().getQuantityString(
928 R.plurals.NNNtrackstoplaylist, numinserted, numinserted);
929 AppMsg.makeText((Activity)context, message, AppMsg.STYLE_CONFIRM).show();
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.
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[] {
945 final String message = context.getResources().getQuantityString(
946 R.plurals.NNNtracksfromplaylist, 1, 1);
947 AppMsg.makeText((Activity)context, message, AppMsg.STYLE_CONFIRM).show();
951 * @param context The {@link Context} to use.
952 * @param list The list to enqueue.
954 public static void addToQueue(final Context context, final long[] list) {
955 if (mService == null) {
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) {
967 * @param context The {@link Context} to use
968 * @param id The song ID.
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);
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) {
982 final String[] projection = new String[] {
983 BaseColumns._ID, MediaColumns.DATA, MediaColumns.TITLE
986 final String selection = BaseColumns._ID + "=" + id;
987 Cursor cursor = resolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection,
988 selection, null, null);
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();
998 if (cursor != null) {
1006 * @param context The {@link Context} to use.
1007 * @param id The id of the album.
1008 * @return The song count for an album.
1010 public static final String getSongCountForAlbum(final Context context, final long id) {
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);
1031 * @param context The {@link Context} to use.
1032 * @param id The id of the album.
1033 * @return The release date for an album.
1035 public static final String getReleaseDateForAlbum(final Context context, final long id) {
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);
1056 * @return The path to the currently playing file as {@link String}
1058 public static final String getFilePath() {
1060 if (mService != null) {
1061 return mService.getPath();
1063 } catch (final RemoteException ignored) {
1069 * @param from The index the item is currently at.
1070 * @param to The index the item is moving to.
1072 public static void moveQueueItem(final int from, final int to) {
1074 if (mService != null) {
1075 mService.moveQueueItem(from, to);
1078 } catch (final RemoteException ignored) {
1083 * @param context The {@link Context} to sue
1084 * @param playlistId The playlist Id
1085 * @return The track list for a playlist
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
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);
1096 if (cursor != null) {
1097 final long[] list = getSongListForCursor(cursor);
1106 * Plays a user created playlist.
1108 * @param context The {@link Context} to use.
1109 * @param playlistId The playlist Id.
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);
1119 * @param context The {@link Context} to use
1120 * @return The song list for the last added playlist
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);
1137 * Plays the last added songs from the past two weeks.
1139 * @param context The {@link Context} to use
1141 public static void playLastAdded(final Context context) {
1142 playAll(context, getSongListForLastAdded(context), 0, false);
1146 * Creates a sub menu used to add items to a new playlist or an existsing
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.
1153 public static void makePlaylistMenu(final Context context, final int groupId,
1154 final SubMenu subMenu) {
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);
1163 intent.putExtra("playlist", getIdForPlaylist(context, name));
1164 subMenu.add(groupId, FragmentMenuItems.PLAYLIST_SELECTED, Menu.NONE,
1165 name).setIntent(intent);
1167 cursor.moveToNext();
1170 if (cursor != null) {
1177 * Called when one of the lists should refresh or requery.
1179 public static void refresh() {
1181 if (mService != null) {
1184 } catch (final RemoteException ignored) {
1189 * Queries {@link RecentStore} for the last album played by an artist
1191 * @param context The {@link Context} to use
1192 * @param artistName The artist name
1193 * @return The last album name played by an artist
1195 public static final String getLastAlbumForArtist(final Context context, final String artistName) {
1196 return RecentStore.getInstance(context).getAlbumName(artistName);
1200 * Seeks the current track to a desired position
1202 * @param position The position to seek to
1204 public static void seek(final long position) {
1205 if (mService != null) {
1207 mService.seek(position);
1208 } catch (final RemoteException ignored) {
1214 * @return The current position time of the track
1216 public static final long position() {
1217 if (mService != null) {
1219 return mService.position();
1220 } catch (final RemoteException ignored) {
1227 * @return The total length of the current track
1229 public static final long duration() {
1230 if (mService != null) {
1232 return mService.duration();
1233 } catch (final RemoteException ignored) {
1240 * @param position The position to move the queue to
1242 public static void setQueuePosition(final int position) {
1243 if (mService != null) {
1245 mService.setQueuePosition(position);
1246 } catch (final RemoteException ignored) {
1254 public static void clearQueue() {
1256 mService.removeTracks(0, Integer.MAX_VALUE);
1257 } catch (final RemoteException ignored) {
1262 * Used to build and show a notification when Apollo is sent into the
1265 * @param context The {@link Context} to use.
1267 public static void notifyForegroundStateChanged(final Context context, boolean inForeground) {
1268 int old = sForegroundActivities;
1270 sForegroundActivities++;
1272 sForegroundActivities--;
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);
1284 * Perminately deletes item(s) from the user's device
1286 * @param context The {@link Context} to use.
1287 * @param list The item(s) to delete.
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
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(",");
1301 selection.append(")");
1302 final Cursor c = context.getContentResolver().query(
1303 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection.toString(),
1306 // Step 1: Remove selected tracks from the current playlist, as well
1307 // as from the album art cache
1309 while (!c.isAfterLast()) {
1310 // Remove from current playlist
1311 final long id = c.getLong(0);
1313 // Remove any items in the recents database
1314 RecentStore.getInstance(context).removeItem(c.getLong(2));
1318 // Step 2: Remove selected tracks from the database
1319 context.getContentResolver().delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1320 selection.toString(), null);
1322 // Step 3: Remove files from card
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
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);
1334 } catch (final SecurityException ex) {
1341 final String message = makeLabel(context, R.plurals.NNNtracksdeleted, list.length);
1343 AppMsg.makeText((Activity)context, message, AppMsg.STYLE_CONFIRM).show();
1344 // We deleted a number of tracks, which could affect any number of
1346 // in the media content domain, so update everything.
1347 context.getContentResolver().notifyChange(Uri.parse("content://media"), null);
1348 // Notify the lists to update
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
1357 public static boolean isInvalidYear(int year) {
1358 return year < MIN_VALID_YEAR;
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
1369 public static String getLocalizedBucketLetter(String name, boolean trimName) {
1371 name = name.trim().toLowerCase();
1372 if (name.startsWith("the ")) {
1373 name = name.substring(4);
1375 if (name.startsWith("an ")) {
1376 name = name.substring(3);
1378 if (name.startsWith("a ")) {
1379 name = name.substring(2);
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(','));
1386 name = name.replaceAll("[\\[\\]\\(\\)\"'.,?!]", "").trim();
1389 if (name.length() > 0) {
1390 return LocaleUtils.getInstance().getLabel(name);