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;
43 import com.cyanogenmod.eleven.Config.IdType;
44 import com.cyanogenmod.eleven.Config.SmartPlaylistType;
45 import com.cyanogenmod.eleven.IElevenService;
46 import com.cyanogenmod.eleven.MusicPlaybackService;
47 import com.cyanogenmod.eleven.R;
48 import com.cyanogenmod.eleven.cache.ImageFetcher;
49 import com.cyanogenmod.eleven.loaders.LastAddedLoader;
50 import com.cyanogenmod.eleven.loaders.PlaylistLoader;
51 import com.cyanogenmod.eleven.loaders.PlaylistSongLoader;
52 import com.cyanogenmod.eleven.loaders.SongLoader;
53 import com.cyanogenmod.eleven.loaders.TopTracksLoader;
54 import com.cyanogenmod.eleven.locale.LocaleUtils;
55 import com.cyanogenmod.eleven.menu.FragmentMenuItems;
56 import com.cyanogenmod.eleven.model.Album;
57 import com.cyanogenmod.eleven.model.AlbumArtistDetails;
58 import com.cyanogenmod.eleven.model.Artist;
59 import com.cyanogenmod.eleven.model.Song;
60 import com.cyanogenmod.eleven.provider.RecentStore;
61 import com.cyanogenmod.eleven.provider.SongPlayCount;
62 import com.cyanogenmod.eleven.service.MusicPlaybackTrack;
65 import java.util.Arrays;
66 import java.util.Collection;
67 import java.util.Iterator;
68 import java.util.WeakHashMap;
71 * A collection of helpers directly related to music or Apollo's service.
73 * @author Andrew Neal (andrewdneal@gmail.com)
75 public final class MusicUtils {
77 public static IElevenService mService = null;
79 private static final WeakHashMap<Context, ServiceBinder> mConnectionMap;
81 private static final long[] sEmptyList;
83 private static ContentValues[] mContentValuesCache = null;
85 private static final int MIN_VALID_YEAR = 1900; // used to remove invalid years from metadata
87 public static final String MUSIC_ONLY_SELECTION = MediaStore.Audio.AudioColumns.IS_MUSIC + "=1"
88 + " AND " + MediaStore.Audio.AudioColumns.TITLE + " != ''"; //$NON-NLS-2$
91 mConnectionMap = new WeakHashMap<Context, ServiceBinder>();
92 sEmptyList = new long[0];
95 /* This class is never initiated */
100 * @param context The {@link Context} to use
101 * @param callback The {@link ServiceConnection} to use
102 * @return The new instance of {@link ServiceToken}
104 public static final ServiceToken bindToService(final Context context,
105 final ServiceConnection callback) {
106 Activity realActivity = ((Activity)context).getParent();
107 if (realActivity == null) {
108 realActivity = (Activity)context;
110 final ContextWrapper contextWrapper = new ContextWrapper(realActivity);
111 contextWrapper.startService(new Intent(contextWrapper, MusicPlaybackService.class));
112 final ServiceBinder binder = new ServiceBinder(callback,
113 contextWrapper.getApplicationContext());
114 if (contextWrapper.bindService(
115 new Intent().setClass(contextWrapper, MusicPlaybackService.class), binder, 0)) {
116 mConnectionMap.put(contextWrapper, binder);
117 return new ServiceToken(contextWrapper);
123 * @param token The {@link ServiceToken} to unbind from
125 public static void unbindFromService(final ServiceToken token) {
129 final ContextWrapper mContextWrapper = token.mWrappedContext;
130 final ServiceBinder mBinder = mConnectionMap.remove(mContextWrapper);
131 if (mBinder == null) {
134 mContextWrapper.unbindService(mBinder);
135 if (mConnectionMap.isEmpty()) {
140 public static final class ServiceBinder implements ServiceConnection {
141 private final ServiceConnection mCallback;
142 private final Context mContext;
145 * Constructor of <code>ServiceBinder</code>
147 * @param context The {@link ServiceConnection} to use
149 public ServiceBinder(final ServiceConnection callback, final Context context) {
150 mCallback = callback;
155 public void onServiceConnected(final ComponentName className, final IBinder service) {
156 mService = IElevenService.Stub.asInterface(service);
157 if (mCallback != null) {
158 mCallback.onServiceConnected(className, service);
160 MusicUtils.initPlaybackServiceWithSettings(mContext);
164 public void onServiceDisconnected(final ComponentName className) {
165 if (mCallback != null) {
166 mCallback.onServiceDisconnected(className);
172 public static final class ServiceToken {
173 public ContextWrapper mWrappedContext;
176 * Constructor of <code>ServiceToken</code>
178 * @param context The {@link ContextWrapper} to use
180 public ServiceToken(final ContextWrapper context) {
181 mWrappedContext = context;
185 public static final boolean isPlaybackServiceConnected() {
186 return mService != null;
190 * Used to make number of labels for the number of artists, albums, songs,
191 * genres, and playlists.
193 * @param context The {@link Context} to use.
194 * @param pluralInt The ID of the plural string to use.
195 * @param number The number of artists, albums, songs, genres, or playlists.
196 * @return A {@link String} used as a label for the number of artists,
197 * albums, songs, genres, and playlists.
199 public static final String makeLabel(final Context context, final int pluralInt,
201 return context.getResources().getQuantityString(pluralInt, number, number);
205 * * Used to create a formatted time string for the duration of tracks.
207 * @param context The {@link Context} to use.
208 * @param secs The track in seconds.
209 * @return Duration of a track that's properly formatted.
211 public static final String makeShortTimeString(final Context context, long secs) {
219 final String durationFormat = context.getResources().getString(
220 hours == 0 ? R.string.durationformatshort : R.string.durationformatlong);
221 return String.format(durationFormat, hours, mins, secs);
225 * Used to create a formatted time string in the format of #h #m or #m if there is only minutes
227 * @param context The {@link Context} to use.
228 * @param secs The duration seconds.
229 * @return Duration properly formatted in #h #m format
231 public static final String makeLongTimeString(final Context context, long secs) {
238 String hoursString = MusicUtils.makeLabel(context, R.plurals.Nhours, (int)hours);
239 String minutesString = MusicUtils.makeLabel(context, R.plurals.Nminutes, (int)mins);
242 return minutesString;
243 } else if (mins == 0) {
247 final String durationFormat = context.getResources().getString(R.string.duration_format);
248 return String.format(durationFormat, hoursString, minutesString);
252 * Used to combine two strings with some kind of separator in between
254 * @param context The {@link Context} to use.
255 * @param first string to combine
256 * @param second string to combine
257 * @return the combined string
259 public static final String makeCombinedString(final Context context, final String first,
260 final String second) {
261 final String formatter = context.getResources().getString(R.string.combine_two_strings);
262 return String.format(formatter, first, second);
266 * Changes to the next track
268 public static void next() {
270 if (mService != null) {
273 } catch (final RemoteException ignored) {
278 * Initialize playback service with values from Settings
280 public static void initPlaybackServiceWithSettings(final Context context) {
281 MusicUtils.setShakeToPlayEnabled(
282 PreferenceUtils.getInstance(context).getShakeToPlay());
283 MusicUtils.setShowAlbumArtOnLockscreen(
284 PreferenceUtils.getInstance(context).getShowAlbumArtOnLockscreen());
288 * Set shake to play status
290 public static void setShakeToPlayEnabled(final boolean enabled) {
292 if (mService != null) {
293 mService.setShakeToPlayEnabled(enabled);
295 } catch (final RemoteException ignored) {
300 * Set show album art on lockscreen
302 public static void setShowAlbumArtOnLockscreen(final boolean enabled) {
304 if (mService != null) {
305 mService.setLockscreenAlbumArt(enabled);
307 } catch (final RemoteException ignored) {
312 * Changes to the next track asynchronously
314 public static void asyncNext(final Context context) {
315 final Intent previous = new Intent(context, MusicPlaybackService.class);
316 previous.setAction(MusicPlaybackService.NEXT_ACTION);
317 context.startService(previous);
321 * Changes to the previous track.
323 * @NOTE The AIDL isn't used here in order to properly use the previous
324 * action. When the user is shuffling, because {@link
325 * MusicPlaybackService.#openCurrentAndNext()} is used, the user won't
326 * be able to travel to the previously skipped track. To remedy this,
327 * {@link MusicPlaybackService.#openCurrent()} is called in {@link
328 * MusicPlaybackService.#prev()}. {@code #startService(Intent intent)}
329 * is called here to specifically invoke the onStartCommand used by
330 * {@link MusicPlaybackService}, which states if the current position
331 * less than 2000 ms, start the track over, otherwise move to the
332 * previously listened track.
334 public static void previous(final Context context, final boolean force) {
335 final Intent previous = new Intent(context, MusicPlaybackService.class);
337 previous.setAction(MusicPlaybackService.PREVIOUS_FORCE_ACTION);
339 previous.setAction(MusicPlaybackService.PREVIOUS_ACTION);
341 context.startService(previous);
345 * Plays or pauses the music.
347 public static void playOrPause() {
349 if (mService != null) {
350 if (mService.isPlaying()) {
356 } catch (final Exception ignored) {
361 * Cycles through the repeat options.
363 public static void cycleRepeat() {
365 if (mService != null) {
366 switch (mService.getRepeatMode()) {
367 case MusicPlaybackService.REPEAT_NONE:
368 mService.setRepeatMode(MusicPlaybackService.REPEAT_ALL);
370 case MusicPlaybackService.REPEAT_ALL:
371 mService.setRepeatMode(MusicPlaybackService.REPEAT_CURRENT);
372 if (mService.getShuffleMode() != MusicPlaybackService.SHUFFLE_NONE) {
373 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
377 mService.setRepeatMode(MusicPlaybackService.REPEAT_NONE);
381 } catch (final RemoteException ignored) {
386 * Cycles through the shuffle options.
388 public static void cycleShuffle() {
390 if (mService != null) {
391 switch (mService.getShuffleMode()) {
392 case MusicPlaybackService.SHUFFLE_NONE:
393 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
394 if (mService.getRepeatMode() == MusicPlaybackService.REPEAT_CURRENT) {
395 mService.setRepeatMode(MusicPlaybackService.REPEAT_ALL);
398 case MusicPlaybackService.SHUFFLE_NORMAL:
399 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
401 case MusicPlaybackService.SHUFFLE_AUTO:
402 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
408 } catch (final RemoteException ignored) {
413 * @return True if we're playing music, false otherwise.
415 public static final boolean isPlaying() {
416 if (mService != null) {
418 return mService.isPlaying();
419 } catch (final RemoteException ignored) {
426 * @return The current shuffle mode.
428 public static final int getShuffleMode() {
429 if (mService != null) {
431 return mService.getShuffleMode();
432 } catch (final RemoteException ignored) {
439 * @return The current repeat mode.
441 public static final int getRepeatMode() {
442 if (mService != null) {
444 return mService.getRepeatMode();
445 } catch (final RemoteException ignored) {
452 * @return The current track name.
454 public static final String getTrackName() {
455 if (mService != null) {
457 return mService.getTrackName();
458 } catch (final RemoteException ignored) {
465 * @return The current artist name.
467 public static final String getArtistName() {
468 if (mService != null) {
470 return mService.getArtistName();
471 } catch (final RemoteException ignored) {
478 * @return The current album name.
480 public static final String getAlbumName() {
481 if (mService != null) {
483 return mService.getAlbumName();
484 } catch (final RemoteException ignored) {
491 * @return The current album Id.
493 public static final long getCurrentAlbumId() {
494 if (mService != null) {
496 return mService.getAlbumId();
497 } catch (final RemoteException ignored) {
504 * @return The current song Id.
506 public static final long getCurrentAudioId() {
507 if (mService != null) {
509 return mService.getAudioId();
510 } catch (final RemoteException ignored) {
517 * @return The current Music Playback Track
519 public static final MusicPlaybackTrack getCurrentTrack() {
520 if (mService != null) {
522 return mService.getCurrentTrack();
523 } catch (final RemoteException ignored) {
530 * @return The Music Playback Track at the specified index
532 public static final MusicPlaybackTrack getTrack(int index) {
533 if (mService != null) {
535 return mService.getTrack(index);
536 } catch (final RemoteException ignored) {
543 * @return The next song Id.
545 public static final long getNextAudioId() {
546 if (mService != null) {
548 return mService.getNextAudioId();
549 } catch (final RemoteException ignored) {
556 * @return The previous song Id.
558 public static final long getPreviousAudioId() {
559 if (mService != null) {
561 return mService.getPreviousAudioId();
562 } catch (final RemoteException ignored) {
569 * @return The current artist Id.
571 public static final long getCurrentArtistId() {
572 if (mService != null) {
574 return mService.getArtistId();
575 } catch (final RemoteException ignored) {
582 * @return The audio session Id.
584 public static final int getAudioSessionId() {
585 if (mService != null) {
587 return mService.getAudioSessionId();
588 } catch (final RemoteException ignored) {
597 public static final long[] getQueue() {
599 if (mService != null) {
600 return mService.getQueue();
603 } catch (final RemoteException ignored) {
610 * @return the id of the track in the queue at the given position
612 public static final long getQueueItemAtPosition(int position) {
614 if (mService != null) {
615 return mService.getQueueItemAtPosition(position);
618 } catch (final RemoteException ignored) {
624 * @return the current queue size
626 public static final int getQueueSize() {
628 if (mService != null) {
629 return mService.getQueueSize();
632 } catch (final RemoteException ignored) {
638 * @return The position of the current track in the queue.
640 public static final int getQueuePosition() {
642 if (mService != null) {
643 return mService.getQueuePosition();
645 } catch (final RemoteException ignored) {
651 * @return The queue history size
653 public static final int getQueueHistorySize() {
654 if (mService != null) {
656 return mService.getQueueHistorySize();
657 } catch (final RemoteException ignored) {
664 * @return The queue history position at the position
666 public static final int getQueueHistoryPosition(int position) {
667 if (mService != null) {
669 return mService.getQueueHistoryPosition(position);
670 } catch (final RemoteException ignored) {
677 * @return The queue history
679 public static final int[] getQueueHistoryList() {
680 if (mService != null) {
682 return mService.getQueueHistoryList();
683 } catch (final RemoteException ignored) {
690 * @param id The ID of the track to remove.
691 * @return removes track from a playlist or the queue.
693 public static final int removeTrack(final long id) {
695 if (mService != null) {
696 return mService.removeTrack(id);
698 } catch (final RemoteException ingored) {
704 * Remove song at a specified position in the list
706 * @param id The ID of the track to remove
707 * @param position The position of the song
709 * @return true if successful, false otherwise
711 public static final boolean removeTrackAtPosition(final long id, final int position) {
713 if (mService != null) {
714 return mService.removeTrackAtPosition(id, position);
716 } catch (final RemoteException ingored) {
722 * @param cursor The {@link Cursor} used to perform our query.
723 * @return The song list for a MIME type.
725 public static final long[] getSongListForCursor(Cursor cursor) {
726 if (cursor == null) {
729 final int len = cursor.getCount();
730 final long[] list = new long[len];
731 cursor.moveToFirst();
732 int columnIndex = -1;
734 columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.Members.AUDIO_ID);
735 } catch (final IllegalArgumentException notaplaylist) {
736 columnIndex = cursor.getColumnIndexOrThrow(BaseColumns._ID);
738 for (int i = 0; i < len; i++) {
739 list[i] = cursor.getLong(columnIndex);
748 * @param context The {@link Context} to use.
749 * @param id The ID of the artist.
750 * @return The song list for an artist.
752 public static final long[] getSongListForArtist(final Context context, final long id) {
753 final String[] projection = new String[] {
756 final String selection = AudioColumns.ARTIST_ID + "=" + id + " AND "
757 + AudioColumns.IS_MUSIC + "=1";
758 Cursor cursor = context.getContentResolver().query(
759 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null,
760 AudioColumns.ALBUM_KEY + "," + AudioColumns.TRACK);
761 if (cursor != null) {
762 final long[] mList = getSongListForCursor(cursor);
771 * @param context The {@link Context} to use.
772 * @param id The ID of the album.
773 * @return The song list for an album.
775 public static final long[] getSongListForAlbum(final Context context, final long id) {
776 final String[] projection = new String[] {
779 final String selection = AudioColumns.ALBUM_ID + "=" + id + " AND " + AudioColumns.IS_MUSIC
781 Cursor cursor = context.getContentResolver().query(
782 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null,
783 AudioColumns.TRACK + ", " + MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
784 if (cursor != null) {
785 final long[] mList = getSongListForCursor(cursor);
794 * Plays songs by an artist.
796 * @param context The {@link Context} to use.
797 * @param artistId The artist Id.
798 * @param position Specify where to start.
800 public static void playArtist(final Context context, final long artistId, int position, boolean shuffle) {
801 final long[] artistList = getSongListForArtist(context, artistId);
802 if (artistList != null) {
803 playAll(context, artistList, position, artistId, IdType.Artist, shuffle);
808 * @param context The {@link Context} to use.
809 * @param id The ID of the genre.
810 * @return The song list for an genre.
812 public static final long[] getSongListForGenre(final Context context, final long id) {
813 final String[] projection = new String[] {
816 final StringBuilder selection = new StringBuilder();
817 selection.append(AudioColumns.IS_MUSIC + "=1");
818 selection.append(" AND " + MediaColumns.TITLE + "!=''");
819 final Uri uri = MediaStore.Audio.Genres.Members.getContentUri("external", Long.valueOf(id));
820 Cursor cursor = context.getContentResolver().query(uri, projection, selection.toString(),
822 if (cursor != null) {
823 final long[] mList = getSongListForCursor(cursor);
832 * @param context The {@link Context} to use
833 * @param uri The source of the file
835 public static void playFile(final Context context, final Uri uri) {
836 if (uri == null || mService == null) {
840 // If this is a file:// URI, just use the path directly instead
841 // of going through the open-from-filedescriptor codepath.
843 String scheme = uri.getScheme();
844 if ("file".equals(scheme)) {
845 filename = uri.getPath();
847 filename = uri.toString();
852 mService.openFile(filename);
854 } catch (final RemoteException ignored) {
859 * @param context The {@link Context} to use.
860 * @param list The list of songs to play.
861 * @param position Specify where to start.
862 * @param forceShuffle True to force a shuffle, false otherwise.
864 public static void playAll(final Context context, final long[] list, int position,
865 final long sourceId, final IdType sourceType,
866 final boolean forceShuffle) {
867 if (list == null || list.length == 0 || mService == null) {
872 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
877 mService.open(list, forceShuffle ? -1 : position, sourceId, sourceType.mId);
879 } catch (final RemoteException ignored) {
884 * @param list The list to enqueue.
886 public static void playNext(final long[] list, final long sourceId, final IdType sourceType) {
887 if (mService == null) {
891 mService.enqueue(list, MusicPlaybackService.NEXT, sourceId, sourceType.mId);
892 } catch (final RemoteException ignored) {
897 * @param context The {@link Context} to use.
899 public static void shuffleAll(final Context context) {
900 Cursor cursor = SongLoader.makeSongCursor(context, null);
901 final long[] mTrackList = getSongListForCursor(cursor);
902 final int position = 0;
903 if (mTrackList.length == 0 || mService == null) {
907 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
908 final long mCurrentId = mService.getAudioId();
909 final int mCurrentQueuePosition = getQueuePosition();
910 if (position != -1 && mCurrentQueuePosition == position
911 && mCurrentId == mTrackList[position]) {
912 final long[] mPlaylist = getQueue();
913 if (Arrays.equals(mTrackList, mPlaylist)) {
918 mService.open(mTrackList, -1, -1, IdType.NA.mId);
922 } catch (final RemoteException ignored) {
927 * Returns The ID for a playlist.
929 * @param context The {@link Context} to use.
930 * @param name The name of the playlist.
931 * @return The ID for a playlist.
933 public static final long getIdForPlaylist(final Context context, final String name) {
934 Cursor cursor = context.getContentResolver().query(
935 MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, new String[]{
937 }, PlaylistsColumns.NAME + "=?", new String[]{
939 }, PlaylistsColumns.NAME);
941 if (cursor != null) {
942 cursor.moveToFirst();
943 if (!cursor.isAfterLast()) {
944 id = cursor.getInt(0);
952 /** @param context The {@link Context} to use.
953 * @param id The id of the playlist.
954 * @return The name for a playlist. */
955 public static final String getNameForPlaylist(final Context context, final long id) {
956 Cursor cursor = context.getContentResolver().query(
957 MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
958 new String[] { PlaylistsColumns.NAME },
959 BaseColumns._ID + "=?",
960 new String[] { Long.toString(id) },
962 if (cursor != null) {
964 if(cursor.moveToFirst()) { return cursor.getString(0); }
965 } finally { cursor.close(); }
972 * Returns the Id for an artist.
974 * @param context The {@link Context} to use.
975 * @param name The name of the artist.
976 * @return The ID for an artist.
978 public static final long getIdForArtist(final Context context, final String name) {
979 Cursor cursor = context.getContentResolver().query(
980 MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI, new String[]{
982 }, ArtistColumns.ARTIST + "=?", new String[]{
984 }, ArtistColumns.ARTIST);
986 if (cursor != null) {
987 cursor.moveToFirst();
988 if (!cursor.isAfterLast()) {
989 id = cursor.getInt(0);
998 * Returns the ID for an album.
1000 * @param context The {@link Context} to use.
1001 * @param albumName The name of the album.
1002 * @param artistName The name of the artist
1003 * @return The ID for an album.
1005 public static final long getIdForAlbum(final Context context, final String albumName,
1006 final String artistName) {
1007 Cursor cursor = context.getContentResolver().query(
1008 MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, new String[] {
1010 }, AlbumColumns.ALBUM + "=? AND " + AlbumColumns.ARTIST + "=?", new String[] {
1011 albumName, artistName
1012 }, AlbumColumns.ALBUM);
1014 if (cursor != null) {
1015 cursor.moveToFirst();
1016 if (!cursor.isAfterLast()) {
1017 id = cursor.getInt(0);
1026 * Plays songs from an album.
1028 * @param context The {@link Context} to use.
1029 * @param albumId The album Id.
1030 * @param position Specify where to start.
1032 public static void playAlbum(final Context context, final long albumId, int position, boolean shuffle) {
1033 final long[] albumList = getSongListForAlbum(context, albumId);
1034 if (albumList != null) {
1035 playAll(context, albumList, position, albumId, IdType.Album, shuffle);
1040 public static void makeInsertItems(final long[] ids, final int offset, int len, final int base) {
1041 if (offset + len > ids.length) {
1042 len = ids.length - offset;
1045 if (mContentValuesCache == null || mContentValuesCache.length != len) {
1046 mContentValuesCache = new ContentValues[len];
1048 for (int i = 0; i < len; i++) {
1049 if (mContentValuesCache[i] == null) {
1050 mContentValuesCache[i] = new ContentValues();
1052 mContentValuesCache[i].put(Playlists.Members.PLAY_ORDER, base + offset + i);
1053 mContentValuesCache[i].put(Playlists.Members.AUDIO_ID, ids[offset + i]);
1058 * @param context The {@link Context} to use.
1059 * @param name The name of the new playlist.
1060 * @return A new playlist ID.
1062 public static final long createPlaylist(final Context context, final String name) {
1063 if (name != null && name.length() > 0) {
1064 final ContentResolver resolver = context.getContentResolver();
1065 final String[] projection = new String[] {
1066 PlaylistsColumns.NAME
1068 final String selection = PlaylistsColumns.NAME + " = '" + name + "'";
1069 Cursor cursor = resolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
1070 projection, selection, null, null);
1071 if (cursor.getCount() <= 0) {
1072 final ContentValues values = new ContentValues(1);
1073 values.put(PlaylistsColumns.NAME, name);
1074 final Uri uri = resolver.insert(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
1076 return Long.parseLong(uri.getLastPathSegment());
1078 if (cursor != null) {
1088 * @param context The {@link Context} to use.
1089 * @param playlistId The playlist ID.
1091 public static void clearPlaylist(final Context context, final int playlistId) {
1092 final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
1093 context.getContentResolver().delete(uri, null, null);
1097 /** remove all backing data for top tracks playlist */
1098 public static void clearTopTracks(Context context) {
1099 SongPlayCount.getInstance(context).deleteAll();
1102 /** remove all backing data for top tracks playlist */
1103 public static void clearRecent(Context context) {
1104 RecentStore.getInstance(context).deleteAll();
1107 /** move up cutoff for last added songs so playlist will be cleared */
1108 public static void clearLastAdded(Context context) {
1109 PreferenceUtils.getInstance(context)
1110 .setLastAddedCutoff(System.currentTimeMillis());
1114 * @param context The {@link Context} to use.
1115 * @param ids The id of the song(s) to add.
1116 * @param playlistid The id of the playlist being added to.
1118 public static void addToPlaylist(final Context context, final long[] ids, final long playlistid) {
1119 final int size = ids.length;
1120 final ContentResolver resolver = context.getContentResolver();
1121 final String[] projection = new String[] {
1122 "max(" + Playlists.Members.PLAY_ORDER + ")",
1124 final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistid);
1125 Cursor cursor = null;
1129 cursor = resolver.query(uri, projection, null, null, null);
1131 if (cursor != null && cursor.moveToFirst()) {
1132 base = cursor.getInt(0) + 1;
1135 if (cursor != null) {
1141 int numinserted = 0;
1142 for (int offSet = 0; offSet < size; offSet += 1000) {
1143 makeInsertItems(ids, offSet, 1000, base);
1144 numinserted += resolver.bulkInsert(uri, mContentValuesCache);
1146 final String message = context.getResources().getQuantityString(
1147 R.plurals.NNNtrackstoplaylist, numinserted, numinserted);
1148 CustomToast.makeText((Activity)context, message, CustomToast.LENGTH_SHORT).show();
1153 * Removes a single track from a given playlist
1154 * @param context The {@link Context} to use.
1155 * @param id The id of the song to remove.
1156 * @param playlistId The id of the playlist being removed from.
1158 public static void removeFromPlaylist(final Context context, final long id,
1159 final long playlistId) {
1160 final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
1161 final ContentResolver resolver = context.getContentResolver();
1162 resolver.delete(uri, Playlists.Members.AUDIO_ID + " = ? ", new String[] {
1165 final String message = context.getResources().getQuantityString(
1166 R.plurals.NNNtracksfromplaylist, 1, 1);
1167 CustomToast.makeText((Activity)context, message, CustomToast.LENGTH_SHORT).show();
1172 * @param context The {@link Context} to use.
1173 * @param list The list to enqueue.
1175 public static void addToQueue(final Context context, final long[] list, long sourceId,
1176 IdType sourceType) {
1177 if (mService == null) {
1181 mService.enqueue(list, MusicPlaybackService.LAST, sourceId, sourceType.mId);
1182 final String message = makeLabel(context, R.plurals.NNNtrackstoqueue, list.length);
1183 CustomToast.makeText((Activity) context, message, CustomToast.LENGTH_SHORT).show();
1184 } catch (final RemoteException ignored) {
1189 * @param context The {@link Context} to use
1190 * @param id The song ID.
1192 public static void setRingtone(final Context context, final long id) {
1193 final ContentResolver resolver = context.getContentResolver();
1194 final Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
1196 final ContentValues values = new ContentValues(2);
1197 values.put(AudioColumns.IS_RINGTONE, "1");
1198 values.put(AudioColumns.IS_ALARM, "1");
1199 resolver.update(uri, values, null, null);
1200 } catch (final UnsupportedOperationException ingored) {
1204 final String[] projection = new String[] {
1205 BaseColumns._ID, MediaColumns.DATA, MediaColumns.TITLE
1208 final String selection = BaseColumns._ID + "=" + id;
1209 Cursor cursor = resolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection,
1210 selection, null, null);
1212 if (cursor != null && cursor.getCount() == 1) {
1213 cursor.moveToFirst();
1214 Settings.System.putString(resolver, Settings.System.RINGTONE, uri.toString());
1215 final String message = context.getString(R.string.set_as_ringtone,
1216 cursor.getString(2));
1217 CustomToast.makeText((Activity)context, message, CustomToast.LENGTH_SHORT).show();
1220 if (cursor != null) {
1228 * @param context The {@link Context} to use.
1229 * @param id The id of the album.
1230 * @return The song count for an album.
1232 public static final int getSongCountForAlbumInt(final Context context, final long id) {
1234 if (id == -1) { return songCount; }
1236 Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, id);
1237 Cursor cursor = context.getContentResolver().query(uri,
1238 new String[] { AlbumColumns.NUMBER_OF_SONGS }, null, null, null);
1239 if (cursor != null) {
1240 cursor.moveToFirst();
1241 if (!cursor.isAfterLast()) {
1242 if(!cursor.isNull(0)) {
1243 songCount = cursor.getInt(0);
1254 * Gets the number of songs for a playlist
1255 * @param context The {@link Context} to use.
1256 * @param playlistId the id of the playlist
1257 * @return the # of songs in the playlist
1259 public static final int getSongCountForPlaylist(final Context context, final long playlistId) {
1260 Cursor c = context.getContentResolver().query(
1261 MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId),
1262 new String[]{BaseColumns._ID}, MusicUtils.MUSIC_ONLY_SELECTION, null, null);
1266 if (c.moveToFirst()) {
1267 count = c.getCount();
1277 public static final AlbumArtistDetails getAlbumArtDetails(final Context context, final long trackId) {
1278 final StringBuilder selection = new StringBuilder();
1279 selection.append(MediaStore.Audio.AudioColumns.IS_MUSIC + "=1");
1280 selection.append(" AND " + BaseColumns._ID + " = '" + trackId + "'");
1282 Cursor cursor = context.getContentResolver().query(
1283 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1286 MediaStore.Audio.AudioColumns.ALBUM_ID,
1288 MediaStore.Audio.AudioColumns.ALBUM,
1290 MediaStore.Audio.AlbumColumns.ARTIST,
1291 }, selection.toString(), null, null
1294 if (!cursor.moveToFirst()) {
1299 AlbumArtistDetails result = new AlbumArtistDetails();
1300 result.mAudioId = trackId;
1301 result.mAlbumId = cursor.getLong(0);
1302 result.mAlbumName = cursor.getString(1);
1303 result.mArtistName = cursor.getString(2);
1310 * @param context The {@link Context} to use.
1311 * @param id The id of the album.
1312 * @return The release date for an album.
1314 public static final String getReleaseDateForAlbum(final Context context, final long id) {
1318 Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, id);
1319 Cursor cursor = context.getContentResolver().query(uri, new String[] {
1320 AlbumColumns.FIRST_YEAR
1321 }, null, null, null);
1322 String releaseDate = null;
1323 if (cursor != null) {
1324 cursor.moveToFirst();
1325 if (!cursor.isAfterLast()) {
1326 releaseDate = cursor.getString(0);
1335 * @return The path to the currently playing file as {@link String}
1337 public static final String getFilePath() {
1339 if (mService != null) {
1340 return mService.getPath();
1342 } catch (final RemoteException ignored) {
1348 * @param from The index the item is currently at.
1349 * @param to The index the item is moving to.
1351 public static void moveQueueItem(final int from, final int to) {
1353 if (mService != null) {
1354 mService.moveQueueItem(from, to);
1357 } catch (final RemoteException ignored) {
1362 * @param context The {@link Context} to sue
1363 * @param playlistId The playlist Id
1364 * @return The track list for a playlist
1366 public static final long[] getSongListForPlaylist(final Context context, final long playlistId) {
1367 Cursor cursor = PlaylistSongLoader.makePlaylistSongCursor(context, playlistId);
1369 if (cursor != null) {
1370 final long[] list = getSongListForCursor(cursor);
1379 * Plays a user created playlist.
1381 * @param context The {@link Context} to use.
1382 * @param playlistId The playlist Id.
1384 public static void playPlaylist(final Context context, final long playlistId, boolean shuffle) {
1385 final long[] playlistList = getSongListForPlaylist(context, playlistId);
1386 if (playlistList != null) {
1387 playAll(context, playlistList, -1, playlistId, IdType.Playlist, shuffle);
1392 * @param context The {@link Context} to use
1393 * @param type The Smart Playlist Type
1394 * @return The song list for the last added playlist
1396 public static final long[] getSongListForSmartPlaylist(final Context context,
1397 final SmartPlaylistType type) {
1398 Cursor cursor = null;
1402 cursor = LastAddedLoader.makeLastAddedCursor(context);
1404 case RecentlyPlayed:
1405 cursor = TopTracksLoader.makeRecentTracksCursor(context);
1408 cursor = TopTracksLoader.makeTopTracksCursor(context);
1411 return MusicUtils.getSongListForCursor(cursor);
1413 if (cursor != null) {
1421 * Plays the smart playlist
1422 * @param context The {@link Context} to use
1423 * @param position the position to start playing from
1424 * @param type The Smart Playlist Type
1426 public static void playSmartPlaylist(final Context context, final int position,
1427 final SmartPlaylistType type, final boolean shuffle) {
1428 final long[] list = getSongListForSmartPlaylist(context, type);
1429 MusicUtils.playAll(context, list, position, type.mId, IdType.Playlist, shuffle);
1433 * Creates a sub menu used to add items to a new playlist or an existsing
1436 * @param context The {@link Context} to use.
1437 * @param groupId The group Id of the menu.
1438 * @param menu The {@link Menu} to add to.
1440 public static void makePlaylistMenu(final Context context, final int groupId,
1443 menu.add(groupId, FragmentMenuItems.NEW_PLAYLIST, Menu.NONE, R.string.new_playlist);
1444 Cursor cursor = PlaylistLoader.makePlaylistCursor(context);
1445 if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) {
1446 while (!cursor.isAfterLast()) {
1447 final Intent intent = new Intent();
1448 String name = cursor.getString(1);
1450 intent.putExtra("playlist", getIdForPlaylist(context, name));
1451 menu.add(groupId, FragmentMenuItems.PLAYLIST_SELECTED, Menu.NONE,
1452 name).setIntent(intent);
1454 cursor.moveToNext();
1457 if (cursor != null) {
1464 * Called when one of the lists should refresh or requery.
1466 public static void refresh() {
1468 if (mService != null) {
1471 } catch (final RemoteException ignored) {
1476 * Called when one of playlists have changed
1478 public static void playlistChanged() {
1480 if (mService != null) {
1481 mService.playlistChanged();
1483 } catch (final RemoteException ignored) {
1488 * Seeks the current track to a desired position
1490 * @param position The position to seek to
1492 public static void seek(final long position) {
1493 if (mService != null) {
1495 mService.seek(position);
1496 } catch (final RemoteException ignored) {
1502 * Seeks the current track to a desired relative position. This can be used
1503 * to simulate fastforward and rewind
1505 * @param deltaInMs The delta in ms to seek from the current position
1507 public static void seekRelative(final long deltaInMs) {
1508 if (mService != null) {
1510 mService.seekRelative(deltaInMs);
1511 } catch (final RemoteException ignored) {
1512 } catch (final IllegalStateException ignored) {
1513 // Illegal State Exception message is empty so logging will actually throw an
1514 // exception. We should come back and figure out why we get an exception in the
1515 // first place and make sure we understand it completely. I will use
1516 // https://cyanogen.atlassian.net/browse/MUSIC-125 to track investigating this more
1522 * @return The current position time of the track
1524 public static final long position() {
1525 if (mService != null) {
1527 return mService.position();
1528 } catch (final RemoteException ignored) {
1529 } catch (final IllegalStateException ex) {
1530 // Illegal State Exception message is empty so logging will actually throw an
1531 // exception. We should come back and figure out why we get an exception in the
1532 // first place and make sure we understand it completely. I will use
1533 // https://cyanogen.atlassian.net/browse/MUSIC-125 to track investigating this more
1540 * @return The total length of the current track
1542 public static final long duration() {
1543 if (mService != null) {
1545 return mService.duration();
1546 } catch (final RemoteException ignored) {
1547 } catch (final IllegalStateException ignored) {
1548 // Illegal State Exception message is empty so logging will actually throw an
1549 // exception. We should come back and figure out why we get an exception in the
1550 // first place and make sure we understand it completely. I will use
1551 // https://cyanogen.atlassian.net/browse/MUSIC-125 to track investigating this more
1558 * @param position The position to move the queue to
1560 public static void setQueuePosition(final int position) {
1561 if (mService != null) {
1563 mService.setQueuePosition(position);
1564 } catch (final RemoteException ignored) {
1572 public static void clearQueue() {
1574 mService.removeTracks(0, Integer.MAX_VALUE);
1575 } catch (final RemoteException ignored) {
1580 * Perminately deletes item(s) from the user's device
1582 * @param context The {@link Context} to use.
1583 * @param list The item(s) to delete.
1585 public static void deleteTracks(final Context context, final long[] list) {
1586 final String[] projection = new String[] {
1587 BaseColumns._ID, MediaColumns.DATA, AudioColumns.ALBUM_ID
1589 final StringBuilder selection = new StringBuilder();
1590 selection.append(BaseColumns._ID + " IN (");
1591 for (int i = 0; i < list.length; i++) {
1592 selection.append(list[i]);
1593 if (i < list.length - 1) {
1594 selection.append(",");
1597 selection.append(")");
1598 final Cursor c = context.getContentResolver().query(
1599 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection.toString(),
1602 // Step 1: Remove selected tracks from the current playlist, as well
1603 // as from the album art cache
1605 while (!c.isAfterLast()) {
1606 // Remove from current playlist
1607 final long id = c.getLong(0);
1609 // Remove the track from the play count
1610 SongPlayCount.getInstance(context).removeItem(id);
1611 // Remove any items in the recents database
1612 RecentStore.getInstance(context).removeItem(id);
1616 // Step 2: Remove selected tracks from the database
1617 context.getContentResolver().delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1618 selection.toString(), null);
1620 // Step 3: Remove files from card
1622 while (!c.isAfterLast()) {
1623 final String name = c.getString(1);
1624 final File f = new File(name);
1625 try { // File.delete can throw a security exception
1627 // I'm not sure if we'd ever get here (deletion would
1628 // have to fail, but no exception thrown)
1629 Log.e("MusicUtils", "Failed to delete file " + name);
1632 } catch (final SecurityException ex) {
1639 final String message = makeLabel(context, R.plurals.NNNtracksdeleted, list.length);
1641 CustomToast.makeText((Activity)context, message, CustomToast.LENGTH_SHORT).show();
1642 // We deleted a number of tracks, which could affect any number of
1644 // in the media content domain, so update everything.
1645 context.getContentResolver().notifyChange(Uri.parse("content://media"), null);
1646 // Notify the lists to update
1651 * Simple function used to determine if the song/album year is invalid
1652 * @param year value to test
1653 * @return true if the app considers it valid
1655 public static boolean isInvalidYear(int year) {
1656 return year < MIN_VALID_YEAR;
1660 * A snippet is taken from MediaStore.Audio.keyFor method
1661 * This will take a name, removes things like "the", "an", etc
1662 * as well as special characters and return it
1663 * @param name the string to trim
1664 * @return the trimmed name
1666 public static String getTrimmedName(String name) {
1667 if (name == null || name.length() == 0) {
1671 name = name.trim().toLowerCase();
1672 if (name.startsWith("the ")) {
1673 name = name.substring(4);
1675 if (name.startsWith("an ")) {
1676 name = name.substring(3);
1678 if (name.startsWith("a ")) {
1679 name = name.substring(2);
1681 if (name.endsWith(", the") || name.endsWith(",the") ||
1682 name.endsWith(", an") || name.endsWith(",an") ||
1683 name.endsWith(", a") || name.endsWith(",a")) {
1684 name = name.substring(0, name.lastIndexOf(','));
1686 name = name.replaceAll("[\\[\\]\\(\\)\"'.,?!]", "").trim();
1692 * A snippet is taken from MediaStore.Audio.keyFor method
1693 * This will take a name, removes things like "the", "an", etc
1694 * as well as special characters, then find the localized label
1695 * @param name Name to get the label of
1696 * @return the localized label of the bucket that the name falls into
1698 public static String getLocalizedBucketLetter(String name) {
1699 if (name == null || name.length() == 0) {
1703 name = getTrimmedName(name);
1705 if (name.length() > 0) {
1706 return LocaleUtils.getInstance().getLabel(name);
1712 /** @return true if a string is null, empty, or contains only whitespace */
1713 public static boolean isBlank(String s) {
1714 if(s == null) { return true; }
1715 if(s.isEmpty()) { return true; }
1716 for(int i = 0; i < s.length(); i++) {
1717 char c = s.charAt(i);
1718 if(!Character.isWhitespace(c)) { return false; }
1724 * Removes the header image from the cache.
1726 public static void removeFromCache(Activity activity, String key) {
1727 ImageFetcher imageFetcher = ApolloUtils.getImageFetcher(activity);
1728 imageFetcher.removeFromCache(key);
1729 // Give the disk cache a little time before requesting a new image.
1730 SystemClock.sleep(80);
1734 * Removes image from cache so that the stock image is retrieved on reload
1736 public static void selectOldPhoto(Activity activity, String key) {
1737 // First remove the old image
1738 removeFromCache(activity, key);
1739 MusicUtils.refresh();
1744 * @param sortOrder values are mostly derived from SortOrder.class or could also be any sql
1748 public static boolean isSortOrderDesending(String sortOrder) {
1749 return sortOrder.endsWith(" DESC");
1753 * Takes a collection of items and builds a comma-separated list of them
1754 * @param items collection of items
1755 * @return comma-separted list of items
1757 public static final <E> String buildCollectionAsString(Collection<E> items) {
1758 Iterator<E> iterator = items.iterator();
1759 StringBuilder str = new StringBuilder();
1760 if (iterator.hasNext()) {
1761 str.append(iterator.next());
1762 while (iterator.hasNext()) {
1764 str.append(iterator.next());
1768 return str.toString();