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 if (contextWrapper.bindService(
118 new Intent().setClass(contextWrapper, MusicPlaybackService.class), binder, 0)) {
119 mConnectionMap.put(contextWrapper, binder);
120 return new ServiceToken(contextWrapper);
126 * @param token The {@link ServiceToken} to unbind from
128 public static void unbindFromService(final ServiceToken token) {
132 final ContextWrapper mContextWrapper = token.mWrappedContext;
133 final ServiceBinder mBinder = mConnectionMap.remove(mContextWrapper);
134 if (mBinder == null) {
137 mContextWrapper.unbindService(mBinder);
138 if (mConnectionMap.isEmpty()) {
143 public static final class ServiceBinder implements ServiceConnection {
144 private final ServiceConnection mCallback;
147 * Constructor of <code>ServiceBinder</code>
149 * @param context The {@link ServiceConnection} to use
151 public ServiceBinder(final ServiceConnection callback) {
152 mCallback = callback;
156 public void onServiceConnected(final ComponentName className, final IBinder service) {
157 mService = IElevenService.Stub.asInterface(service);
158 if (mCallback != null) {
159 mCallback.onServiceConnected(className, service);
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 * Set shake to play status
280 public static void setShakeToPlayEnabled(final boolean enabled) {
282 if (mService != null) {
283 mService.setShakeToPlayEnabled(enabled);
285 } catch (final RemoteException ignored) {
290 * Set show album art on lockscreen
292 public static void setShowAlbumArtOnLockscreen(final boolean enabled) {
294 if (mService != null) {
295 mService.setLockscreenAlbumArt(enabled);
297 } catch (final RemoteException ignored) {
302 * Changes to the next track asynchronously
304 public static void asyncNext(final Context context) {
305 final Intent previous = new Intent(context, MusicPlaybackService.class);
306 previous.setAction(MusicPlaybackService.NEXT_ACTION);
307 context.startService(previous);
311 * Changes to the previous track.
313 * @NOTE The AIDL isn't used here in order to properly use the previous
314 * action. When the user is shuffling, because {@link
315 * MusicPlaybackService.#openCurrentAndNext()} is used, the user won't
316 * be able to travel to the previously skipped track. To remedy this,
317 * {@link MusicPlaybackService.#openCurrent()} is called in {@link
318 * MusicPlaybackService.#prev()}. {@code #startService(Intent intent)}
319 * is called here to specifically invoke the onStartCommand used by
320 * {@link MusicPlaybackService}, which states if the current position
321 * less than 2000 ms, start the track over, otherwise move to the
322 * previously listened track.
324 public static void previous(final Context context, final boolean force) {
325 final Intent previous = new Intent(context, MusicPlaybackService.class);
327 previous.setAction(MusicPlaybackService.PREVIOUS_FORCE_ACTION);
329 previous.setAction(MusicPlaybackService.PREVIOUS_ACTION);
331 context.startService(previous);
335 * Plays or pauses the music.
337 public static void playOrPause() {
339 if (mService != null) {
340 if (mService.isPlaying()) {
346 } catch (final Exception ignored) {
351 * Cycles through the repeat options.
353 public static void cycleRepeat() {
355 if (mService != null) {
356 switch (mService.getRepeatMode()) {
357 case MusicPlaybackService.REPEAT_NONE:
358 mService.setRepeatMode(MusicPlaybackService.REPEAT_ALL);
360 case MusicPlaybackService.REPEAT_ALL:
361 mService.setRepeatMode(MusicPlaybackService.REPEAT_CURRENT);
362 if (mService.getShuffleMode() != MusicPlaybackService.SHUFFLE_NONE) {
363 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
367 mService.setRepeatMode(MusicPlaybackService.REPEAT_NONE);
371 } catch (final RemoteException ignored) {
376 * Cycles through the shuffle options.
378 public static void cycleShuffle() {
380 if (mService != null) {
381 switch (mService.getShuffleMode()) {
382 case MusicPlaybackService.SHUFFLE_NONE:
383 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
384 if (mService.getRepeatMode() == MusicPlaybackService.REPEAT_CURRENT) {
385 mService.setRepeatMode(MusicPlaybackService.REPEAT_ALL);
388 case MusicPlaybackService.SHUFFLE_NORMAL:
389 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
391 case MusicPlaybackService.SHUFFLE_AUTO:
392 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
398 } catch (final RemoteException ignored) {
403 * @return True if we're playing music, false otherwise.
405 public static final boolean isPlaying() {
406 if (mService != null) {
408 return mService.isPlaying();
409 } catch (final RemoteException ignored) {
416 * @return The current shuffle mode.
418 public static final int getShuffleMode() {
419 if (mService != null) {
421 return mService.getShuffleMode();
422 } catch (final RemoteException ignored) {
429 * @return The current repeat mode.
431 public static final int getRepeatMode() {
432 if (mService != null) {
434 return mService.getRepeatMode();
435 } catch (final RemoteException ignored) {
442 * @return The current track name.
444 public static final String getTrackName() {
445 if (mService != null) {
447 return mService.getTrackName();
448 } catch (final RemoteException ignored) {
455 * @return The current artist name.
457 public static final String getArtistName() {
458 if (mService != null) {
460 return mService.getArtistName();
461 } catch (final RemoteException ignored) {
468 * @return The current album name.
470 public static final String getAlbumName() {
471 if (mService != null) {
473 return mService.getAlbumName();
474 } catch (final RemoteException ignored) {
481 * @return The current album Id.
483 public static final long getCurrentAlbumId() {
484 if (mService != null) {
486 return mService.getAlbumId();
487 } catch (final RemoteException ignored) {
494 * @return The current song Id.
496 public static final long getCurrentAudioId() {
497 if (mService != null) {
499 return mService.getAudioId();
500 } catch (final RemoteException ignored) {
507 * @return The current Music Playback Track
509 public static final MusicPlaybackTrack getCurrentTrack() {
510 if (mService != null) {
512 return mService.getCurrentTrack();
513 } catch (final RemoteException ignored) {
520 * @return The Music Playback Track at the specified index
522 public static final MusicPlaybackTrack getTrack(int index) {
523 if (mService != null) {
525 return mService.getTrack(index);
526 } catch (final RemoteException ignored) {
533 * @return The next song Id.
535 public static final long getNextAudioId() {
536 if (mService != null) {
538 return mService.getNextAudioId();
539 } catch (final RemoteException ignored) {
546 * @return The previous song Id.
548 public static final long getPreviousAudioId() {
549 if (mService != null) {
551 return mService.getPreviousAudioId();
552 } catch (final RemoteException ignored) {
559 * @return The current artist Id.
561 public static final long getCurrentArtistId() {
562 if (mService != null) {
564 return mService.getArtistId();
565 } catch (final RemoteException ignored) {
572 * @return The audio session Id.
574 public static final int getAudioSessionId() {
575 if (mService != null) {
577 return mService.getAudioSessionId();
578 } catch (final RemoteException ignored) {
587 public static final long[] getQueue() {
589 if (mService != null) {
590 return mService.getQueue();
593 } catch (final RemoteException ignored) {
600 * @return the id of the track in the queue at the given position
602 public static final long getQueueItemAtPosition(int position) {
604 if (mService != null) {
605 return mService.getQueueItemAtPosition(position);
608 } catch (final RemoteException ignored) {
614 * @return the current queue size
616 public static final int getQueueSize() {
618 if (mService != null) {
619 return mService.getQueueSize();
622 } catch (final RemoteException ignored) {
628 * @return The position of the current track in the queue.
630 public static final int getQueuePosition() {
632 if (mService != null) {
633 return mService.getQueuePosition();
635 } catch (final RemoteException ignored) {
641 * @return The queue history size
643 public static final int getQueueHistorySize() {
644 if (mService != null) {
646 return mService.getQueueHistorySize();
647 } catch (final RemoteException ignored) {
654 * @return The queue history position at the position
656 public static final int getQueueHistoryPosition(int position) {
657 if (mService != null) {
659 return mService.getQueueHistoryPosition(position);
660 } catch (final RemoteException ignored) {
667 * @return The queue history
669 public static final int[] getQueueHistoryList() {
670 if (mService != null) {
672 return mService.getQueueHistoryList();
673 } catch (final RemoteException ignored) {
680 * @param id The ID of the track to remove.
681 * @return removes track from a playlist or the queue.
683 public static final int removeTrack(final long id) {
685 if (mService != null) {
686 return mService.removeTrack(id);
688 } catch (final RemoteException ingored) {
694 * Remove song at a specified position in the list
696 * @param id The ID of the track to remove
697 * @param position The position of the song
699 * @return true if successful, false otherwise
701 public static final boolean removeTrackAtPosition(final long id, final int position) {
703 if (mService != null) {
704 return mService.removeTrackAtPosition(id, position);
706 } catch (final RemoteException ingored) {
712 * @param cursor The {@link Cursor} used to perform our query.
713 * @return The song list for a MIME type.
715 public static final long[] getSongListForCursor(Cursor cursor) {
716 if (cursor == null) {
719 final int len = cursor.getCount();
720 final long[] list = new long[len];
721 cursor.moveToFirst();
722 int columnIndex = -1;
724 columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.Members.AUDIO_ID);
725 } catch (final IllegalArgumentException notaplaylist) {
726 columnIndex = cursor.getColumnIndexOrThrow(BaseColumns._ID);
728 for (int i = 0; i < len; i++) {
729 list[i] = cursor.getLong(columnIndex);
738 * @param context The {@link Context} to use.
739 * @param id The ID of the artist.
740 * @return The song list for an artist.
742 public static final long[] getSongListForArtist(final Context context, final long id) {
743 final String[] projection = new String[] {
746 final String selection = AudioColumns.ARTIST_ID + "=" + id + " AND "
747 + AudioColumns.IS_MUSIC + "=1";
748 Cursor cursor = context.getContentResolver().query(
749 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null,
750 AudioColumns.ALBUM_KEY + "," + AudioColumns.TRACK);
751 if (cursor != null) {
752 final long[] mList = getSongListForCursor(cursor);
761 * @param context The {@link Context} to use.
762 * @param id The ID of the album.
763 * @return The song list for an album.
765 public static final long[] getSongListForAlbum(final Context context, final long id) {
766 final String[] projection = new String[] {
769 final String selection = AudioColumns.ALBUM_ID + "=" + id + " AND " + AudioColumns.IS_MUSIC
771 Cursor cursor = context.getContentResolver().query(
772 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null,
773 AudioColumns.TRACK + ", " + MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
774 if (cursor != null) {
775 final long[] mList = getSongListForCursor(cursor);
784 * Plays songs by an artist.
786 * @param context The {@link Context} to use.
787 * @param artistId The artist Id.
788 * @param position Specify where to start.
790 public static void playArtist(final Context context, final long artistId, int position, boolean shuffle) {
791 final long[] artistList = getSongListForArtist(context, artistId);
792 if (artistList != null) {
793 playAll(context, artistList, position, artistId, IdType.Artist, shuffle);
798 * @param context The {@link Context} to use.
799 * @param id The ID of the genre.
800 * @return The song list for an genre.
802 public static final long[] getSongListForGenre(final Context context, final long id) {
803 final String[] projection = new String[] {
806 String selection = (AudioColumns.IS_MUSIC + "=1") +
807 " AND " + MediaColumns.TITLE + "!=''";
808 final Uri uri = MediaStore.Audio.Genres.Members.getContentUri("external", Long.valueOf(id));
809 Cursor cursor = context.getContentResolver().query(uri, projection, selection,
811 if (cursor != null) {
812 final long[] mList = getSongListForCursor(cursor);
821 * @param context The {@link Context} to use
822 * @param uri The source of the file
824 public static void playFile(final Context context, final Uri uri) {
825 if (uri == null || mService == null) {
829 // If this is a file:// URI, just use the path directly instead
830 // of going through the open-from-filedescriptor codepath.
832 String scheme = uri.getScheme();
833 if ("file".equals(scheme)) {
834 filename = uri.getPath();
836 filename = uri.toString();
841 mService.openFile(filename);
843 } catch (final RemoteException ignored) {
848 * @param context The {@link Context} to use.
849 * @param list The list of songs to play.
850 * @param position Specify where to start.
851 * @param forceShuffle True to force a shuffle, false otherwise.
853 public static void playAll(final Context context, final long[] list, int position,
854 final long sourceId, final IdType sourceType,
855 final boolean forceShuffle) {
856 if (list == null || list.length == 0 || mService == null) {
861 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
866 mService.open(list, forceShuffle ? -1 : position, sourceId, sourceType.mId);
868 } catch (final RemoteException ignored) {
873 * @param list The list to enqueue.
875 public static void playNext(final long[] list, final long sourceId, final IdType sourceType) {
876 if (mService == null) {
880 mService.enqueue(list, MusicPlaybackService.NEXT, sourceId, sourceType.mId);
881 } catch (final RemoteException ignored) {
886 * @param context The {@link Context} to use.
888 public static void shuffleAll(final Context context) {
889 Cursor cursor = SongLoader.makeSongCursor(context, null);
890 final long[] mTrackList = getSongListForCursor(cursor);
891 if (mTrackList.length == 0 || mService == null) {
895 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
896 final long mCurrentId = mService.getAudioId();
897 final int mCurrentQueuePosition = getQueuePosition();
898 if (mCurrentQueuePosition == 0
899 && mCurrentId == mTrackList[0]) {
900 final long[] mPlaylist = getQueue();
901 if (Arrays.equals(mTrackList, mPlaylist)) {
906 mService.open(mTrackList, -1, -1, IdType.NA.mId);
910 } catch (final RemoteException ignored) {
915 * Returns The ID for a playlist.
917 * @param context The {@link Context} to use.
918 * @param name The name of the playlist.
919 * @return The ID for a playlist.
921 public static final long getIdForPlaylist(final Context context, final String name) {
922 Cursor cursor = context.getContentResolver().query(
923 MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, new String[]{
925 }, PlaylistsColumns.NAME + "=?", new String[]{
927 }, PlaylistsColumns.NAME);
929 if (cursor != null) {
930 cursor.moveToFirst();
931 if (!cursor.isAfterLast()) {
932 id = cursor.getInt(0);
940 /** @param context The {@link Context} to use.
941 * @param id The id of the playlist.
942 * @return The name for a playlist. */
943 public static final String getNameForPlaylist(final Context context, final long id) {
944 Cursor cursor = context.getContentResolver().query(
945 MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
946 new String[] { PlaylistsColumns.NAME },
947 BaseColumns._ID + "=?",
948 new String[] { Long.toString(id) },
950 if (cursor != null) {
952 if(cursor.moveToFirst()) { return cursor.getString(0); }
953 } finally { cursor.close(); }
960 * Returns the Id for an artist.
962 * @param context The {@link Context} to use.
963 * @param name The name of the artist.
964 * @return The ID for an artist.
966 public static final long getIdForArtist(final Context context, final String name) {
967 Cursor cursor = context.getContentResolver().query(
968 MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI, new String[]{
970 }, ArtistColumns.ARTIST + "=?", new String[]{
972 }, ArtistColumns.ARTIST);
974 if (cursor != null) {
975 cursor.moveToFirst();
976 if (!cursor.isAfterLast()) {
977 id = cursor.getInt(0);
986 * Returns the ID for an album.
988 * @param context The {@link Context} to use.
989 * @param albumName The name of the album.
990 * @param artistName The name of the artist
991 * @return The ID for an album.
993 public static final long getIdForAlbum(final Context context, final String albumName,
994 final String artistName) {
995 Cursor cursor = context.getContentResolver().query(
996 MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, new String[] {
998 }, AlbumColumns.ALBUM + "=? AND " + AlbumColumns.ARTIST + "=?", new String[] {
999 albumName, artistName
1000 }, AlbumColumns.ALBUM);
1002 if (cursor != null) {
1003 cursor.moveToFirst();
1004 if (!cursor.isAfterLast()) {
1005 id = cursor.getInt(0);
1014 * Plays songs from an album.
1016 * @param context The {@link Context} to use.
1017 * @param albumId The album Id.
1018 * @param position Specify where to start.
1020 public static void playAlbum(final Context context, final long albumId, int position, boolean shuffle) {
1021 final long[] albumList = getSongListForAlbum(context, albumId);
1022 if (albumList != null) {
1023 playAll(context, albumList, position, albumId, IdType.Album, shuffle);
1028 public static void makeInsertItems(final long[] ids, final int offset, int len, final int base) {
1029 if (offset + len > ids.length) {
1030 len = ids.length - offset;
1033 if (mContentValuesCache == null || mContentValuesCache.length != len) {
1034 mContentValuesCache = new ContentValues[len];
1036 for (int i = 0; i < len; i++) {
1037 if (mContentValuesCache[i] == null) {
1038 mContentValuesCache[i] = new ContentValues();
1040 mContentValuesCache[i].put(Playlists.Members.PLAY_ORDER, base + offset + i);
1041 mContentValuesCache[i].put(Playlists.Members.AUDIO_ID, ids[offset + i]);
1046 * @param context The {@link Context} to use.
1047 * @param name The name of the new playlist.
1048 * @return A new playlist ID.
1050 public static final long createPlaylist(final Context context, final String name) {
1051 if (name != null && name.length() > 0) {
1052 final ContentResolver resolver = context.getContentResolver();
1053 final String[] projection = new String[] {
1054 PlaylistsColumns.NAME
1056 final String selection = PlaylistsColumns.NAME + " = '" + name + "'";
1057 Cursor cursor = resolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
1058 projection, selection, null, null);
1059 if (cursor.getCount() <= 0) {
1060 final ContentValues values = new ContentValues(1);
1061 values.put(PlaylistsColumns.NAME, name);
1062 final Uri uri = resolver.insert(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
1064 return Long.parseLong(uri.getLastPathSegment());
1066 if (cursor != null) {
1076 * @param context The {@link Context} to use.
1077 * @param playlistId The playlist ID.
1079 public static void clearPlaylist(final Context context, final int playlistId) {
1080 final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
1081 context.getContentResolver().delete(uri, null, null);
1085 /** remove all backing data for top tracks playlist */
1086 public static void clearTopTracks(Context context) {
1087 SongPlayCount.getInstance(context).deleteAll();
1090 /** remove all backing data for top tracks playlist */
1091 public static void clearRecent(Context context) {
1092 RecentStore.getInstance(context).deleteAll();
1095 /** move up cutoff for last added songs so playlist will be cleared */
1096 public static void clearLastAdded(Context context) {
1097 PreferenceUtils.getInstance(context)
1098 .setLastAddedCutoff(System.currentTimeMillis());
1102 * @param context The {@link Context} to use.
1103 * @param ids The id of the song(s) to add.
1104 * @param playlistid The id of the playlist being added to.
1106 public static void addToPlaylist(final Context context, final long[] ids, final long playlistid) {
1107 final int size = ids.length;
1108 final ContentResolver resolver = context.getContentResolver();
1109 final String[] projection = new String[] {
1110 "max(" + Playlists.Members.PLAY_ORDER + ")",
1112 final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistid);
1113 Cursor cursor = null;
1117 cursor = resolver.query(uri, projection, null, null, null);
1119 if (cursor != null && cursor.moveToFirst()) {
1120 base = cursor.getInt(0) + 1;
1123 if (cursor != null) {
1129 int numinserted = 0;
1130 for (int offSet = 0; offSet < size; offSet += 1000) {
1131 makeInsertItems(ids, offSet, 1000, base);
1132 numinserted += resolver.bulkInsert(uri, mContentValuesCache);
1134 final String message = context.getResources().getQuantityString(
1135 R.plurals.NNNtrackstoplaylist, numinserted, numinserted);
1136 Toast.makeText((Activity)context, message, Toast.LENGTH_SHORT).show();
1141 * Removes a single track from a given playlist
1142 * @param context The {@link Context} to use.
1143 * @param id The id of the song to remove.
1144 * @param playlistId The id of the playlist being removed from.
1146 public static void removeFromPlaylist(final Context context, final long id,
1147 final long playlistId) {
1148 final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
1149 final ContentResolver resolver = context.getContentResolver();
1150 resolver.delete(uri, Playlists.Members.AUDIO_ID + " = ? ", new String[] {
1153 final String message = context.getResources().getQuantityString(
1154 R.plurals.NNNtracksfromplaylist, 1, 1);
1155 Toast.makeText((Activity)context, message, Toast.LENGTH_SHORT).show();
1160 * @param context The {@link Context} to use.
1161 * @param list The list to enqueue.
1163 public static void addToQueue(final Context context, final long[] list, long sourceId,
1164 IdType sourceType) {
1165 if (mService == null) {
1169 mService.enqueue(list, MusicPlaybackService.LAST, sourceId, sourceType.mId);
1170 final String message = makeLabel(context, R.plurals.NNNtrackstoqueue, list.length);
1171 Toast.makeText((Activity) context, message, Toast.LENGTH_SHORT).show();
1172 } catch (final RemoteException ignored) {
1177 * @param context The {@link Context} to use
1178 * @param id The song ID.
1180 public static void setRingtone(final Context context, final long id) {
1181 final ContentResolver resolver = context.getContentResolver();
1182 final Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
1184 final ContentValues values = new ContentValues(2);
1185 values.put(AudioColumns.IS_RINGTONE, "1");
1186 values.put(AudioColumns.IS_ALARM, "1");
1187 resolver.update(uri, values, null, null);
1188 } catch (final UnsupportedOperationException ingored) {
1192 final String[] projection = new String[] {
1193 BaseColumns._ID, MediaColumns.DATA, MediaColumns.TITLE
1196 final String selection = BaseColumns._ID + "=" + id;
1197 Cursor cursor = resolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection,
1198 selection, null, null);
1200 if (cursor != null && cursor.getCount() == 1) {
1201 cursor.moveToFirst();
1202 Settings.System.putString(resolver, Settings.System.RINGTONE, uri.toString());
1203 final String message = context.getString(R.string.set_as_ringtone,
1204 cursor.getString(2));
1205 Toast.makeText((Activity)context, message, Toast.LENGTH_SHORT).show();
1208 if (cursor != null) {
1216 * @param context The {@link Context} to use.
1217 * @param id The id of the album.
1218 * @return The song count for an album.
1220 public static final int getSongCountForAlbumInt(final Context context, final long id) {
1222 if (id == -1) { return songCount; }
1224 Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, id);
1225 Cursor cursor = context.getContentResolver().query(uri,
1226 new String[] { AlbumColumns.NUMBER_OF_SONGS }, null, null, null);
1227 if (cursor != null) {
1228 cursor.moveToFirst();
1229 if (!cursor.isAfterLast()) {
1230 if(!cursor.isNull(0)) {
1231 songCount = cursor.getInt(0);
1242 * Gets the number of songs for a playlist
1243 * @param context The {@link Context} to use.
1244 * @param playlistId the id of the playlist
1245 * @return the # of songs in the playlist
1247 public static final int getSongCountForPlaylist(final Context context, final long playlistId) {
1248 Cursor c = context.getContentResolver().query(
1249 MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId),
1250 new String[]{BaseColumns._ID}, MusicUtils.MUSIC_ONLY_SELECTION, null, null);
1254 if (c.moveToFirst()) {
1255 count = c.getCount();
1265 public static final AlbumArtistDetails getAlbumArtDetails(final Context context, final long trackId) {
1266 String selection = (AudioColumns.IS_MUSIC + "=1") +
1267 " AND " + BaseColumns._ID + " = '" + trackId + "'";
1269 Cursor cursor = context.getContentResolver().query(
1270 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1273 MediaStore.Audio.AudioColumns.ALBUM_ID,
1275 MediaStore.Audio.AudioColumns.ALBUM,
1277 MediaStore.Audio.AlbumColumns.ARTIST,
1278 }, selection, null, null
1281 if (!cursor.moveToFirst()) {
1286 AlbumArtistDetails result = new AlbumArtistDetails();
1287 result.mAudioId = trackId;
1288 result.mAlbumId = cursor.getLong(0);
1289 result.mAlbumName = cursor.getString(1);
1290 result.mArtistName = cursor.getString(2);
1297 * @param context The {@link Context} to use.
1298 * @param id The id of the album.
1299 * @return The release date for an album.
1301 public static final String getReleaseDateForAlbum(final Context context, final long id) {
1305 Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, id);
1306 Cursor cursor = context.getContentResolver().query(uri, new String[] {
1307 AlbumColumns.FIRST_YEAR
1308 }, null, null, null);
1309 String releaseDate = null;
1310 if (cursor != null) {
1311 cursor.moveToFirst();
1312 if (!cursor.isAfterLast()) {
1313 releaseDate = cursor.getString(0);
1322 * @return The path to the currently playing file as {@link String}
1324 public static final String getFilePath() {
1326 if (mService != null) {
1327 return mService.getPath();
1329 } catch (final RemoteException ignored) {
1335 * @param from The index the item is currently at.
1336 * @param to The index the item is moving to.
1338 public static void moveQueueItem(final int from, final int to) {
1340 if (mService != null) {
1341 mService.moveQueueItem(from, to);
1344 } catch (final RemoteException ignored) {
1349 * @param context The {@link Context} to sue
1350 * @param playlistId The playlist Id
1351 * @return The track list for a playlist
1353 public static final long[] getSongListForPlaylist(final Context context, final long playlistId) {
1354 Cursor cursor = PlaylistSongLoader.makePlaylistSongCursor(context, playlistId);
1356 if (cursor != null) {
1357 final long[] list = getSongListForCursor(cursor);
1366 * Plays a user created playlist.
1368 * @param context The {@link Context} to use.
1369 * @param playlistId The playlist Id.
1371 public static void playPlaylist(final Context context, final long playlistId, boolean shuffle) {
1372 final long[] playlistList = getSongListForPlaylist(context, playlistId);
1373 if (playlistList != null) {
1374 playAll(context, playlistList, -1, playlistId, IdType.Playlist, shuffle);
1379 * @param context The {@link Context} to use
1380 * @param type The Smart Playlist Type
1381 * @return The song list for the last added playlist
1383 public static final long[] getSongListForSmartPlaylist(final Context context,
1384 final SmartPlaylistType type) {
1385 Cursor cursor = null;
1389 cursor = LastAddedLoader.makeLastAddedCursor(context);
1391 case RecentlyPlayed:
1392 cursor = TopTracksLoader.makeRecentTracksCursor(context);
1395 cursor = TopTracksLoader.makeTopTracksCursor(context);
1398 return MusicUtils.getSongListForCursor(cursor);
1400 if (cursor != null) {
1408 * Plays the smart playlist
1409 * @param context The {@link Context} to use
1410 * @param position the position to start playing from
1411 * @param type The Smart Playlist Type
1413 public static void playSmartPlaylist(final Context context, final int position,
1414 final SmartPlaylistType type, final boolean shuffle) {
1415 final long[] list = getSongListForSmartPlaylist(context, type);
1416 MusicUtils.playAll(context, list, position, type.mId, IdType.Playlist, shuffle);
1420 * Creates a sub menu used to add items to a new playlist or an existsing
1423 * @param context The {@link Context} to use.
1424 * @param groupId The group Id of the menu.
1425 * @param menu The {@link Menu} to add to.
1427 public static void makePlaylistMenu(final Context context, final int groupId,
1430 menu.add(groupId, FragmentMenuItems.NEW_PLAYLIST, Menu.NONE, R.string.new_playlist);
1431 Cursor cursor = PlaylistLoader.makePlaylistCursor(context);
1432 if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) {
1433 while (!cursor.isAfterLast()) {
1434 final Intent intent = new Intent();
1435 String name = cursor.getString(1);
1437 intent.putExtra("playlist", getIdForPlaylist(context, name));
1438 menu.add(groupId, FragmentMenuItems.PLAYLIST_SELECTED, Menu.NONE,
1439 name).setIntent(intent);
1441 cursor.moveToNext();
1444 if (cursor != null) {
1451 * Called when one of the lists should refresh or requery.
1453 public static void refresh() {
1455 if (mService != null) {
1458 } catch (final RemoteException ignored) {
1463 * Called when one of playlists have changed
1465 public static void playlistChanged() {
1467 if (mService != null) {
1468 mService.playlistChanged();
1470 } catch (final RemoteException ignored) {
1475 * Seeks the current track to a desired position
1477 * @param position The position to seek to
1479 public static void seek(final long position) {
1480 if (mService != null) {
1482 mService.seek(position);
1483 } catch (final RemoteException ignored) {
1489 * Seeks the current track to a desired relative position. This can be used
1490 * to simulate fastforward and rewind
1492 * @param deltaInMs The delta in ms to seek from the current position
1494 public static void seekRelative(final long deltaInMs) {
1495 if (mService != null) {
1497 mService.seekRelative(deltaInMs);
1498 } catch (final RemoteException ignored) {
1499 } catch (final IllegalStateException ignored) {
1500 // Illegal State Exception message is empty so logging will actually throw an
1501 // exception. We should come back and figure out why we get an exception in the
1502 // first place and make sure we understand it completely. I will use
1503 // https://cyanogen.atlassian.net/browse/MUSIC-125 to track investigating this more
1509 * @return The current position time of the track
1511 public static final long position() {
1512 if (mService != null) {
1514 return mService.position();
1515 } catch (final RemoteException ignored) {
1516 } catch (final IllegalStateException ex) {
1517 // Illegal State Exception message is empty so logging will actually throw an
1518 // exception. We should come back and figure out why we get an exception in the
1519 // first place and make sure we understand it completely. I will use
1520 // https://cyanogen.atlassian.net/browse/MUSIC-125 to track investigating this more
1527 * @return The total length of the current track
1529 public static final long duration() {
1530 if (mService != null) {
1532 return mService.duration();
1533 } catch (final RemoteException ignored) {
1534 } catch (final IllegalStateException ignored) {
1535 // Illegal State Exception message is empty so logging will actually throw an
1536 // exception. We should come back and figure out why we get an exception in the
1537 // first place and make sure we understand it completely. I will use
1538 // https://cyanogen.atlassian.net/browse/MUSIC-125 to track investigating this more
1545 * @param position The position to move the queue to
1547 public static void setQueuePosition(final int position) {
1548 if (mService != null) {
1550 mService.setQueuePosition(position);
1551 } catch (final RemoteException ignored) {
1559 public static void clearQueue() {
1561 mService.removeTracks(0, Integer.MAX_VALUE);
1562 } catch (final RemoteException ignored) {
1567 * Perminately deletes item(s) from the user's device
1569 * @param context The {@link Context} to use.
1570 * @param list The item(s) to delete.
1572 public static void deleteTracks(final Context context, final long[] list) {
1573 final String[] projection = new String[] {
1574 BaseColumns._ID, MediaColumns.DATA, AudioColumns.ALBUM_ID
1576 final StringBuilder selection = new StringBuilder();
1577 selection.append(BaseColumns._ID + " IN (");
1578 for (int i = 0; i < list.length; i++) {
1579 selection.append(list[i]);
1580 if (i < list.length - 1) {
1581 selection.append(",");
1584 selection.append(")");
1585 final Cursor c = context.getContentResolver().query(
1586 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection.toString(),
1589 // Step 1: Remove selected tracks from the current playlist, as well
1590 // as from the album art cache
1592 while (!c.isAfterLast()) {
1593 // Remove from current playlist
1594 final long id = c.getLong(0);
1596 // Remove the track from the play count
1597 SongPlayCount.getInstance(context).removeItem(id);
1598 // Remove any items in the recents database
1599 RecentStore.getInstance(context).removeItem(id);
1603 // Step 2: Remove selected tracks from the database
1604 context.getContentResolver().delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1605 selection.toString(), null);
1607 // Step 3: Remove files from card
1609 while (!c.isAfterLast()) {
1610 final String name = c.getString(1);
1611 final File f = new File(name);
1612 try { // File.delete can throw a security exception
1614 // I'm not sure if we'd ever get here (deletion would
1615 // have to fail, but no exception thrown)
1616 Log.e("MusicUtils", "Failed to delete file " + name);
1619 } catch (final SecurityException ex) {
1626 final String message = makeLabel(context, R.plurals.NNNtracksdeleted, list.length);
1628 Toast.makeText((Activity)context, message, Toast.LENGTH_SHORT).show();
1629 // We deleted a number of tracks, which could affect any number of
1631 // in the media content domain, so update everything.
1632 context.getContentResolver().notifyChange(Uri.parse("content://media"), null);
1633 // Notify the lists to update
1638 * Simple function used to determine if the song/album year is invalid
1639 * @param year value to test
1640 * @return true if the app considers it valid
1642 public static boolean isInvalidYear(int year) {
1643 return year < MIN_VALID_YEAR;
1647 * A snippet is taken from MediaStore.Audio.keyFor method
1648 * This will take a name, removes things like "the", "an", etc
1649 * as well as special characters and return it
1650 * @param name the string to trim
1651 * @return the trimmed name
1653 public static String getTrimmedName(String name) {
1654 if (name == null || name.length() == 0) {
1658 name = name.trim().toLowerCase();
1659 if (name.startsWith("the ")) {
1660 name = name.substring(4);
1662 if (name.startsWith("an ")) {
1663 name = name.substring(3);
1665 if (name.startsWith("a ")) {
1666 name = name.substring(2);
1668 if (name.endsWith(", the") || name.endsWith(",the") ||
1669 name.endsWith(", an") || name.endsWith(",an") ||
1670 name.endsWith(", a") || name.endsWith(",a")) {
1671 name = name.substring(0, name.lastIndexOf(','));
1673 name = name.replaceAll("[\\[\\]\\(\\)\"'.,?!]", "").trim();
1679 * A snippet is taken from MediaStore.Audio.keyFor method
1680 * This will take a name, removes things like "the", "an", etc
1681 * as well as special characters, then find the localized label
1682 * @param name Name to get the label of
1683 * @return the localized label of the bucket that the name falls into
1685 public static String getLocalizedBucketLetter(String name) {
1686 if (name == null || name.length() == 0) {
1690 name = getTrimmedName(name);
1692 if (name.length() > 0) {
1693 return LocaleUtils.getInstance().getLabel(name);
1699 /** @return true if a string is null, empty, or contains only whitespace */
1700 public static boolean isBlank(String s) {
1701 if(s == null) { return true; }
1702 if(s.isEmpty()) { return true; }
1703 for(int i = 0; i < s.length(); i++) {
1704 char c = s.charAt(i);
1705 if(!Character.isWhitespace(c)) { return false; }
1711 * Removes the header image from the cache.
1713 public static void removeFromCache(Activity activity, String key) {
1714 ImageFetcher imageFetcher = ApolloUtils.getImageFetcher(activity);
1715 imageFetcher.removeFromCache(key);
1716 // Give the disk cache a little time before requesting a new image.
1717 SystemClock.sleep(80);
1721 * Removes image from cache so that the stock image is retrieved on reload
1723 public static void selectOldPhoto(Activity activity, String key) {
1724 // First remove the old image
1725 removeFromCache(activity, key);
1726 MusicUtils.refresh();
1731 * @param sortOrder values are mostly derived from SortOrder.class or could also be any sql
1735 public static boolean isSortOrderDesending(String sortOrder) {
1736 return sortOrder.endsWith(" DESC");
1740 * Takes a collection of items and builds a comma-separated list of them
1741 * @param items collection of items
1742 * @return comma-separted list of items
1744 public static final <E> String buildCollectionAsString(Collection<E> items) {
1745 Iterator<E> iterator = items.iterator();
1746 StringBuilder str = new StringBuilder();
1747 if (iterator.hasNext()) {
1748 str.append(iterator.next());
1749 while (iterator.hasNext()) {
1751 str.append(iterator.next());
1755 return str.toString();