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.Config;
40 import com.cyngn.eleven.Config.SmartPlaylistType;
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.PlaylistSongLoader;
47 import com.cyngn.eleven.loaders.SongLoader;
48 import com.cyngn.eleven.loaders.TopTracksLoader;
49 import com.cyngn.eleven.menu.FragmentMenuItems;
50 import com.cyngn.eleven.model.AlbumArtistDetails;
51 import com.cyngn.eleven.provider.RecentStore;
52 import com.cyngn.eleven.provider.SongPlayCount;
53 import com.devspark.appmsg.AppMsg;
56 import java.util.Arrays;
57 import java.util.WeakHashMap;
60 * A collection of helpers directly related to music or Apollo's service.
62 * @author Andrew Neal (andrewdneal@gmail.com)
64 public final class MusicUtils {
66 public static IElevenService mService = null;
68 private static int sForegroundActivities = 0;
70 private static final WeakHashMap<Context, ServiceBinder> mConnectionMap;
72 private static final long[] sEmptyList;
74 private static ContentValues[] mContentValuesCache = null;
76 private static final int MIN_VALID_YEAR = 1900; // used to remove invalid years from metadata
78 public static final String MUSIC_ONLY_SELECTION = MediaStore.Audio.AudioColumns.IS_MUSIC + "=1"
79 + " AND " + MediaStore.Audio.AudioColumns.TITLE + " != ''"; //$NON-NLS-2$
82 mConnectionMap = new WeakHashMap<Context, ServiceBinder>();
83 sEmptyList = new long[0];
86 /* This class is never initiated */
91 * @param context The {@link Context} to use
92 * @param callback The {@link ServiceConnection} to use
93 * @return The new instance of {@link ServiceToken}
95 public static final ServiceToken bindToService(final Context context,
96 final ServiceConnection callback) {
97 Activity realActivity = ((Activity)context).getParent();
98 if (realActivity == null) {
99 realActivity = (Activity)context;
101 final ContextWrapper contextWrapper = new ContextWrapper(realActivity);
102 contextWrapper.startService(new Intent(contextWrapper, MusicPlaybackService.class));
103 final ServiceBinder binder = new ServiceBinder(callback);
104 if (contextWrapper.bindService(
105 new Intent().setClass(contextWrapper, MusicPlaybackService.class), binder, 0)) {
106 mConnectionMap.put(contextWrapper, binder);
107 return new ServiceToken(contextWrapper);
113 * @param token The {@link ServiceToken} to unbind from
115 public static void unbindFromService(final ServiceToken token) {
119 final ContextWrapper mContextWrapper = token.mWrappedContext;
120 final ServiceBinder mBinder = mConnectionMap.remove(mContextWrapper);
121 if (mBinder == null) {
124 mContextWrapper.unbindService(mBinder);
125 if (mConnectionMap.isEmpty()) {
130 public static final class ServiceBinder implements ServiceConnection {
131 private final ServiceConnection mCallback;
134 * Constructor of <code>ServiceBinder</code>
136 * @param context The {@link ServiceConnection} to use
138 public ServiceBinder(final ServiceConnection callback) {
139 mCallback = callback;
143 public void onServiceConnected(final ComponentName className, final IBinder service) {
144 mService = IElevenService.Stub.asInterface(service);
145 if (mCallback != null) {
146 mCallback.onServiceConnected(className, service);
151 public void onServiceDisconnected(final ComponentName className) {
152 if (mCallback != null) {
153 mCallback.onServiceDisconnected(className);
159 public static final class ServiceToken {
160 public ContextWrapper mWrappedContext;
163 * Constructor of <code>ServiceToken</code>
165 * @param context The {@link ContextWrapper} to use
167 public ServiceToken(final ContextWrapper context) {
168 mWrappedContext = context;
173 * Used to make number of labels for the number of artists, albums, songs,
174 * genres, and playlists.
176 * @param context The {@link Context} to use.
177 * @param pluralInt The ID of the plural string to use.
178 * @param number The number of artists, albums, songs, genres, or playlists.
179 * @return A {@link String} used as a label for the number of artists,
180 * albums, songs, genres, and playlists.
182 public static final String makeLabel(final Context context, final int pluralInt,
184 return context.getResources().getQuantityString(pluralInt, number, number);
188 * * Used to create a formatted time string for the duration of tracks.
190 * @param context The {@link Context} to use.
191 * @param secs The track in seconds.
192 * @return Duration of a track that's properly formatted.
194 public static final String makeShortTimeString(final Context context, long secs) {
202 final String durationFormat = context.getResources().getString(
203 hours == 0 ? R.string.durationformatshort : R.string.durationformatlong);
204 return String.format(durationFormat, hours, mins, secs);
208 * * Used to create a formatted time string in the format of #d #h #m #s
210 * @param context The {@link Context} to use.
211 * @param secs The duration seconds.
212 * @return Duration properly formatted in #d #h #m #s format
214 public static final String makeLongTimeString(final Context context, long secs) {
215 long days, hours, mins;
217 days = secs / (3600 * 24);
224 int stringId = R.string.duration_mins;
226 stringId = R.string.duration_days;
227 } else if (hours != 0) {
228 stringId = R.string.duration_hours;
231 final String durationFormat = context.getResources().getString(stringId);
232 return String.format(durationFormat, days, hours, mins, secs);
236 * Used to combine two strings with some kind of separator in between
238 * @param context The {@link Context} to use.
239 * @param first string to combine
240 * @param second string to combine
241 * @return the combined string
243 public static final String makeCombinedString(final Context context, final String first,
244 final String second) {
245 final String formatter = context.getResources().getString(R.string.combine_two_strings);
246 return String.format(formatter, first, second);
250 * Changes to the next track
252 public static void next() {
254 if (mService != null) {
257 } catch (final RemoteException ignored) {
262 * Changes to the next track asynchronously
264 public static void asyncNext(final Context context) {
265 final Intent previous = new Intent(context, MusicPlaybackService.class);
266 previous.setAction(MusicPlaybackService.NEXT_ACTION);
267 context.startService(previous);
271 * Changes to the previous track.
273 * @NOTE The AIDL isn't used here in order to properly use the previous
274 * action. When the user is shuffling, because {@link
275 * MusicPlaybackService.#openCurrentAndNext()} is used, the user won't
276 * be able to travel to the previously skipped track. To remedy this,
277 * {@link MusicPlaybackService.#openCurrent()} is called in {@link
278 * MusicPlaybackService.#prev()}. {@code #startService(Intent intent)}
279 * is called here to specifically invoke the onStartCommand used by
280 * {@link MusicPlaybackService}, which states if the current position
281 * less than 2000 ms, start the track over, otherwise move to the
282 * previously listened track.
284 public static void previous(final Context context, final boolean force) {
285 final Intent previous = new Intent(context, MusicPlaybackService.class);
287 previous.setAction(MusicPlaybackService.PREVIOUS_FORCE_ACTION);
289 previous.setAction(MusicPlaybackService.PREVIOUS_ACTION);
291 context.startService(previous);
295 * Plays or pauses the music.
297 public static void playOrPause() {
299 if (mService != null) {
300 if (mService.isPlaying()) {
306 } catch (final Exception ignored) {
311 * Cycles through the repeat options.
313 public static void cycleRepeat() {
315 if (mService != null) {
316 switch (mService.getRepeatMode()) {
317 case MusicPlaybackService.REPEAT_NONE:
318 mService.setRepeatMode(MusicPlaybackService.REPEAT_ALL);
320 case MusicPlaybackService.REPEAT_ALL:
321 mService.setRepeatMode(MusicPlaybackService.REPEAT_CURRENT);
322 if (mService.getShuffleMode() != MusicPlaybackService.SHUFFLE_NONE) {
323 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
327 mService.setRepeatMode(MusicPlaybackService.REPEAT_NONE);
331 } catch (final RemoteException ignored) {
336 * Cycles through the shuffle options.
338 public static void cycleShuffle() {
340 if (mService != null) {
341 switch (mService.getShuffleMode()) {
342 case MusicPlaybackService.SHUFFLE_NONE:
343 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
344 if (mService.getRepeatMode() == MusicPlaybackService.REPEAT_CURRENT) {
345 mService.setRepeatMode(MusicPlaybackService.REPEAT_ALL);
348 case MusicPlaybackService.SHUFFLE_NORMAL:
349 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
351 case MusicPlaybackService.SHUFFLE_AUTO:
352 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
358 } catch (final RemoteException ignored) {
363 * @return True if we're playing music, false otherwise.
365 public static final boolean isPlaying() {
366 if (mService != null) {
368 return mService.isPlaying();
369 } catch (final RemoteException ignored) {
376 * @return The current shuffle mode.
378 public static final int getShuffleMode() {
379 if (mService != null) {
381 return mService.getShuffleMode();
382 } catch (final RemoteException ignored) {
389 * @return The current repeat mode.
391 public static final int getRepeatMode() {
392 if (mService != null) {
394 return mService.getRepeatMode();
395 } catch (final RemoteException ignored) {
402 * @return The current track name.
404 public static final String getTrackName() {
405 if (mService != null) {
407 return mService.getTrackName();
408 } catch (final RemoteException ignored) {
415 * @return The current artist name.
417 public static final String getArtistName() {
418 if (mService != null) {
420 return mService.getArtistName();
421 } catch (final RemoteException ignored) {
428 * @return The current album name.
430 public static final String getAlbumName() {
431 if (mService != null) {
433 return mService.getAlbumName();
434 } catch (final RemoteException ignored) {
441 * @return The current album Id.
443 public static final long getCurrentAlbumId() {
444 if (mService != null) {
446 return mService.getAlbumId();
447 } catch (final RemoteException ignored) {
454 * @return The current song Id.
456 public static final long getCurrentAudioId() {
457 if (mService != null) {
459 return mService.getAudioId();
460 } catch (final RemoteException ignored) {
467 * @return The next song Id.
469 public static final long getNextAudioId() {
470 if (mService != null) {
472 return mService.getNextAudioId();
473 } catch (final RemoteException ignored) {
480 * @return The previous song Id.
482 public static final long getPreviousAudioId() {
483 if (mService != null) {
485 return mService.getPreviousAudioId();
486 } catch (final RemoteException ignored) {
493 * @return The current artist Id.
495 public static final long getCurrentArtistId() {
496 if (mService != null) {
498 return mService.getArtistId();
499 } catch (final RemoteException ignored) {
506 * @return The audio session Id.
508 public static final int getAudioSessionId() {
509 if (mService != null) {
511 return mService.getAudioSessionId();
512 } catch (final RemoteException ignored) {
521 public static final long[] getQueue() {
523 if (mService != null) {
524 return mService.getQueue();
527 } catch (final RemoteException ignored) {
533 * @return The position of the current track in the queue.
535 public static final int getQueuePosition() {
537 if (mService != null) {
538 return mService.getQueuePosition();
540 } catch (final RemoteException ignored) {
546 * @return The queue history size
548 public static final int getQueueHistorySize() {
549 if (mService != null) {
551 return mService.getQueueHistorySize();
552 } catch (final RemoteException ignored) {
559 * @return The queue history
561 public static final int[] getQueueHistoryList() {
562 if (mService != null) {
564 return mService.getQueueHistoryList();
565 } catch (final RemoteException ignored) {
572 * @param id The ID of the track to remove.
573 * @return removes track from a playlist or the queue.
575 public static final int removeTrack(final long id) {
577 if (mService != null) {
578 return mService.removeTrack(id);
580 } catch (final RemoteException ingored) {
586 * @param cursor The {@link Cursor} used to perform our query.
587 * @return The song list for a MIME type.
589 public static final long[] getSongListForCursor(Cursor cursor) {
590 if (cursor == null) {
593 final int len = cursor.getCount();
594 final long[] list = new long[len];
595 cursor.moveToFirst();
596 int columnIndex = -1;
598 columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.Members.AUDIO_ID);
599 } catch (final IllegalArgumentException notaplaylist) {
600 columnIndex = cursor.getColumnIndexOrThrow(BaseColumns._ID);
602 for (int i = 0; i < len; i++) {
603 list[i] = cursor.getLong(columnIndex);
612 * @param context The {@link Context} to use.
613 * @param id The ID of the artist.
614 * @return The song list for an artist.
616 public static final long[] getSongListForArtist(final Context context, final long id) {
617 final String[] projection = new String[] {
620 final String selection = AudioColumns.ARTIST_ID + "=" + id + " AND "
621 + AudioColumns.IS_MUSIC + "=1";
622 Cursor cursor = context.getContentResolver().query(
623 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null,
624 AudioColumns.ALBUM_KEY + "," + AudioColumns.TRACK);
625 if (cursor != null) {
626 final long[] mList = getSongListForCursor(cursor);
635 * @param context The {@link Context} to use.
636 * @param id The ID of the album.
637 * @return The song list for an album.
639 public static final long[] getSongListForAlbum(final Context context, final long id) {
640 final String[] projection = new String[] {
643 final String selection = AudioColumns.ALBUM_ID + "=" + id + " AND " + AudioColumns.IS_MUSIC
645 Cursor cursor = context.getContentResolver().query(
646 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null,
647 AudioColumns.TRACK + ", " + MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
648 if (cursor != null) {
649 final long[] mList = getSongListForCursor(cursor);
658 * Plays songs by an artist.
660 * @param context The {@link Context} to use.
661 * @param artistId The artist Id.
662 * @param position Specify where to start.
664 public static void playArtist(final Context context, final long artistId, int position) {
665 final long[] artistList = getSongListForArtist(context, artistId);
666 if (artistList != null) {
667 playAll(context, artistList, position, false);
672 * @param context The {@link Context} to use.
673 * @param id The ID of the genre.
674 * @return The song list for an genre.
676 public static final long[] getSongListForGenre(final Context context, final long id) {
677 final String[] projection = new String[] {
680 final StringBuilder selection = new StringBuilder();
681 selection.append(AudioColumns.IS_MUSIC + "=1");
682 selection.append(" AND " + MediaColumns.TITLE + "!=''");
683 final Uri uri = MediaStore.Audio.Genres.Members.getContentUri("external", Long.valueOf(id));
684 Cursor cursor = context.getContentResolver().query(uri, projection, selection.toString(),
686 if (cursor != null) {
687 final long[] mList = getSongListForCursor(cursor);
696 * @param context The {@link Context} to use
697 * @param uri The source of the file
699 public static void playFile(final Context context, final Uri uri) {
700 if (uri == null || mService == null) {
704 // If this is a file:// URI, just use the path directly instead
705 // of going through the open-from-filedescriptor codepath.
707 String scheme = uri.getScheme();
708 if ("file".equals(scheme)) {
709 filename = uri.getPath();
711 filename = uri.toString();
716 mService.openFile(filename);
718 } catch (final RemoteException ignored) {
723 * @param context The {@link Context} to use.
724 * @param list The list of songs to play.
725 * @param position Specify where to start.
726 * @param forceShuffle True to force a shuffle, false otherwise.
728 public static void playAll(final Context context, final long[] list, int position,
729 final boolean forceShuffle) {
730 if (list == null || list.length == 0 || mService == null) {
735 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
737 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
739 final long currentId = mService.getAudioId();
740 final int currentQueuePosition = getQueuePosition();
741 if (position != -1 && currentQueuePosition == position && currentId == list[position]) {
742 final long[] playlist = getQueue();
743 if (Arrays.equals(list, playlist)) {
751 mService.open(list, forceShuffle ? -1 : position);
753 } catch (final RemoteException ignored) {
758 * @param list The list to enqueue.
760 public static void playNext(final long[] list) {
761 if (mService == null) {
765 mService.enqueue(list, MusicPlaybackService.NEXT);
766 } catch (final RemoteException ignored) {
771 * @param context The {@link Context} to use.
773 public static void shuffleAll(final Context context) {
774 Cursor cursor = SongLoader.makeSongCursor(context, null);
775 final long[] mTrackList = getSongListForCursor(cursor);
776 final int position = 0;
777 if (mTrackList.length == 0 || mService == null) {
781 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
782 final long mCurrentId = mService.getAudioId();
783 final int mCurrentQueuePosition = getQueuePosition();
784 if (position != -1 && mCurrentQueuePosition == position
785 && mCurrentId == mTrackList[position]) {
786 final long[] mPlaylist = getQueue();
787 if (Arrays.equals(mTrackList, mPlaylist)) {
792 mService.open(mTrackList, -1);
796 } catch (final RemoteException ignored) {
801 * Returns The ID for a playlist.
803 * @param context The {@link Context} to use.
804 * @param name The name of the playlist.
805 * @return The ID for a playlist.
807 public static final long getIdForPlaylist(final Context context, final String name) {
808 Cursor cursor = context.getContentResolver().query(
809 MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, new String[] {
811 }, PlaylistsColumns.NAME + "=?", new String[] {
813 }, PlaylistsColumns.NAME);
815 if (cursor != null) {
816 cursor.moveToFirst();
817 if (!cursor.isAfterLast()) {
818 id = cursor.getInt(0);
827 * Returns the Id for an artist.
829 * @param context The {@link Context} to use.
830 * @param name The name of the artist.
831 * @return The ID for an artist.
833 public static final long getIdForArtist(final Context context, final String name) {
834 Cursor cursor = context.getContentResolver().query(
835 MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI, new String[]{
837 }, ArtistColumns.ARTIST + "=?", new String[]{
839 }, ArtistColumns.ARTIST);
841 if (cursor != null) {
842 cursor.moveToFirst();
843 if (!cursor.isAfterLast()) {
844 id = cursor.getInt(0);
853 * Returns the ID for an album.
855 * @param context The {@link Context} to use.
856 * @param albumName The name of the album.
857 * @param artistName The name of the artist
858 * @return The ID for an album.
860 public static final long getIdForAlbum(final Context context, final String albumName,
861 final String artistName) {
862 Cursor cursor = context.getContentResolver().query(
863 MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, new String[] {
865 }, AlbumColumns.ALBUM + "=? AND " + AlbumColumns.ARTIST + "=?", new String[] {
866 albumName, artistName
867 }, AlbumColumns.ALBUM);
869 if (cursor != null) {
870 cursor.moveToFirst();
871 if (!cursor.isAfterLast()) {
872 id = cursor.getInt(0);
881 * Plays songs from an album.
883 * @param context The {@link Context} to use.
884 * @param albumId The album Id.
885 * @param position Specify where to start.
887 public static void playAlbum(final Context context, final long albumId, int position) {
888 final long[] albumList = getSongListForAlbum(context, albumId);
889 if (albumList != null) {
890 playAll(context, albumList, position, false);
895 public static void makeInsertItems(final long[] ids, final int offset, int len, final int base) {
896 if (offset + len > ids.length) {
897 len = ids.length - offset;
900 if (mContentValuesCache == null || mContentValuesCache.length != len) {
901 mContentValuesCache = new ContentValues[len];
903 for (int i = 0; i < len; i++) {
904 if (mContentValuesCache[i] == null) {
905 mContentValuesCache[i] = new ContentValues();
907 mContentValuesCache[i].put(Playlists.Members.PLAY_ORDER, base + offset + i);
908 mContentValuesCache[i].put(Playlists.Members.AUDIO_ID, ids[offset + i]);
913 * @param context The {@link Context} to use.
914 * @param name The name of the new playlist.
915 * @return A new playlist ID.
917 public static final long createPlaylist(final Context context, final String name) {
918 if (name != null && name.length() > 0) {
919 final ContentResolver resolver = context.getContentResolver();
920 final String[] projection = new String[] {
921 PlaylistsColumns.NAME
923 final String selection = PlaylistsColumns.NAME + " = '" + name + "'";
924 Cursor cursor = resolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
925 projection, selection, null, null);
926 if (cursor.getCount() <= 0) {
927 final ContentValues values = new ContentValues(1);
928 values.put(PlaylistsColumns.NAME, name);
929 final Uri uri = resolver.insert(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
931 return Long.parseLong(uri.getLastPathSegment());
933 if (cursor != null) {
943 * @param context The {@link Context} to use.
944 * @param playlistId The playlist ID.
946 public static void clearPlaylist(final Context context, final int playlistId) {
947 final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
948 context.getContentResolver().delete(uri, null, null);
953 * @param context The {@link Context} to use.
954 * @param ids The id of the song(s) to add.
955 * @param playlistid The id of the playlist being added to.
957 public static void addToPlaylist(final Context context, final long[] ids, final long playlistid) {
958 final int size = ids.length;
959 final ContentResolver resolver = context.getContentResolver();
960 final String[] projection = new String[] {
963 final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistid);
964 Cursor cursor = resolver.query(uri, projection, null, null, null);
965 cursor.moveToFirst();
966 final int base = cursor.getInt(0);
970 for (int offSet = 0; offSet < size; offSet += 1000) {
971 makeInsertItems(ids, offSet, 1000, base);
972 numinserted += resolver.bulkInsert(uri, mContentValuesCache);
974 final String message = context.getResources().getQuantityString(
975 R.plurals.NNNtrackstoplaylist, numinserted, numinserted);
976 AppMsg.makeText((Activity)context, message, AppMsg.STYLE_CONFIRM).show();
981 * Removes a single track from a given playlist
982 * @param context The {@link Context} to use.
983 * @param id The id of the song to remove.
984 * @param playlistId The id of the playlist being removed from.
986 public static void removeFromPlaylist(final Context context, final long id,
987 final long playlistId) {
988 final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
989 final ContentResolver resolver = context.getContentResolver();
990 resolver.delete(uri, Playlists.Members.AUDIO_ID + " = ? ", new String[] {
993 final String message = context.getResources().getQuantityString(
994 R.plurals.NNNtracksfromplaylist, 1, 1);
995 AppMsg.makeText((Activity)context, message, AppMsg.STYLE_CONFIRM).show();
1000 * @param context The {@link Context} to use.
1001 * @param list The list to enqueue.
1003 public static void addToQueue(final Context context, final long[] list) {
1004 if (mService == null) {
1008 mService.enqueue(list, MusicPlaybackService.LAST);
1009 final String message = makeLabel(context, R.plurals.NNNtrackstoqueue, list.length);
1010 AppMsg.makeText((Activity)context, message, AppMsg.STYLE_CONFIRM).show();
1011 } catch (final RemoteException ignored) {
1016 * @param context The {@link Context} to use
1017 * @param id The song ID.
1019 public static void setRingtone(final Context context, final long id) {
1020 final ContentResolver resolver = context.getContentResolver();
1021 final Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
1023 final ContentValues values = new ContentValues(2);
1024 values.put(AudioColumns.IS_RINGTONE, "1");
1025 values.put(AudioColumns.IS_ALARM, "1");
1026 resolver.update(uri, values, null, null);
1027 } catch (final UnsupportedOperationException ingored) {
1031 final String[] projection = new String[] {
1032 BaseColumns._ID, MediaColumns.DATA, MediaColumns.TITLE
1035 final String selection = BaseColumns._ID + "=" + id;
1036 Cursor cursor = resolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection,
1037 selection, null, null);
1039 if (cursor != null && cursor.getCount() == 1) {
1040 cursor.moveToFirst();
1041 Settings.System.putString(resolver, Settings.System.RINGTONE, uri.toString());
1042 final String message = context.getString(R.string.set_as_ringtone,
1043 cursor.getString(2));
1044 AppMsg.makeText((Activity)context, message, AppMsg.STYLE_CONFIRM).show();
1047 if (cursor != null) {
1054 public static final String getSongCountForAlbum(final Context context, final long id) {
1055 Integer i = getSongCountForAlbumInt(context, id);
1056 return i == null ? null : Integer.toString(i);
1060 * @param context The {@link Context} to use.
1061 * @param id The id of the album.
1062 * @return The song count for an album.
1064 public static final Integer getSongCountForAlbumInt(final Context context, final long id) {
1068 Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, id);
1069 Cursor cursor = context.getContentResolver().query(uri, new String[] {
1070 AlbumColumns.NUMBER_OF_SONGS
1071 }, null, null, null);
1072 Integer songCount = null;
1073 if (cursor != null) {
1074 cursor.moveToFirst();
1075 if (!cursor.isAfterLast()) {
1076 if(!cursor.isNull(0)) {
1077 songCount = cursor.getInt(0);
1087 * Gets the number of songs for a playlist
1088 * @param context The {@link Context} to use.
1089 * @param playlistId the id of the playlist
1090 * @return the # of songs in the playlist
1092 public static final int getSongCountForPlaylist(final Context context, final long playlistId) {
1093 Cursor c = context.getContentResolver().query(
1094 MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId),
1095 new String[]{BaseColumns._ID}, MusicUtils.MUSIC_ONLY_SELECTION, null, null);
1097 if (c != null && c.moveToFirst()) {
1098 int count = c.getCount();
1107 public static final AlbumArtistDetails getAlbumArtDetails(final Context context, final long trackId) {
1108 final StringBuilder selection = new StringBuilder();
1109 selection.append(MediaStore.Audio.AudioColumns.IS_MUSIC + "=1");
1110 selection.append(" AND " + BaseColumns._ID + " = '" + trackId + "'");
1112 Cursor cursor = context.getContentResolver().query(
1113 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1116 MediaStore.Audio.AudioColumns.ALBUM_ID,
1118 MediaStore.Audio.AudioColumns.ALBUM,
1120 MediaStore.Audio.AlbumColumns.ARTIST,
1121 }, selection.toString(), null, null
1124 if (!cursor.moveToFirst()) {
1129 AlbumArtistDetails result = new AlbumArtistDetails();
1130 result.mAudioId = trackId;
1131 result.mAlbumId = cursor.getLong(0);
1132 result.mAlbumName = cursor.getString(1);
1133 result.mArtistName = cursor.getString(2);
1140 * @param context The {@link Context} to use.
1141 * @param id The id of the album.
1142 * @return The release date for an album.
1144 public static final String getReleaseDateForAlbum(final Context context, final long id) {
1148 Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, id);
1149 Cursor cursor = context.getContentResolver().query(uri, new String[] {
1150 AlbumColumns.FIRST_YEAR
1151 }, null, null, null);
1152 String releaseDate = null;
1153 if (cursor != null) {
1154 cursor.moveToFirst();
1155 if (!cursor.isAfterLast()) {
1156 releaseDate = cursor.getString(0);
1165 * @return The path to the currently playing file as {@link String}
1167 public static final String getFilePath() {
1169 if (mService != null) {
1170 return mService.getPath();
1172 } catch (final RemoteException ignored) {
1178 * @param from The index the item is currently at.
1179 * @param to The index the item is moving to.
1181 public static void moveQueueItem(final int from, final int to) {
1183 if (mService != null) {
1184 mService.moveQueueItem(from, to);
1187 } catch (final RemoteException ignored) {
1192 * @param context The {@link Context} to sue
1193 * @param playlistId The playlist Id
1194 * @return The track list for a playlist
1196 public static final long[] getSongListForPlaylist(final Context context, final long playlistId) {
1197 Cursor cursor = PlaylistSongLoader.makePlaylistSongCursor(context, playlistId);
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 * @param type The Smart Playlist Type
1224 * @return The song list for the last added playlist
1226 public static final long[] getSongListForSmartPlaylist(final Context context,
1227 final SmartPlaylistType type) {
1228 Cursor cursor = null;
1232 cursor = LastAddedLoader.makeLastAddedCursor(context);
1234 case RecentlyPlayed:
1235 cursor = TopTracksLoader.makeRecentTracksCursor(context);
1238 cursor = TopTracksLoader.makeTopTracksCursor(context);
1241 return MusicUtils.getSongListForCursor(cursor);
1243 if (cursor != null) {
1251 * Plays the smart playlist
1252 * @param context The {@link Context} to use
1253 * @param position the position to start playing from
1254 * @param type The Smart Playlist Type
1256 public static void playSmartPlaylist(final Context context, final int position,
1257 final SmartPlaylistType type) {
1258 final long[] list = getSongListForSmartPlaylist(context, type);
1259 MusicUtils.playAll(context, list, position, false);
1263 * Creates a sub menu used to add items to a new playlist or an existsing
1266 * @param context The {@link Context} to use.
1267 * @param groupId The group Id of the menu.
1268 * @param menu The {@link Menu} to add to.
1270 public static void makePlaylistMenu(final Context context, final int groupId,
1273 menu.add(groupId, FragmentMenuItems.NEW_PLAYLIST, Menu.NONE, R.string.new_playlist);
1274 Cursor cursor = PlaylistLoader.makePlaylistCursor(context);
1275 if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) {
1276 while (!cursor.isAfterLast()) {
1277 final Intent intent = new Intent();
1278 String name = cursor.getString(1);
1280 intent.putExtra("playlist", getIdForPlaylist(context, name));
1281 menu.add(groupId, FragmentMenuItems.PLAYLIST_SELECTED, Menu.NONE,
1282 name).setIntent(intent);
1284 cursor.moveToNext();
1287 if (cursor != null) {
1294 * Called when one of the lists should refresh or requery.
1296 public static void refresh() {
1298 if (mService != null) {
1301 } catch (final RemoteException ignored) {
1306 * Called when one of playlists have changed
1308 public static void playlistChanged() {
1310 if (mService != null) {
1311 mService.playlistChanged();
1313 } catch (final RemoteException ignored) {
1318 * Seeks the current track to a desired position
1320 * @param position The position to seek to
1322 public static void seek(final long position) {
1323 if (mService != null) {
1325 mService.seek(position);
1326 } catch (final RemoteException ignored) {
1332 * @return The current position time of the track
1334 public static final long position() {
1335 if (mService != null) {
1337 return mService.position();
1338 } catch (final RemoteException ignored) {
1339 } catch (final IllegalStateException ex) {
1340 Log.e(MusicUtils.class.getSimpleName(), ex.getMessage());
1347 * @return The total length of the current track
1349 public static final long duration() {
1350 if (mService != null) {
1352 return mService.duration();
1353 } catch (final RemoteException ignored) {
1354 } catch (final IllegalStateException ignored) {
1361 * @param position The position to move the queue to
1363 public static void setQueuePosition(final int position) {
1364 if (mService != null) {
1366 mService.setQueuePosition(position);
1367 } catch (final RemoteException ignored) {
1375 public static void clearQueue() {
1377 mService.removeTracks(0, Integer.MAX_VALUE);
1378 } catch (final RemoteException ignored) {
1383 * Used to build and show a notification when Apollo is sent into the
1386 * @param context The {@link Context} to use.
1388 public static void notifyForegroundStateChanged(final Context context, boolean inForeground) {
1389 int old = sForegroundActivities;
1391 sForegroundActivities++;
1393 sForegroundActivities--;
1396 if (old == 0 || sForegroundActivities == 0) {
1397 final Intent intent = new Intent(context, MusicPlaybackService.class);
1398 intent.setAction(MusicPlaybackService.FOREGROUND_STATE_CHANGED);
1399 intent.putExtra(MusicPlaybackService.NOW_IN_FOREGROUND, sForegroundActivities != 0);
1400 context.startService(intent);
1405 * Perminately deletes item(s) from the user's device
1407 * @param context The {@link Context} to use.
1408 * @param list The item(s) to delete.
1410 public static void deleteTracks(final Context context, final long[] list) {
1411 final String[] projection = new String[] {
1412 BaseColumns._ID, MediaColumns.DATA, AudioColumns.ALBUM_ID
1414 final StringBuilder selection = new StringBuilder();
1415 selection.append(BaseColumns._ID + " IN (");
1416 for (int i = 0; i < list.length; i++) {
1417 selection.append(list[i]);
1418 if (i < list.length - 1) {
1419 selection.append(",");
1422 selection.append(")");
1423 final Cursor c = context.getContentResolver().query(
1424 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection.toString(),
1427 // Step 1: Remove selected tracks from the current playlist, as well
1428 // as from the album art cache
1430 while (!c.isAfterLast()) {
1431 // Remove from current playlist
1432 final long id = c.getLong(0);
1434 // Remove the track from the play count
1435 SongPlayCount.getInstance(context).removeItem(id);
1436 // Remove any items in the recents database
1437 RecentStore.getInstance(context).removeItem(id);
1441 // Step 2: Remove selected tracks from the database
1442 context.getContentResolver().delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1443 selection.toString(), null);
1445 // Step 3: Remove files from card
1447 while (!c.isAfterLast()) {
1448 final String name = c.getString(1);
1449 final File f = new File(name);
1450 try { // File.delete can throw a security exception
1452 // I'm not sure if we'd ever get here (deletion would
1453 // have to fail, but no exception thrown)
1454 Log.e("MusicUtils", "Failed to delete file " + name);
1457 } catch (final SecurityException ex) {
1464 final String message = makeLabel(context, R.plurals.NNNtracksdeleted, list.length);
1466 AppMsg.makeText((Activity)context, message, AppMsg.STYLE_CONFIRM).show();
1467 // We deleted a number of tracks, which could affect any number of
1469 // in the media content domain, so update everything.
1470 context.getContentResolver().notifyChange(Uri.parse("content://media"), null);
1471 // Notify the lists to update
1476 * Simple function used to determine if the song/album year is invalid
1477 * @param year value to test
1478 * @return true if the app considers it valid
1480 public static boolean isInvalidYear(int year) {
1481 return year < MIN_VALID_YEAR;
1485 * A snippet is taken from MediaStore.Audio.keyFor method
1486 * This will take a name, removes things like "the", "an", etc
1487 * as well as special characters and return it
1488 * @param name the string to trim
1489 * @return the trimmed name
1491 public static String getTrimmedName(String name) {
1492 if (name == null || name.length() == 0) {
1496 name = name.trim().toLowerCase();
1497 if (name.startsWith("the ")) {
1498 name = name.substring(4);
1500 if (name.startsWith("an ")) {
1501 name = name.substring(3);
1503 if (name.startsWith("a ")) {
1504 name = name.substring(2);
1506 if (name.endsWith(", the") || name.endsWith(",the") ||
1507 name.endsWith(", an") || name.endsWith(",an") ||
1508 name.endsWith(", a") || name.endsWith(",a")) {
1509 name = name.substring(0, name.lastIndexOf(','));
1511 name = name.replaceAll("[\\[\\]\\(\\)\"'.,?!]", "").trim();
1517 * A snippet is taken from MediaStore.Audio.keyFor method
1518 * This will take a name, removes things like "the", "an", etc
1519 * as well as special characters, then find the localized label
1520 * @param name Name to get the label of
1521 * @param trimName boolean flag to run the trimmer on the name
1522 * @return the localized label of the bucket that the name falls into
1524 public static String getLocalizedBucketLetter(String name, boolean trimName) {
1525 if (name == null || name.length() == 0) {
1530 name = getTrimmedName(name);
1533 if (name.length() > 0) {
1534 String lbl = LocaleUtils.getInstance().getLabel(name);
1535 // For now let's cap it to latin alphabet and the # sign
1536 // since chinese characters are resulting in " " and other random
1537 // characters but the sort doesn't match the sql sort so it is
1539 if (lbl != null && lbl.length() > 0) {
1540 char ch = lbl.charAt(0);
1541 if (ch < 'A' && ch > 'Z' && ch != '#') {
1546 if (lbl != null && lbl.length() > 0) {
1554 /** @return true if a string is null, empty, or contains only whitespace */
1555 public static boolean isBlank(String s) {
1556 if(s == null) { return true; }
1557 if(s.isEmpty()) { return true; }
1558 for(int i = 0; i < s.length(); i++) {
1559 char c = s.charAt(i);
1560 if(!Character.isWhitespace(c)) { return false; }