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.IBinder;
26 import android.os.RemoteException;
27 import android.provider.BaseColumns;
28 import android.provider.MediaStore;
29 import android.provider.MediaStore.Audio.AlbumColumns;
30 import android.provider.MediaStore.Audio.ArtistColumns;
31 import android.provider.MediaStore.Audio.AudioColumns;
32 import android.provider.MediaStore.Audio.Playlists;
33 import android.provider.MediaStore.Audio.PlaylistsColumns;
34 import android.provider.MediaStore.MediaColumns;
35 import android.provider.Settings;
36 import android.util.Log;
37 import android.view.Menu;
39 import com.cyngn.eleven.IElevenService;
40 import com.cyngn.eleven.MusicPlaybackService;
41 import com.cyngn.eleven.R;
42 import com.cyngn.eleven.loaders.LastAddedLoader;
43 import com.cyngn.eleven.loaders.PlaylistLoader;
44 import com.cyngn.eleven.loaders.SongLoader;
45 import com.cyngn.eleven.menu.FragmentMenuItems;
46 import com.cyngn.eleven.model.AlbumArtistDetails;
47 import com.cyngn.eleven.provider.RecentStore;
48 import com.cyngn.eleven.provider.SongPlayCount;
49 import com.devspark.appmsg.AppMsg;
52 import java.util.Arrays;
53 import java.util.WeakHashMap;
56 * A collection of helpers directly related to music or Apollo's service.
58 * @author Andrew Neal (andrewdneal@gmail.com)
60 public final class MusicUtils {
62 public static IElevenService mService = null;
64 private static int sForegroundActivities = 0;
66 private static final WeakHashMap<Context, ServiceBinder> mConnectionMap;
68 private static final long[] sEmptyList;
70 private static ContentValues[] mContentValuesCache = null;
72 private static final int MIN_VALID_YEAR = 1900; // used to remove invalid years from metadata
74 public static final String MUSIC_ONLY_SELECTION = MediaStore.Audio.AudioColumns.IS_MUSIC + "=1"
75 + " AND " + MediaStore.Audio.AudioColumns.TITLE + " != ''"; //$NON-NLS-2$
78 mConnectionMap = new WeakHashMap<Context, ServiceBinder>();
79 sEmptyList = new long[0];
82 /* This class is never initiated */
87 * @param context The {@link Context} to use
88 * @param callback The {@link ServiceConnection} to use
89 * @return The new instance of {@link ServiceToken}
91 public static final ServiceToken bindToService(final Context context,
92 final ServiceConnection callback) {
93 Activity realActivity = ((Activity)context).getParent();
94 if (realActivity == null) {
95 realActivity = (Activity)context;
97 final ContextWrapper contextWrapper = new ContextWrapper(realActivity);
98 contextWrapper.startService(new Intent(contextWrapper, MusicPlaybackService.class));
99 final ServiceBinder binder = new ServiceBinder(callback);
100 if (contextWrapper.bindService(
101 new Intent().setClass(contextWrapper, MusicPlaybackService.class), binder, 0)) {
102 mConnectionMap.put(contextWrapper, binder);
103 return new ServiceToken(contextWrapper);
109 * @param token The {@link ServiceToken} to unbind from
111 public static void unbindFromService(final ServiceToken token) {
115 final ContextWrapper mContextWrapper = token.mWrappedContext;
116 final ServiceBinder mBinder = mConnectionMap.remove(mContextWrapper);
117 if (mBinder == null) {
120 mContextWrapper.unbindService(mBinder);
121 if (mConnectionMap.isEmpty()) {
126 public static final class ServiceBinder implements ServiceConnection {
127 private final ServiceConnection mCallback;
130 * Constructor of <code>ServiceBinder</code>
132 * @param context The {@link ServiceConnection} to use
134 public ServiceBinder(final ServiceConnection callback) {
135 mCallback = callback;
139 public void onServiceConnected(final ComponentName className, final IBinder service) {
140 mService = IElevenService.Stub.asInterface(service);
141 if (mCallback != null) {
142 mCallback.onServiceConnected(className, service);
147 public void onServiceDisconnected(final ComponentName className) {
148 if (mCallback != null) {
149 mCallback.onServiceDisconnected(className);
155 public static final class ServiceToken {
156 public ContextWrapper mWrappedContext;
159 * Constructor of <code>ServiceToken</code>
161 * @param context The {@link ContextWrapper} to use
163 public ServiceToken(final ContextWrapper context) {
164 mWrappedContext = context;
169 * Used to make number of labels for the number of artists, albums, songs,
170 * genres, and playlists.
172 * @param context The {@link Context} to use.
173 * @param pluralInt The ID of the plural string to use.
174 * @param number The number of artists, albums, songs, genres, or playlists.
175 * @return A {@link String} used as a label for the number of artists,
176 * albums, songs, genres, and playlists.
178 public static final String makeLabel(final Context context, final int pluralInt,
180 return context.getResources().getQuantityString(pluralInt, number, number);
184 * * Used to create a formatted time string for the duration of tracks.
186 * @param context The {@link Context} to use.
187 * @param secs The track in seconds.
188 * @return Duration of a track that's properly formatted.
190 public static final String makeShortTimeString(final Context context, long secs) {
198 final String durationFormat = context.getResources().getString(
199 hours == 0 ? R.string.durationformatshort : R.string.durationformatlong);
200 return String.format(durationFormat, hours, mins, secs);
204 * * Used to create a formatted time string in the format of #d #h #m #s
206 * @param context The {@link Context} to use.
207 * @param secs The duration seconds.
208 * @return Duration properly formatted in #d #h #m #s format
210 public static final String makeLongTimeString(final Context context, long secs) {
211 long days, hours, mins;
213 days = secs / (3600 * 24);
220 int stringId = R.string.duration_mins;
222 stringId = R.string.duration_days;
223 } else if (hours != 0) {
224 stringId = R.string.duration_hours;
227 final String durationFormat = context.getResources().getString(stringId);
228 return String.format(durationFormat, days, hours, mins, secs);
232 * Used to combine two strings with some kind of separator in between
234 * @param context The {@link Context} to use.
235 * @param first string to combine
236 * @param second string to combine
237 * @return the combined string
239 public static final String makeCombinedString(final Context context, final String first,
240 final String second) {
241 final String formatter = context.getResources().getString(R.string.combine_two_strings);
242 return String.format(formatter, first, second);
246 * Changes to the next track
248 public static void next() {
250 if (mService != null) {
253 } catch (final RemoteException ignored) {
258 * Changes to the next track asynchronously
260 public static void asyncNext(final Context context) {
261 final Intent previous = new Intent(context, MusicPlaybackService.class);
262 previous.setAction(MusicPlaybackService.NEXT_ACTION);
263 context.startService(previous);
267 * Changes to the previous track.
269 * @NOTE The AIDL isn't used here in order to properly use the previous
270 * action. When the user is shuffling, because {@link
271 * MusicPlaybackService.#openCurrentAndNext()} is used, the user won't
272 * be able to travel to the previously skipped track. To remedy this,
273 * {@link MusicPlaybackService.#openCurrent()} is called in {@link
274 * MusicPlaybackService.#prev()}. {@code #startService(Intent intent)}
275 * is called here to specifically invoke the onStartCommand used by
276 * {@link MusicPlaybackService}, which states if the current position
277 * less than 2000 ms, start the track over, otherwise move to the
278 * previously listened track.
280 public static void previous(final Context context, final boolean force) {
281 final Intent previous = new Intent(context, MusicPlaybackService.class);
283 previous.setAction(MusicPlaybackService.PREVIOUS_FORCE_ACTION);
285 previous.setAction(MusicPlaybackService.PREVIOUS_ACTION);
287 context.startService(previous);
291 * Plays or pauses the music.
293 public static void playOrPause() {
295 if (mService != null) {
296 if (mService.isPlaying()) {
302 } catch (final Exception ignored) {
307 * Cycles through the repeat options.
309 public static void cycleRepeat() {
311 if (mService != null) {
312 switch (mService.getRepeatMode()) {
313 case MusicPlaybackService.REPEAT_NONE:
314 mService.setRepeatMode(MusicPlaybackService.REPEAT_ALL);
316 case MusicPlaybackService.REPEAT_ALL:
317 mService.setRepeatMode(MusicPlaybackService.REPEAT_CURRENT);
318 if (mService.getShuffleMode() != MusicPlaybackService.SHUFFLE_NONE) {
319 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
323 mService.setRepeatMode(MusicPlaybackService.REPEAT_NONE);
327 } catch (final RemoteException ignored) {
332 * Cycles through the shuffle options.
334 public static void cycleShuffle() {
336 if (mService != null) {
337 switch (mService.getShuffleMode()) {
338 case MusicPlaybackService.SHUFFLE_NONE:
339 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
340 if (mService.getRepeatMode() == MusicPlaybackService.REPEAT_CURRENT) {
341 mService.setRepeatMode(MusicPlaybackService.REPEAT_ALL);
344 case MusicPlaybackService.SHUFFLE_NORMAL:
345 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
347 case MusicPlaybackService.SHUFFLE_AUTO:
348 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
354 } catch (final RemoteException ignored) {
359 * @return True if we're playing music, false otherwise.
361 public static final boolean isPlaying() {
362 if (mService != null) {
364 return mService.isPlaying();
365 } catch (final RemoteException ignored) {
372 * @return The current shuffle mode.
374 public static final int getShuffleMode() {
375 if (mService != null) {
377 return mService.getShuffleMode();
378 } catch (final RemoteException ignored) {
385 * @return The current repeat mode.
387 public static final int getRepeatMode() {
388 if (mService != null) {
390 return mService.getRepeatMode();
391 } catch (final RemoteException ignored) {
398 * @return The current track name.
400 public static final String getTrackName() {
401 if (mService != null) {
403 return mService.getTrackName();
404 } catch (final RemoteException ignored) {
411 * @return The current artist name.
413 public static final String getArtistName() {
414 if (mService != null) {
416 return mService.getArtistName();
417 } catch (final RemoteException ignored) {
424 * @return The current album name.
426 public static final String getAlbumName() {
427 if (mService != null) {
429 return mService.getAlbumName();
430 } catch (final RemoteException ignored) {
437 * @return The current album Id.
439 public static final long getCurrentAlbumId() {
440 if (mService != null) {
442 return mService.getAlbumId();
443 } catch (final RemoteException ignored) {
450 * @return The current song Id.
452 public static final long getCurrentAudioId() {
453 if (mService != null) {
455 return mService.getAudioId();
456 } catch (final RemoteException ignored) {
463 * @return The next song Id.
465 public static final long getNextAudioId() {
466 if (mService != null) {
468 return mService.getNextAudioId();
469 } catch (final RemoteException ignored) {
476 * @return The previous song Id.
478 public static final long getPreviousAudioId() {
479 if (mService != null) {
481 return mService.getPreviousAudioId();
482 } catch (final RemoteException ignored) {
489 * @return The current artist Id.
491 public static final long getCurrentArtistId() {
492 if (mService != null) {
494 return mService.getArtistId();
495 } catch (final RemoteException ignored) {
502 * @return The audio session Id.
504 public static final int getAudioSessionId() {
505 if (mService != null) {
507 return mService.getAudioSessionId();
508 } catch (final RemoteException ignored) {
517 public static final long[] getQueue() {
519 if (mService != null) {
520 return mService.getQueue();
523 } catch (final RemoteException ignored) {
529 * @return The position of the current track in the queue.
531 public static final int getQueuePosition() {
533 if (mService != null) {
534 return mService.getQueuePosition();
536 } catch (final RemoteException ignored) {
542 * @return The queue history size
544 public static final int getQueueHistorySize() {
545 if (mService != null) {
547 return mService.getQueueHistorySize();
548 } catch (final RemoteException ignored) {
555 * @return The queue history
557 public static final int[] getQueueHistoryList() {
558 if (mService != null) {
560 return mService.getQueueHistoryList();
561 } catch (final RemoteException ignored) {
568 * @param id The ID of the track to remove.
569 * @return removes track from a playlist or the queue.
571 public static final int removeTrack(final long id) {
573 if (mService != null) {
574 return mService.removeTrack(id);
576 } catch (final RemoteException ingored) {
582 * @param cursor The {@link Cursor} used to perform our query.
583 * @return The song list for a MIME type.
585 public static final long[] getSongListForCursor(Cursor cursor) {
586 if (cursor == null) {
589 final int len = cursor.getCount();
590 final long[] list = new long[len];
591 cursor.moveToFirst();
592 int columnIndex = -1;
594 columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.Members.AUDIO_ID);
595 } catch (final IllegalArgumentException notaplaylist) {
596 columnIndex = cursor.getColumnIndexOrThrow(BaseColumns._ID);
598 for (int i = 0; i < len; i++) {
599 list[i] = cursor.getLong(columnIndex);
608 * @param context The {@link Context} to use.
609 * @param id The ID of the artist.
610 * @return The song list for an artist.
612 public static final long[] getSongListForArtist(final Context context, final long id) {
613 final String[] projection = new String[] {
616 final String selection = AudioColumns.ARTIST_ID + "=" + id + " AND "
617 + AudioColumns.IS_MUSIC + "=1";
618 Cursor cursor = context.getContentResolver().query(
619 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null,
620 AudioColumns.ALBUM_KEY + "," + AudioColumns.TRACK);
621 if (cursor != null) {
622 final long[] mList = getSongListForCursor(cursor);
631 * @param context The {@link Context} to use.
632 * @param id The ID of the album.
633 * @return The song list for an album.
635 public static final long[] getSongListForAlbum(final Context context, final long id) {
636 final String[] projection = new String[] {
639 final String selection = AudioColumns.ALBUM_ID + "=" + id + " AND " + AudioColumns.IS_MUSIC
641 Cursor cursor = context.getContentResolver().query(
642 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null,
643 AudioColumns.TRACK + ", " + MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
644 if (cursor != null) {
645 final long[] mList = getSongListForCursor(cursor);
654 * Plays songs by an artist.
656 * @param context The {@link Context} to use.
657 * @param artistId The artist Id.
658 * @param position Specify where to start.
660 public static void playArtist(final Context context, final long artistId, int position) {
661 final long[] artistList = getSongListForArtist(context, artistId);
662 if (artistList != null) {
663 playAll(context, artistList, position, false);
668 * @param context The {@link Context} to use.
669 * @param id The ID of the genre.
670 * @return The song list for an genre.
672 public static final long[] getSongListForGenre(final Context context, final long id) {
673 final String[] projection = new String[] {
676 final StringBuilder selection = new StringBuilder();
677 selection.append(AudioColumns.IS_MUSIC + "=1");
678 selection.append(" AND " + MediaColumns.TITLE + "!=''");
679 final Uri uri = MediaStore.Audio.Genres.Members.getContentUri("external", Long.valueOf(id));
680 Cursor cursor = context.getContentResolver().query(uri, projection, selection.toString(),
682 if (cursor != null) {
683 final long[] mList = getSongListForCursor(cursor);
692 * @param context The {@link Context} to use
693 * @param uri The source of the file
695 public static void playFile(final Context context, final Uri uri) {
696 if (uri == null || mService == null) {
700 // If this is a file:// URI, just use the path directly instead
701 // of going through the open-from-filedescriptor codepath.
703 String scheme = uri.getScheme();
704 if ("file".equals(scheme)) {
705 filename = uri.getPath();
707 filename = uri.toString();
712 mService.openFile(filename);
714 } catch (final RemoteException ignored) {
719 * @param context The {@link Context} to use.
720 * @param list The list of songs to play.
721 * @param position Specify where to start.
722 * @param forceShuffle True to force a shuffle, false otherwise.
724 public static void playAll(final Context context, final long[] list, int position,
725 final boolean forceShuffle) {
726 if (list.length == 0 || mService == null) {
731 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
733 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
735 final long currentId = mService.getAudioId();
736 final int currentQueuePosition = getQueuePosition();
737 if (position != -1 && currentQueuePosition == position && currentId == list[position]) {
738 final long[] playlist = getQueue();
739 if (Arrays.equals(list, playlist)) {
747 mService.open(list, forceShuffle ? -1 : position);
749 } catch (final RemoteException ignored) {
754 * @param list The list to enqueue.
756 public static void playNext(final long[] list) {
757 if (mService == null) {
761 mService.enqueue(list, MusicPlaybackService.NEXT);
762 } catch (final RemoteException ignored) {
767 * @param context The {@link Context} to use.
769 public static void shuffleAll(final Context context) {
770 Cursor cursor = SongLoader.makeSongCursor(context, null);
771 final long[] mTrackList = getSongListForCursor(cursor);
772 final int position = 0;
773 if (mTrackList.length == 0 || mService == null) {
777 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
778 final long mCurrentId = mService.getAudioId();
779 final int mCurrentQueuePosition = getQueuePosition();
780 if (position != -1 && mCurrentQueuePosition == position
781 && mCurrentId == mTrackList[position]) {
782 final long[] mPlaylist = getQueue();
783 if (Arrays.equals(mTrackList, mPlaylist)) {
788 mService.open(mTrackList, -1);
792 } catch (final RemoteException ignored) {
797 * Returns The ID for a playlist.
799 * @param context The {@link Context} to use.
800 * @param name The name of the playlist.
801 * @return The ID for a playlist.
803 public static final long getIdForPlaylist(final Context context, final String name) {
804 Cursor cursor = context.getContentResolver().query(
805 MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, new String[] {
807 }, PlaylistsColumns.NAME + "=?", new String[] {
809 }, PlaylistsColumns.NAME);
811 if (cursor != null) {
812 cursor.moveToFirst();
813 if (!cursor.isAfterLast()) {
814 id = cursor.getInt(0);
823 * Returns the Id for an artist.
825 * @param context The {@link Context} to use.
826 * @param name The name of the artist.
827 * @return The ID for an artist.
829 public static final long getIdForArtist(final Context context, final String name) {
830 Cursor cursor = context.getContentResolver().query(
831 MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI, new String[]{
833 }, ArtistColumns.ARTIST + "=?", new String[]{
835 }, ArtistColumns.ARTIST);
837 if (cursor != null) {
838 cursor.moveToFirst();
839 if (!cursor.isAfterLast()) {
840 id = cursor.getInt(0);
849 * Returns the ID for an album.
851 * @param context The {@link Context} to use.
852 * @param albumName The name of the album.
853 * @param artistName The name of the artist
854 * @return The ID for an album.
856 public static final long getIdForAlbum(final Context context, final String albumName,
857 final String artistName) {
858 Cursor cursor = context.getContentResolver().query(
859 MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, new String[] {
861 }, AlbumColumns.ALBUM + "=? AND " + AlbumColumns.ARTIST + "=?", new String[] {
862 albumName, artistName
863 }, AlbumColumns.ALBUM);
865 if (cursor != null) {
866 cursor.moveToFirst();
867 if (!cursor.isAfterLast()) {
868 id = cursor.getInt(0);
877 * Plays songs from an album.
879 * @param context The {@link Context} to use.
880 * @param albumId The album Id.
881 * @param position Specify where to start.
883 public static void playAlbum(final Context context, final long albumId, int position) {
884 final long[] albumList = getSongListForAlbum(context, albumId);
885 if (albumList != null) {
886 playAll(context, albumList, position, false);
891 public static void makeInsertItems(final long[] ids, final int offset, int len, final int base) {
892 if (offset + len > ids.length) {
893 len = ids.length - offset;
896 if (mContentValuesCache == null || mContentValuesCache.length != len) {
897 mContentValuesCache = new ContentValues[len];
899 for (int i = 0; i < len; i++) {
900 if (mContentValuesCache[i] == null) {
901 mContentValuesCache[i] = new ContentValues();
903 mContentValuesCache[i].put(Playlists.Members.PLAY_ORDER, base + offset + i);
904 mContentValuesCache[i].put(Playlists.Members.AUDIO_ID, ids[offset + i]);
909 * @param context The {@link Context} to use.
910 * @param name The name of the new playlist.
911 * @return A new playlist ID.
913 public static final long createPlaylist(final Context context, final String name) {
914 if (name != null && name.length() > 0) {
915 final ContentResolver resolver = context.getContentResolver();
916 final String[] projection = new String[] {
917 PlaylistsColumns.NAME
919 final String selection = PlaylistsColumns.NAME + " = '" + name + "'";
920 Cursor cursor = resolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
921 projection, selection, null, null);
922 if (cursor.getCount() <= 0) {
923 final ContentValues values = new ContentValues(1);
924 values.put(PlaylistsColumns.NAME, name);
925 final Uri uri = resolver.insert(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
927 return Long.parseLong(uri.getLastPathSegment());
929 if (cursor != null) {
939 * @param context The {@link Context} to use.
940 * @param playlistId The playlist ID.
942 public static void clearPlaylist(final Context context, final int playlistId) {
943 final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
944 context.getContentResolver().delete(uri, null, null);
949 * @param context The {@link Context} to use.
950 * @param ids The id of the song(s) to add.
951 * @param playlistid The id of the playlist being added to.
953 public static void addToPlaylist(final Context context, final long[] ids, final long playlistid) {
954 final int size = ids.length;
955 final ContentResolver resolver = context.getContentResolver();
956 final String[] projection = new String[] {
959 final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistid);
960 Cursor cursor = resolver.query(uri, projection, null, null, null);
961 cursor.moveToFirst();
962 final int base = cursor.getInt(0);
966 for (int offSet = 0; offSet < size; offSet += 1000) {
967 makeInsertItems(ids, offSet, 1000, base);
968 numinserted += resolver.bulkInsert(uri, mContentValuesCache);
970 final String message = context.getResources().getQuantityString(
971 R.plurals.NNNtrackstoplaylist, numinserted, numinserted);
972 AppMsg.makeText((Activity)context, message, AppMsg.STYLE_CONFIRM).show();
976 * Removes a single track from a given playlist
977 * @param context The {@link Context} to use.
978 * @param id The id of the song to remove.
979 * @param playlistId The id of the playlist being removed from.
981 public static void removeFromPlaylist(final Context context, final long id,
982 final long playlistId) {
983 final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
984 final ContentResolver resolver = context.getContentResolver();
985 resolver.delete(uri, Playlists.Members.AUDIO_ID + " = ? ", new String[] {
988 final String message = context.getResources().getQuantityString(
989 R.plurals.NNNtracksfromplaylist, 1, 1);
990 AppMsg.makeText((Activity)context, message, AppMsg.STYLE_CONFIRM).show();
994 * @param context The {@link Context} to use.
995 * @param list The list to enqueue.
997 public static void addToQueue(final Context context, final long[] list) {
998 if (mService == null) {
1002 mService.enqueue(list, MusicPlaybackService.LAST);
1003 final String message = makeLabel(context, R.plurals.NNNtrackstoqueue, list.length);
1004 AppMsg.makeText((Activity)context, message, AppMsg.STYLE_CONFIRM).show();
1005 } catch (final RemoteException ignored) {
1010 * @param context The {@link Context} to use
1011 * @param id The song ID.
1013 public static void setRingtone(final Context context, final long id) {
1014 final ContentResolver resolver = context.getContentResolver();
1015 final Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
1017 final ContentValues values = new ContentValues(2);
1018 values.put(AudioColumns.IS_RINGTONE, "1");
1019 values.put(AudioColumns.IS_ALARM, "1");
1020 resolver.update(uri, values, null, null);
1021 } catch (final UnsupportedOperationException ingored) {
1025 final String[] projection = new String[] {
1026 BaseColumns._ID, MediaColumns.DATA, MediaColumns.TITLE
1029 final String selection = BaseColumns._ID + "=" + id;
1030 Cursor cursor = resolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection,
1031 selection, null, null);
1033 if (cursor != null && cursor.getCount() == 1) {
1034 cursor.moveToFirst();
1035 Settings.System.putString(resolver, Settings.System.RINGTONE, uri.toString());
1036 final String message = context.getString(R.string.set_as_ringtone,
1037 cursor.getString(2));
1038 AppMsg.makeText((Activity)context, message, AppMsg.STYLE_CONFIRM).show();
1041 if (cursor != null) {
1048 public static final String getSongCountForAlbum(final Context context, final long id) {
1049 Integer i = getSongCountForAlbumInt(context, id);
1050 return i == null ? null : Integer.toString(i);
1054 * @param context The {@link Context} to use.
1055 * @param id The id of the album.
1056 * @return The song count for an album.
1058 public static final Integer getSongCountForAlbumInt(final Context context, final long id) {
1062 Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, id);
1063 Cursor cursor = context.getContentResolver().query(uri, new String[] {
1064 AlbumColumns.NUMBER_OF_SONGS
1065 }, null, null, null);
1066 Integer songCount = null;
1067 if (cursor != null) {
1068 cursor.moveToFirst();
1069 if (!cursor.isAfterLast()) {
1070 if(!cursor.isNull(0)) {
1071 songCount = cursor.getInt(0);
1081 * Gets the number of songs for a playlist
1082 * @param context The {@link Context} to use.
1083 * @param playlistId the id of the playlist
1084 * @return the # of songs in the playlist
1086 public static final int getSongCountForPlaylist(final Context context, final long playlistId) {
1087 Cursor c = context.getContentResolver().query(
1088 MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId),
1089 new String[]{BaseColumns._ID}, MusicUtils.MUSIC_ONLY_SELECTION, null, null);
1091 if (c != null && c.moveToFirst()) {
1092 int count = c.getCount();
1101 public static final AlbumArtistDetails getAlbumArtDetails(final Context context, final long trackId) {
1102 final StringBuilder selection = new StringBuilder();
1103 selection.append(MediaStore.Audio.AudioColumns.IS_MUSIC + "=1");
1104 selection.append(" AND " + BaseColumns._ID + " = '" + trackId + "'");
1106 Cursor cursor = context.getContentResolver().query(
1107 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1110 MediaStore.Audio.AudioColumns.ALBUM_ID,
1112 MediaStore.Audio.AudioColumns.ALBUM,
1114 MediaStore.Audio.AlbumColumns.ARTIST,
1115 }, selection.toString(), null, null
1118 if (!cursor.moveToFirst()) {
1123 AlbumArtistDetails result = new AlbumArtistDetails();
1124 result.mAudioId = trackId;
1125 result.mAlbumId = cursor.getLong(0);
1126 result.mAlbumName = cursor.getString(1);
1127 result.mArtistName = cursor.getString(2);
1134 * @param context The {@link Context} to use.
1135 * @param id The id of the album.
1136 * @return The release date for an album.
1138 public static final String getReleaseDateForAlbum(final Context context, final long id) {
1142 Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, id);
1143 Cursor cursor = context.getContentResolver().query(uri, new String[] {
1144 AlbumColumns.FIRST_YEAR
1145 }, null, null, null);
1146 String releaseDate = null;
1147 if (cursor != null) {
1148 cursor.moveToFirst();
1149 if (!cursor.isAfterLast()) {
1150 releaseDate = cursor.getString(0);
1159 * @return The path to the currently playing file as {@link String}
1161 public static final String getFilePath() {
1163 if (mService != null) {
1164 return mService.getPath();
1166 } catch (final RemoteException ignored) {
1172 * @param from The index the item is currently at.
1173 * @param to The index the item is moving to.
1175 public static void moveQueueItem(final int from, final int to) {
1177 if (mService != null) {
1178 mService.moveQueueItem(from, to);
1181 } catch (final RemoteException ignored) {
1186 * @param context The {@link Context} to sue
1187 * @param playlistId The playlist Id
1188 * @return The track list for a playlist
1190 public static final long[] getSongListForPlaylist(final Context context, final long playlistId) {
1191 final String[] projection = new String[] {
1192 MediaStore.Audio.Playlists.Members.AUDIO_ID
1194 Cursor cursor = context.getContentResolver().query(
1195 MediaStore.Audio.Playlists.Members.getContentUri("external",
1196 Long.valueOf(playlistId)), projection, null, null,
1197 MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER);
1199 if (cursor != null) {
1200 final long[] list = getSongListForCursor(cursor);
1209 * Plays a user created playlist.
1211 * @param context The {@link Context} to use.
1212 * @param playlistId The playlist Id.
1214 public static void playPlaylist(final Context context, final long playlistId) {
1215 final long[] playlistList = getSongListForPlaylist(context, playlistId);
1216 if (playlistList != null) {
1217 playAll(context, playlistList, -1, false);
1222 * @param context The {@link Context} to use
1223 * @return The song list for the last added playlist
1225 public static final long[] getSongListForLastAdded(final Context context) {
1226 final Cursor cursor = LastAddedLoader.makeLastAddedCursor(context);
1227 if (cursor != null) {
1228 final int count = cursor.getCount();
1229 final long[] list = new long[count];
1230 for (int i = 0; i < count; i++) {
1231 cursor.moveToNext();
1232 list[i] = cursor.getLong(0);
1240 * Plays the last added songs from the past two weeks.
1242 * @param context The {@link Context} to use
1244 public static void playLastAdded(final Context context) {
1245 playAll(context, getSongListForLastAdded(context), 0, false);
1249 * Creates a sub menu used to add items to a new playlist or an existsing
1252 * @param context The {@link Context} to use.
1253 * @param groupId The group Id of the menu.
1254 * @param menu The {@link Menu} to add to.
1256 public static void makePlaylistMenu(final Context context, final int groupId,
1259 menu.add(groupId, FragmentMenuItems.NEW_PLAYLIST, Menu.NONE, R.string.new_playlist);
1260 Cursor cursor = PlaylistLoader.makePlaylistCursor(context);
1261 if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) {
1262 while (!cursor.isAfterLast()) {
1263 final Intent intent = new Intent();
1264 String name = cursor.getString(1);
1266 intent.putExtra("playlist", getIdForPlaylist(context, name));
1267 menu.add(groupId, FragmentMenuItems.PLAYLIST_SELECTED, Menu.NONE,
1268 name).setIntent(intent);
1270 cursor.moveToNext();
1273 if (cursor != null) {
1280 * Called when one of the lists should refresh or requery.
1282 public static void refresh() {
1284 if (mService != null) {
1287 } catch (final RemoteException ignored) {
1292 * Queries {@link RecentStore} for the last album played by an artist
1294 * @param context The {@link Context} to use
1295 * @param artistName The artist name
1296 * @return The last album name played by an artist
1298 public static final String getLastAlbumForArtist(final Context context, final String artistName) {
1299 return RecentStore.getInstance(context).getAlbumName(artistName);
1303 * Seeks the current track to a desired position
1305 * @param position The position to seek to
1307 public static void seek(final long position) {
1308 if (mService != null) {
1310 mService.seek(position);
1311 } catch (final RemoteException ignored) {
1317 * @return The current position time of the track
1319 public static final long position() {
1320 if (mService != null) {
1322 return mService.position();
1323 } catch (final RemoteException ignored) {
1324 } catch (final IllegalStateException ex) {
1325 Log.e(MusicUtils.class.getSimpleName(), ex.getMessage());
1332 * @return The total length of the current track
1334 public static final long duration() {
1335 if (mService != null) {
1337 return mService.duration();
1338 } catch (final RemoteException ignored) {
1339 } catch (final IllegalStateException ignored) {
1346 * @param position The position to move the queue to
1348 public static void setQueuePosition(final int position) {
1349 if (mService != null) {
1351 mService.setQueuePosition(position);
1352 } catch (final RemoteException ignored) {
1360 public static void clearQueue() {
1362 mService.removeTracks(0, Integer.MAX_VALUE);
1363 } catch (final RemoteException ignored) {
1368 * Used to build and show a notification when Apollo is sent into the
1371 * @param context The {@link Context} to use.
1373 public static void notifyForegroundStateChanged(final Context context, boolean inForeground) {
1374 int old = sForegroundActivities;
1376 sForegroundActivities++;
1378 sForegroundActivities--;
1381 if (old == 0 || sForegroundActivities == 0) {
1382 final Intent intent = new Intent(context, MusicPlaybackService.class);
1383 intent.setAction(MusicPlaybackService.FOREGROUND_STATE_CHANGED);
1384 intent.putExtra(MusicPlaybackService.NOW_IN_FOREGROUND, sForegroundActivities != 0);
1385 context.startService(intent);
1390 * Perminately deletes item(s) from the user's device
1392 * @param context The {@link Context} to use.
1393 * @param list The item(s) to delete.
1395 public static void deleteTracks(final Context context, final long[] list) {
1396 final String[] projection = new String[] {
1397 BaseColumns._ID, MediaColumns.DATA, AudioColumns.ALBUM_ID
1399 final StringBuilder selection = new StringBuilder();
1400 selection.append(BaseColumns._ID + " IN (");
1401 for (int i = 0; i < list.length; i++) {
1402 selection.append(list[i]);
1403 if (i < list.length - 1) {
1404 selection.append(",");
1407 selection.append(")");
1408 final Cursor c = context.getContentResolver().query(
1409 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection.toString(),
1412 // Step 1: Remove selected tracks from the current playlist, as well
1413 // as from the album art cache
1415 while (!c.isAfterLast()) {
1416 // Remove from current playlist
1417 final long id = c.getLong(0);
1419 // Remove the track from the play count
1420 SongPlayCount.getInstance(context).removeItem(id);
1421 // Remove any items in the recents database
1422 RecentStore.getInstance(context).removeItem(c.getLong(2));
1426 // Step 2: Remove selected tracks from the database
1427 context.getContentResolver().delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1428 selection.toString(), null);
1430 // Step 3: Remove files from card
1432 while (!c.isAfterLast()) {
1433 final String name = c.getString(1);
1434 final File f = new File(name);
1435 try { // File.delete can throw a security exception
1437 // I'm not sure if we'd ever get here (deletion would
1438 // have to fail, but no exception thrown)
1439 Log.e("MusicUtils", "Failed to delete file " + name);
1442 } catch (final SecurityException ex) {
1449 final String message = makeLabel(context, R.plurals.NNNtracksdeleted, list.length);
1451 AppMsg.makeText((Activity)context, message, AppMsg.STYLE_CONFIRM).show();
1452 // We deleted a number of tracks, which could affect any number of
1454 // in the media content domain, so update everything.
1455 context.getContentResolver().notifyChange(Uri.parse("content://media"), null);
1456 // Notify the lists to update
1461 * Simple function used to determine if the song/album year is invalid
1462 * @param year value to test
1463 * @return true if the app considers it valid
1465 public static boolean isInvalidYear(int year) {
1466 return year < MIN_VALID_YEAR;
1470 * A snippet is taken from MediaStore.Audio.keyFor method
1471 * This will take a name, removes things like "the", "an", etc
1472 * as well as special characters and return it
1473 * @param name the string to trim
1474 * @return the trimmed name
1476 public static String getTrimmedName(String name) {
1477 if (name == null || name.length() == 0) {
1481 name = name.trim().toLowerCase();
1482 if (name.startsWith("the ")) {
1483 name = name.substring(4);
1485 if (name.startsWith("an ")) {
1486 name = name.substring(3);
1488 if (name.startsWith("a ")) {
1489 name = name.substring(2);
1491 if (name.endsWith(", the") || name.endsWith(",the") ||
1492 name.endsWith(", an") || name.endsWith(",an") ||
1493 name.endsWith(", a") || name.endsWith(",a")) {
1494 name = name.substring(0, name.lastIndexOf(','));
1496 name = name.replaceAll("[\\[\\]\\(\\)\"'.,?!]", "").trim();
1502 * A snippet is taken from MediaStore.Audio.keyFor method
1503 * This will take a name, removes things like "the", "an", etc
1504 * as well as special characters, then find the localized label
1505 * @param name Name to get the label of
1506 * @param trimName boolean flag to run the trimmer on the name
1507 * @return the localized label of the bucket that the name falls into
1509 public static String getLocalizedBucketLetter(String name, boolean trimName) {
1510 if (name == null || name.length() == 0) {
1515 name = getTrimmedName(name);
1518 if (name.length() > 0) {
1519 String lbl = LocaleUtils.getInstance().getLabel(name);
1520 // For now let's cap it to latin alphabet and the # sign
1521 // since chinese characters are resulting in " " and other random
1522 // characters but the sort doesn't match the sql sort so it is
1524 if (lbl != null && lbl.length() > 0) {
1525 char ch = lbl.charAt(0);
1526 if (ch < 'A' && ch > 'Z' && ch != '#') {
1531 if (lbl != null && lbl.length() > 0) {
1539 /** @return true if a string is null, empty, or contains only whitespace */
1540 public static boolean isBlank(String s) {
1541 if(s == null) { return true; }
1542 if(s.isEmpty()) { return true; }
1543 for(int i = 0; i < s.length(); i++) {
1544 char c = s.charAt(i);
1545 if(!Character.isWhitespace(c)) { return false; }