2 * Copyright (C) 2012 Andrew Neal
3 * Copyright (C) 2014 The CyanogenMod Project
4 * Licensed under the Apache License, Version 2.0
5 * (the "License"); you may not use this file except in compliance with the
6 * License. You may obtain a copy of the License at
7 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
8 * or agreed to in writing, software distributed under the License is
9 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
10 * KIND, either express or implied. See the License for the specific language
11 * governing permissions and limitations under the License.
14 package com.cyanogenmod.eleven.utils;
16 import android.app.Activity;
17 import android.content.ComponentName;
18 import android.content.ContentResolver;
19 import android.content.ContentUris;
20 import android.content.ContentValues;
21 import android.content.Context;
22 import android.content.ContextWrapper;
23 import android.content.Intent;
24 import android.content.ServiceConnection;
25 import android.database.Cursor;
26 import android.net.Uri;
27 import android.os.AsyncTask;
28 import android.os.IBinder;
29 import android.os.RemoteException;
30 import android.os.SystemClock;
31 import android.provider.BaseColumns;
32 import android.provider.MediaStore;
33 import android.provider.MediaStore.Audio.AlbumColumns;
34 import android.provider.MediaStore.Audio.ArtistColumns;
35 import android.provider.MediaStore.Audio.AudioColumns;
36 import android.provider.MediaStore.Audio.Playlists;
37 import android.provider.MediaStore.Audio.PlaylistsColumns;
38 import android.provider.MediaStore.MediaColumns;
39 import android.provider.Settings;
40 import android.util.Log;
41 import android.view.Menu;
42 import android.widget.Toast;
44 import com.cyanogenmod.eleven.Config.IdType;
45 import com.cyanogenmod.eleven.Config.SmartPlaylistType;
46 import com.cyanogenmod.eleven.IElevenService;
47 import com.cyanogenmod.eleven.MusicPlaybackService;
48 import com.cyanogenmod.eleven.R;
49 import com.cyanogenmod.eleven.cache.ImageFetcher;
50 import com.cyanogenmod.eleven.loaders.LastAddedLoader;
51 import com.cyanogenmod.eleven.loaders.PlaylistLoader;
52 import com.cyanogenmod.eleven.loaders.PlaylistSongLoader;
53 import com.cyanogenmod.eleven.loaders.SongLoader;
54 import com.cyanogenmod.eleven.loaders.TopTracksLoader;
55 import com.cyanogenmod.eleven.locale.LocaleUtils;
56 import com.cyanogenmod.eleven.menu.FragmentMenuItems;
57 import com.cyanogenmod.eleven.model.Album;
58 import com.cyanogenmod.eleven.model.AlbumArtistDetails;
59 import com.cyanogenmod.eleven.model.Artist;
60 import com.cyanogenmod.eleven.model.Song;
61 import com.cyanogenmod.eleven.provider.RecentStore;
62 import com.cyanogenmod.eleven.provider.SongPlayCount;
63 import com.cyanogenmod.eleven.service.MusicPlaybackTrack;
66 import java.util.Arrays;
67 import java.util.Collection;
68 import java.util.Iterator;
69 import java.util.WeakHashMap;
72 * A collection of helpers directly related to music or Apollo's service.
74 * @author Andrew Neal (andrewdneal@gmail.com)
76 public final class MusicUtils {
78 public static IElevenService mService = null;
80 private static final WeakHashMap<Context, ServiceBinder> mConnectionMap;
82 private static final long[] sEmptyList;
84 private static ContentValues[] mContentValuesCache = null;
86 private static final int MIN_VALID_YEAR = 1900; // used to remove invalid years from metadata
88 public static final String MUSIC_ONLY_SELECTION = MediaStore.Audio.AudioColumns.IS_MUSIC + "=1"
89 + " AND " + MediaStore.Audio.AudioColumns.TITLE + " != ''"; //$NON-NLS-2$
91 public static final long UPDATE_FREQUENCY_MS = 500;
92 public static final long UPDATE_FREQUENCY_FAST_MS = 30;
95 mConnectionMap = new WeakHashMap<Context, ServiceBinder>();
96 sEmptyList = new long[0];
99 /* This class is never initiated */
100 public MusicUtils() {
104 * @param context The {@link Context} to use
105 * @param callback The {@link ServiceConnection} to use
106 * @return The new instance of {@link ServiceToken}
108 public static final ServiceToken bindToService(final Context context,
109 final ServiceConnection callback) {
110 Activity realActivity = ((Activity)context).getParent();
111 if (realActivity == null) {
112 realActivity = (Activity)context;
114 final ContextWrapper contextWrapper = new ContextWrapper(realActivity);
115 contextWrapper.startService(new Intent(contextWrapper, MusicPlaybackService.class));
116 final ServiceBinder binder = new ServiceBinder(callback,
117 contextWrapper.getApplicationContext());
118 if (contextWrapper.bindService(
119 new Intent().setClass(contextWrapper, MusicPlaybackService.class), binder, 0)) {
120 mConnectionMap.put(contextWrapper, binder);
121 return new ServiceToken(contextWrapper);
127 * @param token The {@link ServiceToken} to unbind from
129 public static void unbindFromService(final ServiceToken token) {
133 final ContextWrapper mContextWrapper = token.mWrappedContext;
134 final ServiceBinder mBinder = mConnectionMap.remove(mContextWrapper);
135 if (mBinder == null) {
138 mContextWrapper.unbindService(mBinder);
139 if (mConnectionMap.isEmpty()) {
144 public static final class ServiceBinder implements ServiceConnection {
145 private final ServiceConnection mCallback;
146 private final Context mContext;
149 * Constructor of <code>ServiceBinder</code>
151 * @param context The {@link ServiceConnection} to use
153 public ServiceBinder(final ServiceConnection callback, final Context context) {
154 mCallback = callback;
159 public void onServiceConnected(final ComponentName className, final IBinder service) {
160 mService = IElevenService.Stub.asInterface(service);
161 if (mCallback != null) {
162 mCallback.onServiceConnected(className, service);
164 MusicUtils.initPlaybackServiceWithSettings(mContext);
168 public void onServiceDisconnected(final ComponentName className) {
169 if (mCallback != null) {
170 mCallback.onServiceDisconnected(className);
176 public static final class ServiceToken {
177 public ContextWrapper mWrappedContext;
180 * Constructor of <code>ServiceToken</code>
182 * @param context The {@link ContextWrapper} to use
184 public ServiceToken(final ContextWrapper context) {
185 mWrappedContext = context;
189 public static final boolean isPlaybackServiceConnected() {
190 return mService != null;
194 * Used to make number of labels for the number of artists, albums, songs,
195 * genres, and playlists.
197 * @param context The {@link Context} to use.
198 * @param pluralInt The ID of the plural string to use.
199 * @param number The number of artists, albums, songs, genres, or playlists.
200 * @return A {@link String} used as a label for the number of artists,
201 * albums, songs, genres, and playlists.
203 public static final String makeLabel(final Context context, final int pluralInt,
205 return context.getResources().getQuantityString(pluralInt, number, number);
209 * * Used to create a formatted time string for the duration of tracks.
211 * @param context The {@link Context} to use.
212 * @param secs The track in seconds.
213 * @return Duration of a track that's properly formatted.
215 public static final String makeShortTimeString(final Context context, long secs) {
223 final String durationFormat = context.getResources().getString(
224 hours == 0 ? R.string.durationformatshort : R.string.durationformatlong);
225 return String.format(durationFormat, hours, mins, secs);
229 * Used to create a formatted time string in the format of #h #m or #m if there is only minutes
231 * @param context The {@link Context} to use.
232 * @param secs The duration seconds.
233 * @return Duration properly formatted in #h #m format
235 public static final String makeLongTimeString(final Context context, long secs) {
242 String hoursString = MusicUtils.makeLabel(context, R.plurals.Nhours, (int)hours);
243 String minutesString = MusicUtils.makeLabel(context, R.plurals.Nminutes, (int)mins);
246 return minutesString;
247 } else if (mins == 0) {
251 final String durationFormat = context.getResources().getString(R.string.duration_format);
252 return String.format(durationFormat, hoursString, minutesString);
256 * Used to combine two strings with some kind of separator in between
258 * @param context The {@link Context} to use.
259 * @param first string to combine
260 * @param second string to combine
261 * @return the combined string
263 public static final String makeCombinedString(final Context context, final String first,
264 final String second) {
265 final String formatter = context.getResources().getString(R.string.combine_two_strings);
266 return String.format(formatter, first, second);
270 * Changes to the next track
272 public static void next() {
274 if (mService != null) {
277 } catch (final RemoteException ignored) {
282 * Initialize playback service with values from Settings
284 public static void initPlaybackServiceWithSettings(final Context context) {
285 MusicUtils.setShakeToPlayEnabled(
286 PreferenceUtils.getInstance(context).getShakeToPlay());
287 MusicUtils.setShowAlbumArtOnLockscreen(
288 PreferenceUtils.getInstance(context).getShowAlbumArtOnLockscreen());
292 * Set shake to play status
294 public static void setShakeToPlayEnabled(final boolean enabled) {
296 if (mService != null) {
297 mService.setShakeToPlayEnabled(enabled);
299 } catch (final RemoteException ignored) {
304 * Set show album art on lockscreen
306 public static void setShowAlbumArtOnLockscreen(final boolean enabled) {
308 if (mService != null) {
309 mService.setLockscreenAlbumArt(enabled);
311 } catch (final RemoteException ignored) {
316 * Changes to the next track asynchronously
318 public static void asyncNext(final Context context) {
319 final Intent previous = new Intent(context, MusicPlaybackService.class);
320 previous.setAction(MusicPlaybackService.NEXT_ACTION);
321 context.startService(previous);
325 * Changes to the previous track.
327 * @NOTE The AIDL isn't used here in order to properly use the previous
328 * action. When the user is shuffling, because {@link
329 * MusicPlaybackService.#openCurrentAndNext()} is used, the user won't
330 * be able to travel to the previously skipped track. To remedy this,
331 * {@link MusicPlaybackService.#openCurrent()} is called in {@link
332 * MusicPlaybackService.#prev()}. {@code #startService(Intent intent)}
333 * is called here to specifically invoke the onStartCommand used by
334 * {@link MusicPlaybackService}, which states if the current position
335 * less than 2000 ms, start the track over, otherwise move to the
336 * previously listened track.
338 public static void previous(final Context context, final boolean force) {
339 final Intent previous = new Intent(context, MusicPlaybackService.class);
341 previous.setAction(MusicPlaybackService.PREVIOUS_FORCE_ACTION);
343 previous.setAction(MusicPlaybackService.PREVIOUS_ACTION);
345 context.startService(previous);
349 * Plays or pauses the music.
351 public static void playOrPause() {
353 if (mService != null) {
354 if (mService.isPlaying()) {
360 } catch (final Exception ignored) {
365 * Cycles through the repeat options.
367 public static void cycleRepeat() {
369 if (mService != null) {
370 switch (mService.getRepeatMode()) {
371 case MusicPlaybackService.REPEAT_NONE:
372 mService.setRepeatMode(MusicPlaybackService.REPEAT_ALL);
374 case MusicPlaybackService.REPEAT_ALL:
375 mService.setRepeatMode(MusicPlaybackService.REPEAT_CURRENT);
376 if (mService.getShuffleMode() != MusicPlaybackService.SHUFFLE_NONE) {
377 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
381 mService.setRepeatMode(MusicPlaybackService.REPEAT_NONE);
385 } catch (final RemoteException ignored) {
390 * Cycles through the shuffle options.
392 public static void cycleShuffle() {
394 if (mService != null) {
395 switch (mService.getShuffleMode()) {
396 case MusicPlaybackService.SHUFFLE_NONE:
397 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
398 if (mService.getRepeatMode() == MusicPlaybackService.REPEAT_CURRENT) {
399 mService.setRepeatMode(MusicPlaybackService.REPEAT_ALL);
402 case MusicPlaybackService.SHUFFLE_NORMAL:
403 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
405 case MusicPlaybackService.SHUFFLE_AUTO:
406 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
412 } catch (final RemoteException ignored) {
417 * @return True if we're playing music, false otherwise.
419 public static final boolean isPlaying() {
420 if (mService != null) {
422 return mService.isPlaying();
423 } catch (final RemoteException ignored) {
430 * @return The current shuffle mode.
432 public static final int getShuffleMode() {
433 if (mService != null) {
435 return mService.getShuffleMode();
436 } catch (final RemoteException ignored) {
443 * @return The current repeat mode.
445 public static final int getRepeatMode() {
446 if (mService != null) {
448 return mService.getRepeatMode();
449 } catch (final RemoteException ignored) {
456 * @return The current track name.
458 public static final String getTrackName() {
459 if (mService != null) {
461 return mService.getTrackName();
462 } catch (final RemoteException ignored) {
469 * @return The current artist name.
471 public static final String getArtistName() {
472 if (mService != null) {
474 return mService.getArtistName();
475 } catch (final RemoteException ignored) {
482 * @return The current album name.
484 public static final String getAlbumName() {
485 if (mService != null) {
487 return mService.getAlbumName();
488 } catch (final RemoteException ignored) {
495 * @return The current album Id.
497 public static final long getCurrentAlbumId() {
498 if (mService != null) {
500 return mService.getAlbumId();
501 } catch (final RemoteException ignored) {
508 * @return The current song Id.
510 public static final long getCurrentAudioId() {
511 if (mService != null) {
513 return mService.getAudioId();
514 } catch (final RemoteException ignored) {
521 * @return The current Music Playback Track
523 public static final MusicPlaybackTrack getCurrentTrack() {
524 if (mService != null) {
526 return mService.getCurrentTrack();
527 } catch (final RemoteException ignored) {
534 * @return The Music Playback Track at the specified index
536 public static final MusicPlaybackTrack getTrack(int index) {
537 if (mService != null) {
539 return mService.getTrack(index);
540 } catch (final RemoteException ignored) {
547 * @return The next song Id.
549 public static final long getNextAudioId() {
550 if (mService != null) {
552 return mService.getNextAudioId();
553 } catch (final RemoteException ignored) {
560 * @return The previous song Id.
562 public static final long getPreviousAudioId() {
563 if (mService != null) {
565 return mService.getPreviousAudioId();
566 } catch (final RemoteException ignored) {
573 * @return The current artist Id.
575 public static final long getCurrentArtistId() {
576 if (mService != null) {
578 return mService.getArtistId();
579 } catch (final RemoteException ignored) {
586 * @return The audio session Id.
588 public static final int getAudioSessionId() {
589 if (mService != null) {
591 return mService.getAudioSessionId();
592 } catch (final RemoteException ignored) {
601 public static final long[] getQueue() {
603 if (mService != null) {
604 return mService.getQueue();
607 } catch (final RemoteException ignored) {
614 * @return the id of the track in the queue at the given position
616 public static final long getQueueItemAtPosition(int position) {
618 if (mService != null) {
619 return mService.getQueueItemAtPosition(position);
622 } catch (final RemoteException ignored) {
628 * @return the current queue size
630 public static final int getQueueSize() {
632 if (mService != null) {
633 return mService.getQueueSize();
636 } catch (final RemoteException ignored) {
642 * @return The position of the current track in the queue.
644 public static final int getQueuePosition() {
646 if (mService != null) {
647 return mService.getQueuePosition();
649 } catch (final RemoteException ignored) {
655 * @return The queue history size
657 public static final int getQueueHistorySize() {
658 if (mService != null) {
660 return mService.getQueueHistorySize();
661 } catch (final RemoteException ignored) {
668 * @return The queue history position at the position
670 public static final int getQueueHistoryPosition(int position) {
671 if (mService != null) {
673 return mService.getQueueHistoryPosition(position);
674 } catch (final RemoteException ignored) {
681 * @return The queue history
683 public static final int[] getQueueHistoryList() {
684 if (mService != null) {
686 return mService.getQueueHistoryList();
687 } catch (final RemoteException ignored) {
694 * @param id The ID of the track to remove.
695 * @return removes track from a playlist or the queue.
697 public static final int removeTrack(final long id) {
699 if (mService != null) {
700 return mService.removeTrack(id);
702 } catch (final RemoteException ingored) {
708 * Remove song at a specified position in the list
710 * @param id The ID of the track to remove
711 * @param position The position of the song
713 * @return true if successful, false otherwise
715 public static final boolean removeTrackAtPosition(final long id, final int position) {
717 if (mService != null) {
718 return mService.removeTrackAtPosition(id, position);
720 } catch (final RemoteException ingored) {
726 * @param cursor The {@link Cursor} used to perform our query.
727 * @return The song list for a MIME type.
729 public static final long[] getSongListForCursor(Cursor cursor) {
730 if (cursor == null) {
733 final int len = cursor.getCount();
734 final long[] list = new long[len];
735 cursor.moveToFirst();
736 int columnIndex = -1;
738 columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.Members.AUDIO_ID);
739 } catch (final IllegalArgumentException notaplaylist) {
740 columnIndex = cursor.getColumnIndexOrThrow(BaseColumns._ID);
742 for (int i = 0; i < len; i++) {
743 list[i] = cursor.getLong(columnIndex);
752 * @param context The {@link Context} to use.
753 * @param id The ID of the artist.
754 * @return The song list for an artist.
756 public static final long[] getSongListForArtist(final Context context, final long id) {
757 final String[] projection = new String[] {
760 final String selection = AudioColumns.ARTIST_ID + "=" + id + " AND "
761 + AudioColumns.IS_MUSIC + "=1";
762 Cursor cursor = context.getContentResolver().query(
763 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null,
764 AudioColumns.ALBUM_KEY + "," + AudioColumns.TRACK);
765 if (cursor != null) {
766 final long[] mList = getSongListForCursor(cursor);
775 * @param context The {@link Context} to use.
776 * @param id The ID of the album.
777 * @return The song list for an album.
779 public static final long[] getSongListForAlbum(final Context context, final long id) {
780 final String[] projection = new String[] {
783 final String selection = AudioColumns.ALBUM_ID + "=" + id + " AND " + AudioColumns.IS_MUSIC
785 Cursor cursor = context.getContentResolver().query(
786 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null,
787 AudioColumns.TRACK + ", " + MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
788 if (cursor != null) {
789 final long[] mList = getSongListForCursor(cursor);
798 * Plays songs by an artist.
800 * @param context The {@link Context} to use.
801 * @param artistId The artist Id.
802 * @param position Specify where to start.
804 public static void playArtist(final Context context, final long artistId, int position, boolean shuffle) {
805 final long[] artistList = getSongListForArtist(context, artistId);
806 if (artistList != null) {
807 playAll(context, artistList, position, artistId, IdType.Artist, shuffle);
812 * @param context The {@link Context} to use.
813 * @param id The ID of the genre.
814 * @return The song list for an genre.
816 public static final long[] getSongListForGenre(final Context context, final long id) {
817 final String[] projection = new String[] {
820 String selection = (AudioColumns.IS_MUSIC + "=1") +
821 " AND " + MediaColumns.TITLE + "!=''";
822 final Uri uri = MediaStore.Audio.Genres.Members.getContentUri("external", Long.valueOf(id));
823 Cursor cursor = context.getContentResolver().query(uri, projection, selection,
825 if (cursor != null) {
826 final long[] mList = getSongListForCursor(cursor);
835 * @param context The {@link Context} to use
836 * @param uri The source of the file
838 public static void playFile(final Context context, final Uri uri) {
839 if (uri == null || mService == null) {
843 // If this is a file:// URI, just use the path directly instead
844 // of going through the open-from-filedescriptor codepath.
846 String scheme = uri.getScheme();
847 if ("file".equals(scheme)) {
848 filename = uri.getPath();
850 filename = uri.toString();
855 mService.openFile(filename);
857 } catch (final RemoteException ignored) {
862 * @param context The {@link Context} to use.
863 * @param list The list of songs to play.
864 * @param position Specify where to start.
865 * @param forceShuffle True to force a shuffle, false otherwise.
867 public static void playAll(final Context context, final long[] list, int position,
868 final long sourceId, final IdType sourceType,
869 final boolean forceShuffle) {
870 if (list == null || list.length == 0 || mService == null) {
875 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
880 mService.open(list, forceShuffle ? -1 : position, sourceId, sourceType.mId);
882 } catch (final RemoteException ignored) {
887 * @param list The list to enqueue.
889 public static void playNext(final long[] list, final long sourceId, final IdType sourceType) {
890 if (mService == null) {
894 mService.enqueue(list, MusicPlaybackService.NEXT, sourceId, sourceType.mId);
895 } catch (final RemoteException ignored) {
900 * @param context The {@link Context} to use.
902 public static void shuffleAll(final Context context) {
903 Cursor cursor = SongLoader.makeSongCursor(context, null);
904 final long[] mTrackList = getSongListForCursor(cursor);
905 if (mTrackList.length == 0 || mService == null) {
909 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
910 final long mCurrentId = mService.getAudioId();
911 final int mCurrentQueuePosition = getQueuePosition();
912 if (mCurrentQueuePosition == 0
913 && mCurrentId == mTrackList[0]) {
914 final long[] mPlaylist = getQueue();
915 if (Arrays.equals(mTrackList, mPlaylist)) {
920 mService.open(mTrackList, -1, -1, IdType.NA.mId);
924 } catch (final RemoteException ignored) {
929 * Returns The ID for a playlist.
931 * @param context The {@link Context} to use.
932 * @param name The name of the playlist.
933 * @return The ID for a playlist.
935 public static final long getIdForPlaylist(final Context context, final String name) {
936 Cursor cursor = context.getContentResolver().query(
937 MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, new String[]{
939 }, PlaylistsColumns.NAME + "=?", new String[]{
941 }, PlaylistsColumns.NAME);
943 if (cursor != null) {
944 cursor.moveToFirst();
945 if (!cursor.isAfterLast()) {
946 id = cursor.getInt(0);
954 /** @param context The {@link Context} to use.
955 * @param id The id of the playlist.
956 * @return The name for a playlist. */
957 public static final String getNameForPlaylist(final Context context, final long id) {
958 Cursor cursor = context.getContentResolver().query(
959 MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
960 new String[] { PlaylistsColumns.NAME },
961 BaseColumns._ID + "=?",
962 new String[] { Long.toString(id) },
964 if (cursor != null) {
966 if(cursor.moveToFirst()) { return cursor.getString(0); }
967 } finally { cursor.close(); }
974 * Returns the Id for an artist.
976 * @param context The {@link Context} to use.
977 * @param name The name of the artist.
978 * @return The ID for an artist.
980 public static final long getIdForArtist(final Context context, final String name) {
981 Cursor cursor = context.getContentResolver().query(
982 MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI, new String[]{
984 }, ArtistColumns.ARTIST + "=?", new String[]{
986 }, ArtistColumns.ARTIST);
988 if (cursor != null) {
989 cursor.moveToFirst();
990 if (!cursor.isAfterLast()) {
991 id = cursor.getInt(0);
1000 * Returns the ID for an album.
1002 * @param context The {@link Context} to use.
1003 * @param albumName The name of the album.
1004 * @param artistName The name of the artist
1005 * @return The ID for an album.
1007 public static final long getIdForAlbum(final Context context, final String albumName,
1008 final String artistName) {
1009 Cursor cursor = context.getContentResolver().query(
1010 MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, new String[] {
1012 }, AlbumColumns.ALBUM + "=? AND " + AlbumColumns.ARTIST + "=?", new String[] {
1013 albumName, artistName
1014 }, AlbumColumns.ALBUM);
1016 if (cursor != null) {
1017 cursor.moveToFirst();
1018 if (!cursor.isAfterLast()) {
1019 id = cursor.getInt(0);
1028 * Plays songs from an album.
1030 * @param context The {@link Context} to use.
1031 * @param albumId The album Id.
1032 * @param position Specify where to start.
1034 public static void playAlbum(final Context context, final long albumId, int position, boolean shuffle) {
1035 final long[] albumList = getSongListForAlbum(context, albumId);
1036 if (albumList != null) {
1037 playAll(context, albumList, position, albumId, IdType.Album, shuffle);
1042 public static void makeInsertItems(final long[] ids, final int offset, int len, final int base) {
1043 if (offset + len > ids.length) {
1044 len = ids.length - offset;
1047 if (mContentValuesCache == null || mContentValuesCache.length != len) {
1048 mContentValuesCache = new ContentValues[len];
1050 for (int i = 0; i < len; i++) {
1051 if (mContentValuesCache[i] == null) {
1052 mContentValuesCache[i] = new ContentValues();
1054 mContentValuesCache[i].put(Playlists.Members.PLAY_ORDER, base + offset + i);
1055 mContentValuesCache[i].put(Playlists.Members.AUDIO_ID, ids[offset + i]);
1060 * @param context The {@link Context} to use.
1061 * @param name The name of the new playlist.
1062 * @return A new playlist ID.
1064 public static final long createPlaylist(final Context context, final String name) {
1065 if (name != null && name.length() > 0) {
1066 final ContentResolver resolver = context.getContentResolver();
1067 final String[] projection = new String[] {
1068 PlaylistsColumns.NAME
1070 final String selection = PlaylistsColumns.NAME + " = '" + name + "'";
1071 Cursor cursor = resolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
1072 projection, selection, null, null);
1073 if (cursor.getCount() <= 0) {
1074 final ContentValues values = new ContentValues(1);
1075 values.put(PlaylistsColumns.NAME, name);
1076 final Uri uri = resolver.insert(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
1078 return Long.parseLong(uri.getLastPathSegment());
1080 if (cursor != null) {
1090 * @param context The {@link Context} to use.
1091 * @param playlistId The playlist ID.
1093 public static void clearPlaylist(final Context context, final int playlistId) {
1094 final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
1095 context.getContentResolver().delete(uri, null, null);
1099 /** remove all backing data for top tracks playlist */
1100 public static void clearTopTracks(Context context) {
1101 SongPlayCount.getInstance(context).deleteAll();
1104 /** remove all backing data for top tracks playlist */
1105 public static void clearRecent(Context context) {
1106 RecentStore.getInstance(context).deleteAll();
1109 /** move up cutoff for last added songs so playlist will be cleared */
1110 public static void clearLastAdded(Context context) {
1111 PreferenceUtils.getInstance(context)
1112 .setLastAddedCutoff(System.currentTimeMillis());
1116 * @param context The {@link Context} to use.
1117 * @param ids The id of the song(s) to add.
1118 * @param playlistid The id of the playlist being added to.
1120 public static void addToPlaylist(final Context context, final long[] ids, final long playlistid) {
1121 final int size = ids.length;
1122 final ContentResolver resolver = context.getContentResolver();
1123 final String[] projection = new String[] {
1124 "max(" + Playlists.Members.PLAY_ORDER + ")",
1126 final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistid);
1127 Cursor cursor = null;
1131 cursor = resolver.query(uri, projection, null, null, null);
1133 if (cursor != null && cursor.moveToFirst()) {
1134 base = cursor.getInt(0) + 1;
1137 if (cursor != null) {
1143 int numinserted = 0;
1144 for (int offSet = 0; offSet < size; offSet += 1000) {
1145 makeInsertItems(ids, offSet, 1000, base);
1146 numinserted += resolver.bulkInsert(uri, mContentValuesCache);
1148 final String message = context.getResources().getQuantityString(
1149 R.plurals.NNNtrackstoplaylist, numinserted, numinserted);
1150 Toast.makeText((Activity)context, message, Toast.LENGTH_SHORT).show();
1155 * Removes a single track from a given playlist
1156 * @param context The {@link Context} to use.
1157 * @param id The id of the song to remove.
1158 * @param playlistId The id of the playlist being removed from.
1160 public static void removeFromPlaylist(final Context context, final long id,
1161 final long playlistId) {
1162 final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
1163 final ContentResolver resolver = context.getContentResolver();
1164 resolver.delete(uri, Playlists.Members.AUDIO_ID + " = ? ", new String[] {
1167 final String message = context.getResources().getQuantityString(
1168 R.plurals.NNNtracksfromplaylist, 1, 1);
1169 Toast.makeText((Activity)context, message, Toast.LENGTH_SHORT).show();
1174 * @param context The {@link Context} to use.
1175 * @param list The list to enqueue.
1177 public static void addToQueue(final Context context, final long[] list, long sourceId,
1178 IdType sourceType) {
1179 if (mService == null) {
1183 mService.enqueue(list, MusicPlaybackService.LAST, sourceId, sourceType.mId);
1184 final String message = makeLabel(context, R.plurals.NNNtrackstoqueue, list.length);
1185 Toast.makeText((Activity) context, message, Toast.LENGTH_SHORT).show();
1186 } catch (final RemoteException ignored) {
1191 * @param context The {@link Context} to use
1192 * @param id The song ID.
1194 public static void setRingtone(final Context context, final long id) {
1195 final ContentResolver resolver = context.getContentResolver();
1196 final Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
1198 final ContentValues values = new ContentValues(2);
1199 values.put(AudioColumns.IS_RINGTONE, "1");
1200 values.put(AudioColumns.IS_ALARM, "1");
1201 resolver.update(uri, values, null, null);
1202 } catch (final UnsupportedOperationException ingored) {
1206 final String[] projection = new String[] {
1207 BaseColumns._ID, MediaColumns.DATA, MediaColumns.TITLE
1210 final String selection = BaseColumns._ID + "=" + id;
1211 Cursor cursor = resolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection,
1212 selection, null, null);
1214 if (cursor != null && cursor.getCount() == 1) {
1215 cursor.moveToFirst();
1216 Settings.System.putString(resolver, Settings.System.RINGTONE, uri.toString());
1217 final String message = context.getString(R.string.set_as_ringtone,
1218 cursor.getString(2));
1219 Toast.makeText((Activity)context, message, Toast.LENGTH_SHORT).show();
1222 if (cursor != null) {
1230 * @param context The {@link Context} to use.
1231 * @param id The id of the album.
1232 * @return The song count for an album.
1234 public static final int getSongCountForAlbumInt(final Context context, final long id) {
1236 if (id == -1) { return songCount; }
1238 Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, id);
1239 Cursor cursor = context.getContentResolver().query(uri,
1240 new String[] { AlbumColumns.NUMBER_OF_SONGS }, null, null, null);
1241 if (cursor != null) {
1242 cursor.moveToFirst();
1243 if (!cursor.isAfterLast()) {
1244 if(!cursor.isNull(0)) {
1245 songCount = cursor.getInt(0);
1256 * Gets the number of songs for a playlist
1257 * @param context The {@link Context} to use.
1258 * @param playlistId the id of the playlist
1259 * @return the # of songs in the playlist
1261 public static final int getSongCountForPlaylist(final Context context, final long playlistId) {
1262 Cursor c = context.getContentResolver().query(
1263 MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId),
1264 new String[]{BaseColumns._ID}, MusicUtils.MUSIC_ONLY_SELECTION, null, null);
1268 if (c.moveToFirst()) {
1269 count = c.getCount();
1279 public static final AlbumArtistDetails getAlbumArtDetails(final Context context, final long trackId) {
1280 String selection = (AudioColumns.IS_MUSIC + "=1") +
1281 " AND " + BaseColumns._ID + " = '" + trackId + "'";
1283 Cursor cursor = context.getContentResolver().query(
1284 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1287 MediaStore.Audio.AudioColumns.ALBUM_ID,
1289 MediaStore.Audio.AudioColumns.ALBUM,
1291 MediaStore.Audio.AlbumColumns.ARTIST,
1292 }, selection, null, null
1295 if (!cursor.moveToFirst()) {
1300 AlbumArtistDetails result = new AlbumArtistDetails();
1301 result.mAudioId = trackId;
1302 result.mAlbumId = cursor.getLong(0);
1303 result.mAlbumName = cursor.getString(1);
1304 result.mArtistName = cursor.getString(2);
1311 * @param context The {@link Context} to use.
1312 * @param id The id of the album.
1313 * @return The release date for an album.
1315 public static final String getReleaseDateForAlbum(final Context context, final long id) {
1319 Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, id);
1320 Cursor cursor = context.getContentResolver().query(uri, new String[] {
1321 AlbumColumns.FIRST_YEAR
1322 }, null, null, null);
1323 String releaseDate = null;
1324 if (cursor != null) {
1325 cursor.moveToFirst();
1326 if (!cursor.isAfterLast()) {
1327 releaseDate = cursor.getString(0);
1336 * @return The path to the currently playing file as {@link String}
1338 public static final String getFilePath() {
1340 if (mService != null) {
1341 return mService.getPath();
1343 } catch (final RemoteException ignored) {
1349 * @param from The index the item is currently at.
1350 * @param to The index the item is moving to.
1352 public static void moveQueueItem(final int from, final int to) {
1354 if (mService != null) {
1355 mService.moveQueueItem(from, to);
1358 } catch (final RemoteException ignored) {
1363 * @param context The {@link Context} to sue
1364 * @param playlistId The playlist Id
1365 * @return The track list for a playlist
1367 public static final long[] getSongListForPlaylist(final Context context, final long playlistId) {
1368 Cursor cursor = PlaylistSongLoader.makePlaylistSongCursor(context, playlistId);
1370 if (cursor != null) {
1371 final long[] list = getSongListForCursor(cursor);
1380 * Plays a user created playlist.
1382 * @param context The {@link Context} to use.
1383 * @param playlistId The playlist Id.
1385 public static void playPlaylist(final Context context, final long playlistId, boolean shuffle) {
1386 final long[] playlistList = getSongListForPlaylist(context, playlistId);
1387 if (playlistList != null) {
1388 playAll(context, playlistList, -1, playlistId, IdType.Playlist, shuffle);
1393 * @param context The {@link Context} to use
1394 * @param type The Smart Playlist Type
1395 * @return The song list for the last added playlist
1397 public static final long[] getSongListForSmartPlaylist(final Context context,
1398 final SmartPlaylistType type) {
1399 Cursor cursor = null;
1403 cursor = LastAddedLoader.makeLastAddedCursor(context);
1405 case RecentlyPlayed:
1406 cursor = TopTracksLoader.makeRecentTracksCursor(context);
1409 cursor = TopTracksLoader.makeTopTracksCursor(context);
1412 return MusicUtils.getSongListForCursor(cursor);
1414 if (cursor != null) {
1422 * Plays the smart playlist
1423 * @param context The {@link Context} to use
1424 * @param position the position to start playing from
1425 * @param type The Smart Playlist Type
1427 public static void playSmartPlaylist(final Context context, final int position,
1428 final SmartPlaylistType type, final boolean shuffle) {
1429 final long[] list = getSongListForSmartPlaylist(context, type);
1430 MusicUtils.playAll(context, list, position, type.mId, IdType.Playlist, shuffle);
1434 * Creates a sub menu used to add items to a new playlist or an existsing
1437 * @param context The {@link Context} to use.
1438 * @param groupId The group Id of the menu.
1439 * @param menu The {@link Menu} to add to.
1441 public static void makePlaylistMenu(final Context context, final int groupId,
1444 menu.add(groupId, FragmentMenuItems.NEW_PLAYLIST, Menu.NONE, R.string.new_playlist);
1445 Cursor cursor = PlaylistLoader.makePlaylistCursor(context);
1446 if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) {
1447 while (!cursor.isAfterLast()) {
1448 final Intent intent = new Intent();
1449 String name = cursor.getString(1);
1451 intent.putExtra("playlist", getIdForPlaylist(context, name));
1452 menu.add(groupId, FragmentMenuItems.PLAYLIST_SELECTED, Menu.NONE,
1453 name).setIntent(intent);
1455 cursor.moveToNext();
1458 if (cursor != null) {
1465 * Called when one of the lists should refresh or requery.
1467 public static void refresh() {
1469 if (mService != null) {
1472 } catch (final RemoteException ignored) {
1477 * Called when one of playlists have changed
1479 public static void playlistChanged() {
1481 if (mService != null) {
1482 mService.playlistChanged();
1484 } catch (final RemoteException ignored) {
1489 * Seeks the current track to a desired position
1491 * @param position The position to seek to
1493 public static void seek(final long position) {
1494 if (mService != null) {
1496 mService.seek(position);
1497 } catch (final RemoteException ignored) {
1503 * Seeks the current track to a desired relative position. This can be used
1504 * to simulate fastforward and rewind
1506 * @param deltaInMs The delta in ms to seek from the current position
1508 public static void seekRelative(final long deltaInMs) {
1509 if (mService != null) {
1511 mService.seekRelative(deltaInMs);
1512 } catch (final RemoteException ignored) {
1513 } catch (final IllegalStateException ignored) {
1514 // Illegal State Exception message is empty so logging will actually throw an
1515 // exception. We should come back and figure out why we get an exception in the
1516 // first place and make sure we understand it completely. I will use
1517 // https://cyanogen.atlassian.net/browse/MUSIC-125 to track investigating this more
1523 * @return The current position time of the track
1525 public static final long position() {
1526 if (mService != null) {
1528 return mService.position();
1529 } catch (final RemoteException ignored) {
1530 } catch (final IllegalStateException ex) {
1531 // Illegal State Exception message is empty so logging will actually throw an
1532 // exception. We should come back and figure out why we get an exception in the
1533 // first place and make sure we understand it completely. I will use
1534 // https://cyanogen.atlassian.net/browse/MUSIC-125 to track investigating this more
1541 * @return The total length of the current track
1543 public static final long duration() {
1544 if (mService != null) {
1546 return mService.duration();
1547 } catch (final RemoteException ignored) {
1548 } catch (final IllegalStateException ignored) {
1549 // Illegal State Exception message is empty so logging will actually throw an
1550 // exception. We should come back and figure out why we get an exception in the
1551 // first place and make sure we understand it completely. I will use
1552 // https://cyanogen.atlassian.net/browse/MUSIC-125 to track investigating this more
1559 * @param position The position to move the queue to
1561 public static void setQueuePosition(final int position) {
1562 if (mService != null) {
1564 mService.setQueuePosition(position);
1565 } catch (final RemoteException ignored) {
1573 public static void clearQueue() {
1575 mService.removeTracks(0, Integer.MAX_VALUE);
1576 } catch (final RemoteException ignored) {
1581 * Perminately deletes item(s) from the user's device
1583 * @param context The {@link Context} to use.
1584 * @param list The item(s) to delete.
1586 public static void deleteTracks(final Context context, final long[] list) {
1587 final String[] projection = new String[] {
1588 BaseColumns._ID, MediaColumns.DATA, AudioColumns.ALBUM_ID
1590 final StringBuilder selection = new StringBuilder();
1591 selection.append(BaseColumns._ID + " IN (");
1592 for (int i = 0; i < list.length; i++) {
1593 selection.append(list[i]);
1594 if (i < list.length - 1) {
1595 selection.append(",");
1598 selection.append(")");
1599 final Cursor c = context.getContentResolver().query(
1600 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection.toString(),
1603 // Step 1: Remove selected tracks from the current playlist, as well
1604 // as from the album art cache
1606 while (!c.isAfterLast()) {
1607 // Remove from current playlist
1608 final long id = c.getLong(0);
1610 // Remove the track from the play count
1611 SongPlayCount.getInstance(context).removeItem(id);
1612 // Remove any items in the recents database
1613 RecentStore.getInstance(context).removeItem(id);
1617 // Step 2: Remove selected tracks from the database
1618 context.getContentResolver().delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1619 selection.toString(), null);
1621 // Step 3: Remove files from card
1623 while (!c.isAfterLast()) {
1624 final String name = c.getString(1);
1625 final File f = new File(name);
1626 try { // File.delete can throw a security exception
1628 // I'm not sure if we'd ever get here (deletion would
1629 // have to fail, but no exception thrown)
1630 Log.e("MusicUtils", "Failed to delete file " + name);
1633 } catch (final SecurityException ex) {
1640 final String message = makeLabel(context, R.plurals.NNNtracksdeleted, list.length);
1642 Toast.makeText((Activity)context, message, Toast.LENGTH_SHORT).show();
1643 // We deleted a number of tracks, which could affect any number of
1645 // in the media content domain, so update everything.
1646 context.getContentResolver().notifyChange(Uri.parse("content://media"), null);
1647 // Notify the lists to update
1652 * Simple function used to determine if the song/album year is invalid
1653 * @param year value to test
1654 * @return true if the app considers it valid
1656 public static boolean isInvalidYear(int year) {
1657 return year < MIN_VALID_YEAR;
1661 * A snippet is taken from MediaStore.Audio.keyFor method
1662 * This will take a name, removes things like "the", "an", etc
1663 * as well as special characters and return it
1664 * @param name the string to trim
1665 * @return the trimmed name
1667 public static String getTrimmedName(String name) {
1668 if (name == null || name.length() == 0) {
1672 name = name.trim().toLowerCase();
1673 if (name.startsWith("the ")) {
1674 name = name.substring(4);
1676 if (name.startsWith("an ")) {
1677 name = name.substring(3);
1679 if (name.startsWith("a ")) {
1680 name = name.substring(2);
1682 if (name.endsWith(", the") || name.endsWith(",the") ||
1683 name.endsWith(", an") || name.endsWith(",an") ||
1684 name.endsWith(", a") || name.endsWith(",a")) {
1685 name = name.substring(0, name.lastIndexOf(','));
1687 name = name.replaceAll("[\\[\\]\\(\\)\"'.,?!]", "").trim();
1693 * A snippet is taken from MediaStore.Audio.keyFor method
1694 * This will take a name, removes things like "the", "an", etc
1695 * as well as special characters, then find the localized label
1696 * @param name Name to get the label of
1697 * @return the localized label of the bucket that the name falls into
1699 public static String getLocalizedBucketLetter(String name) {
1700 if (name == null || name.length() == 0) {
1704 name = getTrimmedName(name);
1706 if (name.length() > 0) {
1707 return LocaleUtils.getInstance().getLabel(name);
1713 /** @return true if a string is null, empty, or contains only whitespace */
1714 public static boolean isBlank(String s) {
1715 if(s == null) { return true; }
1716 if(s.isEmpty()) { return true; }
1717 for(int i = 0; i < s.length(); i++) {
1718 char c = s.charAt(i);
1719 if(!Character.isWhitespace(c)) { return false; }
1725 * Removes the header image from the cache.
1727 public static void removeFromCache(Activity activity, String key) {
1728 ImageFetcher imageFetcher = ApolloUtils.getImageFetcher(activity);
1729 imageFetcher.removeFromCache(key);
1730 // Give the disk cache a little time before requesting a new image.
1731 SystemClock.sleep(80);
1735 * Removes image from cache so that the stock image is retrieved on reload
1737 public static void selectOldPhoto(Activity activity, String key) {
1738 // First remove the old image
1739 removeFromCache(activity, key);
1740 MusicUtils.refresh();
1745 * @param sortOrder values are mostly derived from SortOrder.class or could also be any sql
1749 public static boolean isSortOrderDesending(String sortOrder) {
1750 return sortOrder.endsWith(" DESC");
1754 * Takes a collection of items and builds a comma-separated list of them
1755 * @param items collection of items
1756 * @return comma-separted list of items
1758 public static final <E> String buildCollectionAsString(Collection<E> items) {
1759 Iterator<E> iterator = items.iterator();
1760 StringBuilder str = new StringBuilder();
1761 if (iterator.hasNext()) {
1762 str.append(iterator.next());
1763 while (iterator.hasNext()) {
1765 str.append(iterator.next());
1769 return str.toString();