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 int sForegroundActivities = 0;
81 private static final WeakHashMap<Context, ServiceBinder> mConnectionMap;
83 private static final long[] sEmptyList;
85 private static ContentValues[] mContentValuesCache = null;
87 private static final int MIN_VALID_YEAR = 1900; // used to remove invalid years from metadata
89 public static final String MUSIC_ONLY_SELECTION = MediaStore.Audio.AudioColumns.IS_MUSIC + "=1"
90 + " AND " + MediaStore.Audio.AudioColumns.TITLE + " != ''"; //$NON-NLS-2$
93 mConnectionMap = new WeakHashMap<Context, ServiceBinder>();
94 sEmptyList = new long[0];
97 /* This class is never initiated */
102 * @param context The {@link Context} to use
103 * @param callback The {@link ServiceConnection} to use
104 * @return The new instance of {@link ServiceToken}
106 public static final ServiceToken bindToService(final Context context,
107 final ServiceConnection callback) {
108 Activity realActivity = ((Activity)context).getParent();
109 if (realActivity == null) {
110 realActivity = (Activity)context;
112 final ContextWrapper contextWrapper = new ContextWrapper(realActivity);
113 contextWrapper.startService(new Intent(contextWrapper, MusicPlaybackService.class));
114 final ServiceBinder binder = new ServiceBinder(callback);
115 if (contextWrapper.bindService(
116 new Intent().setClass(contextWrapper, MusicPlaybackService.class), binder, 0)) {
117 mConnectionMap.put(contextWrapper, binder);
118 return new ServiceToken(contextWrapper);
124 * @param token The {@link ServiceToken} to unbind from
126 public static void unbindFromService(final ServiceToken token) {
130 final ContextWrapper mContextWrapper = token.mWrappedContext;
131 final ServiceBinder mBinder = mConnectionMap.remove(mContextWrapper);
132 if (mBinder == null) {
135 mContextWrapper.unbindService(mBinder);
136 if (mConnectionMap.isEmpty()) {
141 public static final class ServiceBinder implements ServiceConnection {
142 private final ServiceConnection mCallback;
145 * Constructor of <code>ServiceBinder</code>
147 * @param context The {@link ServiceConnection} to use
149 public ServiceBinder(final ServiceConnection callback) {
150 mCallback = callback;
154 public void onServiceConnected(final ComponentName className, final IBinder service) {
155 mService = IElevenService.Stub.asInterface(service);
156 if (mCallback != null) {
157 mCallback.onServiceConnected(className, service);
162 public void onServiceDisconnected(final ComponentName className) {
163 if (mCallback != null) {
164 mCallback.onServiceDisconnected(className);
170 public static final class ServiceToken {
171 public ContextWrapper mWrappedContext;
174 * Constructor of <code>ServiceToken</code>
176 * @param context The {@link ContextWrapper} to use
178 public ServiceToken(final ContextWrapper context) {
179 mWrappedContext = context;
183 public static final boolean isPlaybackServiceConnected() {
184 return mService != null;
188 * Used to make number of labels for the number of artists, albums, songs,
189 * genres, and playlists.
191 * @param context The {@link Context} to use.
192 * @param pluralInt The ID of the plural string to use.
193 * @param number The number of artists, albums, songs, genres, or playlists.
194 * @return A {@link String} used as a label for the number of artists,
195 * albums, songs, genres, and playlists.
197 public static final String makeLabel(final Context context, final int pluralInt,
199 return context.getResources().getQuantityString(pluralInt, number, number);
203 * * Used to create a formatted time string for the duration of tracks.
205 * @param context The {@link Context} to use.
206 * @param secs The track in seconds.
207 * @return Duration of a track that's properly formatted.
209 public static final String makeShortTimeString(final Context context, long secs) {
217 final String durationFormat = context.getResources().getString(
218 hours == 0 ? R.string.durationformatshort : R.string.durationformatlong);
219 return String.format(durationFormat, hours, mins, secs);
223 * Used to create a formatted time string in the format of #h #m or #m if there is only minutes
225 * @param context The {@link Context} to use.
226 * @param secs The duration seconds.
227 * @return Duration properly formatted in #h #m format
229 public static final String makeLongTimeString(final Context context, long secs) {
236 String hoursString = MusicUtils.makeLabel(context, R.plurals.Nhours, (int)hours);
237 String minutesString = MusicUtils.makeLabel(context, R.plurals.Nminutes, (int)mins);
240 return minutesString;
241 } else if (mins == 0) {
245 final String durationFormat = context.getResources().getString(R.string.duration_format);
246 return String.format(durationFormat, hoursString, minutesString);
250 * Used to combine two strings with some kind of separator in between
252 * @param context The {@link Context} to use.
253 * @param first string to combine
254 * @param second string to combine
255 * @return the combined string
257 public static final String makeCombinedString(final Context context, final String first,
258 final String second) {
259 final String formatter = context.getResources().getString(R.string.combine_two_strings);
260 return String.format(formatter, first, second);
264 * Changes to the next track
266 public static void next() {
268 if (mService != null) {
271 } catch (final RemoteException ignored) {
276 * Changes to the next track asynchronously
278 public static void asyncNext(final Context context) {
279 final Intent previous = new Intent(context, MusicPlaybackService.class);
280 previous.setAction(MusicPlaybackService.NEXT_ACTION);
281 context.startService(previous);
285 * Changes to the previous track.
287 * @NOTE The AIDL isn't used here in order to properly use the previous
288 * action. When the user is shuffling, because {@link
289 * MusicPlaybackService.#openCurrentAndNext()} is used, the user won't
290 * be able to travel to the previously skipped track. To remedy this,
291 * {@link MusicPlaybackService.#openCurrent()} is called in {@link
292 * MusicPlaybackService.#prev()}. {@code #startService(Intent intent)}
293 * is called here to specifically invoke the onStartCommand used by
294 * {@link MusicPlaybackService}, which states if the current position
295 * less than 2000 ms, start the track over, otherwise move to the
296 * previously listened track.
298 public static void previous(final Context context, final boolean force) {
299 final Intent previous = new Intent(context, MusicPlaybackService.class);
301 previous.setAction(MusicPlaybackService.PREVIOUS_FORCE_ACTION);
303 previous.setAction(MusicPlaybackService.PREVIOUS_ACTION);
305 context.startService(previous);
309 * Plays or pauses the music.
311 public static void playOrPause() {
313 if (mService != null) {
314 if (mService.isPlaying()) {
320 } catch (final Exception ignored) {
325 * Cycles through the repeat options.
327 public static void cycleRepeat() {
329 if (mService != null) {
330 switch (mService.getRepeatMode()) {
331 case MusicPlaybackService.REPEAT_NONE:
332 mService.setRepeatMode(MusicPlaybackService.REPEAT_ALL);
334 case MusicPlaybackService.REPEAT_ALL:
335 mService.setRepeatMode(MusicPlaybackService.REPEAT_CURRENT);
336 if (mService.getShuffleMode() != MusicPlaybackService.SHUFFLE_NONE) {
337 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
341 mService.setRepeatMode(MusicPlaybackService.REPEAT_NONE);
345 } catch (final RemoteException ignored) {
350 * Cycles through the shuffle options.
352 public static void cycleShuffle() {
354 if (mService != null) {
355 switch (mService.getShuffleMode()) {
356 case MusicPlaybackService.SHUFFLE_NONE:
357 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
358 if (mService.getRepeatMode() == MusicPlaybackService.REPEAT_CURRENT) {
359 mService.setRepeatMode(MusicPlaybackService.REPEAT_ALL);
362 case MusicPlaybackService.SHUFFLE_NORMAL:
363 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
365 case MusicPlaybackService.SHUFFLE_AUTO:
366 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
372 } catch (final RemoteException ignored) {
377 * @return True if we're playing music, false otherwise.
379 public static final boolean isPlaying() {
380 if (mService != null) {
382 return mService.isPlaying();
383 } catch (final RemoteException ignored) {
390 * @return The current shuffle mode.
392 public static final int getShuffleMode() {
393 if (mService != null) {
395 return mService.getShuffleMode();
396 } catch (final RemoteException ignored) {
403 * @return The current repeat mode.
405 public static final int getRepeatMode() {
406 if (mService != null) {
408 return mService.getRepeatMode();
409 } catch (final RemoteException ignored) {
416 * @return The current track name.
418 public static final String getTrackName() {
419 if (mService != null) {
421 return mService.getTrackName();
422 } catch (final RemoteException ignored) {
429 * @return The current artist name.
431 public static final String getArtistName() {
432 if (mService != null) {
434 return mService.getArtistName();
435 } catch (final RemoteException ignored) {
442 * @return The current album name.
444 public static final String getAlbumName() {
445 if (mService != null) {
447 return mService.getAlbumName();
448 } catch (final RemoteException ignored) {
455 * @return The current album Id.
457 public static final long getCurrentAlbumId() {
458 if (mService != null) {
460 return mService.getAlbumId();
461 } catch (final RemoteException ignored) {
468 * @return The current song Id.
470 public static final long getCurrentAudioId() {
471 if (mService != null) {
473 return mService.getAudioId();
474 } catch (final RemoteException ignored) {
481 * @return The current Music Playback Track
483 public static final MusicPlaybackTrack getCurrentTrack() {
484 if (mService != null) {
486 return mService.getCurrentTrack();
487 } catch (final RemoteException ignored) {
494 * @return The Music Playback Track at the specified index
496 public static final MusicPlaybackTrack getTrack(int index) {
497 if (mService != null) {
499 return mService.getTrack(index);
500 } catch (final RemoteException ignored) {
507 * @return The next song Id.
509 public static final long getNextAudioId() {
510 if (mService != null) {
512 return mService.getNextAudioId();
513 } catch (final RemoteException ignored) {
520 * @return The previous song Id.
522 public static final long getPreviousAudioId() {
523 if (mService != null) {
525 return mService.getPreviousAudioId();
526 } catch (final RemoteException ignored) {
533 * @return The current artist Id.
535 public static final long getCurrentArtistId() {
536 if (mService != null) {
538 return mService.getArtistId();
539 } catch (final RemoteException ignored) {
546 * @return The audio session Id.
548 public static final int getAudioSessionId() {
549 if (mService != null) {
551 return mService.getAudioSessionId();
552 } catch (final RemoteException ignored) {
561 public static final long[] getQueue() {
563 if (mService != null) {
564 return mService.getQueue();
567 } catch (final RemoteException ignored) {
574 * @return the id of the track in the queue at the given position
576 public static final long getQueueItemAtPosition(int position) {
578 if (mService != null) {
579 return mService.getQueueItemAtPosition(position);
582 } catch (final RemoteException ignored) {
588 * @return the current queue size
590 public static final int getQueueSize() {
592 if (mService != null) {
593 return mService.getQueueSize();
596 } catch (final RemoteException ignored) {
602 * @return The position of the current track in the queue.
604 public static final int getQueuePosition() {
606 if (mService != null) {
607 return mService.getQueuePosition();
609 } catch (final RemoteException ignored) {
615 * @return The queue history size
617 public static final int getQueueHistorySize() {
618 if (mService != null) {
620 return mService.getQueueHistorySize();
621 } catch (final RemoteException ignored) {
628 * @return The queue history position at the position
630 public static final int getQueueHistoryPosition(int position) {
631 if (mService != null) {
633 return mService.getQueueHistoryPosition(position);
634 } catch (final RemoteException ignored) {
641 * @return The queue history
643 public static final int[] getQueueHistoryList() {
644 if (mService != null) {
646 return mService.getQueueHistoryList();
647 } catch (final RemoteException ignored) {
654 * @param id The ID of the track to remove.
655 * @return removes track from a playlist or the queue.
657 public static final int removeTrack(final long id) {
659 if (mService != null) {
660 return mService.removeTrack(id);
662 } catch (final RemoteException ingored) {
668 * Remove song at a specified position in the list
670 * @param id The ID of the track to remove
671 * @param position The position of the song
673 * @return true if successful, false otherwise
675 public static final boolean removeTrackAtPosition(final long id, final int position) {
677 if (mService != null) {
678 return mService.removeTrackAtPosition(id, position);
680 } catch (final RemoteException ingored) {
686 * @param cursor The {@link Cursor} used to perform our query.
687 * @return The song list for a MIME type.
689 public static final long[] getSongListForCursor(Cursor cursor) {
690 if (cursor == null) {
693 final int len = cursor.getCount();
694 final long[] list = new long[len];
695 cursor.moveToFirst();
696 int columnIndex = -1;
698 columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.Members.AUDIO_ID);
699 } catch (final IllegalArgumentException notaplaylist) {
700 columnIndex = cursor.getColumnIndexOrThrow(BaseColumns._ID);
702 for (int i = 0; i < len; i++) {
703 list[i] = cursor.getLong(columnIndex);
712 * @param context The {@link Context} to use.
713 * @param id The ID of the artist.
714 * @return The song list for an artist.
716 public static final long[] getSongListForArtist(final Context context, final long id) {
717 final String[] projection = new String[] {
720 final String selection = AudioColumns.ARTIST_ID + "=" + id + " AND "
721 + AudioColumns.IS_MUSIC + "=1";
722 Cursor cursor = context.getContentResolver().query(
723 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null,
724 AudioColumns.ALBUM_KEY + "," + AudioColumns.TRACK);
725 if (cursor != null) {
726 final long[] mList = getSongListForCursor(cursor);
735 * @param context The {@link Context} to use.
736 * @param id The ID of the album.
737 * @return The song list for an album.
739 public static final long[] getSongListForAlbum(final Context context, final long id) {
740 final String[] projection = new String[] {
743 final String selection = AudioColumns.ALBUM_ID + "=" + id + " AND " + AudioColumns.IS_MUSIC
745 Cursor cursor = context.getContentResolver().query(
746 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null,
747 AudioColumns.TRACK + ", " + MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
748 if (cursor != null) {
749 final long[] mList = getSongListForCursor(cursor);
758 * Plays songs by an artist.
760 * @param context The {@link Context} to use.
761 * @param artistId The artist Id.
762 * @param position Specify where to start.
764 public static void playArtist(final Context context, final long artistId, int position, boolean shuffle) {
765 final long[] artistList = getSongListForArtist(context, artistId);
766 if (artistList != null) {
767 playAll(context, artistList, position, artistId, IdType.Artist, shuffle);
772 * @param context The {@link Context} to use.
773 * @param id The ID of the genre.
774 * @return The song list for an genre.
776 public static final long[] getSongListForGenre(final Context context, final long id) {
777 final String[] projection = new String[] {
780 final StringBuilder selection = new StringBuilder();
781 selection.append(AudioColumns.IS_MUSIC + "=1");
782 selection.append(" AND " + MediaColumns.TITLE + "!=''");
783 final Uri uri = MediaStore.Audio.Genres.Members.getContentUri("external", Long.valueOf(id));
784 Cursor cursor = context.getContentResolver().query(uri, projection, selection.toString(),
786 if (cursor != null) {
787 final long[] mList = getSongListForCursor(cursor);
796 * @param context The {@link Context} to use
797 * @param uri The source of the file
799 public static void playFile(final Context context, final Uri uri) {
800 if (uri == null || mService == null) {
804 // If this is a file:// URI, just use the path directly instead
805 // of going through the open-from-filedescriptor codepath.
807 String scheme = uri.getScheme();
808 if ("file".equals(scheme)) {
809 filename = uri.getPath();
811 filename = uri.toString();
816 mService.openFile(filename);
818 } catch (final RemoteException ignored) {
823 * @param context The {@link Context} to use.
824 * @param list The list of songs to play.
825 * @param position Specify where to start.
826 * @param forceShuffle True to force a shuffle, false otherwise.
828 public static void playAll(final Context context, final long[] list, int position,
829 final long sourceId, final IdType sourceType,
830 final boolean forceShuffle) {
831 if (list == null || list.length == 0 || mService == null) {
836 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
838 final long currentId = mService.getAudioId();
839 final int currentQueuePosition = getQueuePosition();
840 if (position != -1 && currentQueuePosition == position && currentId == list[position]) {
841 final long[] playlist = getQueue();
842 if (Arrays.equals(list, playlist)) {
850 mService.open(list, forceShuffle ? -1 : position, sourceId, sourceType.mId);
852 } catch (final RemoteException ignored) {
857 * @param list The list to enqueue.
859 public static void playNext(final long[] list, final long sourceId, final IdType sourceType) {
860 if (mService == null) {
864 mService.enqueue(list, MusicPlaybackService.NEXT, sourceId, sourceType.mId);
865 } catch (final RemoteException ignored) {
870 * @param context The {@link Context} to use.
872 public static void shuffleAll(final Context context) {
873 Cursor cursor = SongLoader.makeSongCursor(context, null);
874 final long[] mTrackList = getSongListForCursor(cursor);
875 final int position = 0;
876 if (mTrackList.length == 0 || mService == null) {
880 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
881 final long mCurrentId = mService.getAudioId();
882 final int mCurrentQueuePosition = getQueuePosition();
883 if (position != -1 && mCurrentQueuePosition == position
884 && mCurrentId == mTrackList[position]) {
885 final long[] mPlaylist = getQueue();
886 if (Arrays.equals(mTrackList, mPlaylist)) {
891 mService.open(mTrackList, -1, -1, IdType.NA.mId);
895 } catch (final RemoteException ignored) {
900 * Returns The ID for a playlist.
902 * @param context The {@link Context} to use.
903 * @param name The name of the playlist.
904 * @return The ID for a playlist.
906 public static final long getIdForPlaylist(final Context context, final String name) {
907 Cursor cursor = context.getContentResolver().query(
908 MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, new String[]{
910 }, PlaylistsColumns.NAME + "=?", new String[]{
912 }, PlaylistsColumns.NAME);
914 if (cursor != null) {
915 cursor.moveToFirst();
916 if (!cursor.isAfterLast()) {
917 id = cursor.getInt(0);
925 /** @param context The {@link Context} to use.
926 * @param id The id of the playlist.
927 * @return The name for a playlist. */
928 public static final String getNameForPlaylist(final Context context, final long id) {
929 Cursor cursor = context.getContentResolver().query(
930 MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
931 new String[] { PlaylistsColumns.NAME },
932 BaseColumns._ID + "=?",
933 new String[] { Long.toString(id) },
935 if (cursor != null) {
937 if(cursor.moveToFirst()) { return cursor.getString(0); }
938 } finally { cursor.close(); }
945 * Returns the Id for an artist.
947 * @param context The {@link Context} to use.
948 * @param name The name of the artist.
949 * @return The ID for an artist.
951 public static final long getIdForArtist(final Context context, final String name) {
952 Cursor cursor = context.getContentResolver().query(
953 MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI, new String[]{
955 }, ArtistColumns.ARTIST + "=?", new String[]{
957 }, ArtistColumns.ARTIST);
959 if (cursor != null) {
960 cursor.moveToFirst();
961 if (!cursor.isAfterLast()) {
962 id = cursor.getInt(0);
971 * Returns the ID for an album.
973 * @param context The {@link Context} to use.
974 * @param albumName The name of the album.
975 * @param artistName The name of the artist
976 * @return The ID for an album.
978 public static final long getIdForAlbum(final Context context, final String albumName,
979 final String artistName) {
980 Cursor cursor = context.getContentResolver().query(
981 MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, new String[] {
983 }, AlbumColumns.ALBUM + "=? AND " + AlbumColumns.ARTIST + "=?", new String[] {
984 albumName, artistName
985 }, AlbumColumns.ALBUM);
987 if (cursor != null) {
988 cursor.moveToFirst();
989 if (!cursor.isAfterLast()) {
990 id = cursor.getInt(0);
999 * Plays songs from an album.
1001 * @param context The {@link Context} to use.
1002 * @param albumId The album Id.
1003 * @param position Specify where to start.
1005 public static void playAlbum(final Context context, final long albumId, int position, boolean shuffle) {
1006 final long[] albumList = getSongListForAlbum(context, albumId);
1007 if (albumList != null) {
1008 playAll(context, albumList, position, albumId, IdType.Album, shuffle);
1013 public static void makeInsertItems(final long[] ids, final int offset, int len, final int base) {
1014 if (offset + len > ids.length) {
1015 len = ids.length - offset;
1018 if (mContentValuesCache == null || mContentValuesCache.length != len) {
1019 mContentValuesCache = new ContentValues[len];
1021 for (int i = 0; i < len; i++) {
1022 if (mContentValuesCache[i] == null) {
1023 mContentValuesCache[i] = new ContentValues();
1025 mContentValuesCache[i].put(Playlists.Members.PLAY_ORDER, base + offset + i);
1026 mContentValuesCache[i].put(Playlists.Members.AUDIO_ID, ids[offset + i]);
1031 * @param context The {@link Context} to use.
1032 * @param name The name of the new playlist.
1033 * @return A new playlist ID.
1035 public static final long createPlaylist(final Context context, final String name) {
1036 if (name != null && name.length() > 0) {
1037 final ContentResolver resolver = context.getContentResolver();
1038 final String[] projection = new String[] {
1039 PlaylistsColumns.NAME
1041 final String selection = PlaylistsColumns.NAME + " = '" + name + "'";
1042 Cursor cursor = resolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
1043 projection, selection, null, null);
1044 if (cursor.getCount() <= 0) {
1045 final ContentValues values = new ContentValues(1);
1046 values.put(PlaylistsColumns.NAME, name);
1047 final Uri uri = resolver.insert(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
1049 return Long.parseLong(uri.getLastPathSegment());
1051 if (cursor != null) {
1061 * @param context The {@link Context} to use.
1062 * @param playlistId The playlist ID.
1064 public static void clearPlaylist(final Context context, final int playlistId) {
1065 final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
1066 context.getContentResolver().delete(uri, null, null);
1070 /** remove all backing data for top tracks playlist */
1071 public static void clearTopTracks(Context context) {
1072 SongPlayCount.getInstance(context).deleteAll();
1075 /** remove all backing data for top tracks playlist */
1076 public static void clearRecent(Context context) {
1077 RecentStore.getInstance(context).deleteAll();
1080 /** move up cutoff for last added songs so playlist will be cleared */
1081 public static void clearLastAdded(Context context) {
1082 PreferenceUtils.getInstance(context)
1083 .setLastAddedCutoff(System.currentTimeMillis());
1087 * @param context The {@link Context} to use.
1088 * @param ids The id of the song(s) to add.
1089 * @param playlistid The id of the playlist being added to.
1091 public static void addToPlaylist(final Context context, final long[] ids, final long playlistid) {
1092 final int size = ids.length;
1093 final ContentResolver resolver = context.getContentResolver();
1094 final String[] projection = new String[] {
1095 "max(" + Playlists.Members.PLAY_ORDER + ")",
1097 final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistid);
1098 Cursor cursor = null;
1102 cursor = resolver.query(uri, projection, null, null, null);
1104 if (cursor != null && cursor.moveToFirst()) {
1105 base = cursor.getInt(0) + 1;
1108 if (cursor != null) {
1114 int numinserted = 0;
1115 for (int offSet = 0; offSet < size; offSet += 1000) {
1116 makeInsertItems(ids, offSet, 1000, base);
1117 numinserted += resolver.bulkInsert(uri, mContentValuesCache);
1119 final String message = context.getResources().getQuantityString(
1120 R.plurals.NNNtrackstoplaylist, numinserted, numinserted);
1121 CustomToast.makeText((Activity)context, message, CustomToast.LENGTH_SHORT).show();
1126 * Removes a single track from a given playlist
1127 * @param context The {@link Context} to use.
1128 * @param id The id of the song to remove.
1129 * @param playlistId The id of the playlist being removed from.
1131 public static void removeFromPlaylist(final Context context, final long id,
1132 final long playlistId) {
1133 final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
1134 final ContentResolver resolver = context.getContentResolver();
1135 resolver.delete(uri, Playlists.Members.AUDIO_ID + " = ? ", new String[] {
1138 final String message = context.getResources().getQuantityString(
1139 R.plurals.NNNtracksfromplaylist, 1, 1);
1140 CustomToast.makeText((Activity)context, message, CustomToast.LENGTH_SHORT).show();
1145 * @param context The {@link Context} to use.
1146 * @param list The list to enqueue.
1148 public static void addToQueue(final Context context, final long[] list, long sourceId,
1149 IdType sourceType) {
1150 if (mService == null) {
1154 mService.enqueue(list, MusicPlaybackService.LAST, sourceId, sourceType.mId);
1155 final String message = makeLabel(context, R.plurals.NNNtrackstoqueue, list.length);
1156 CustomToast.makeText((Activity) context, message, CustomToast.LENGTH_SHORT).show();
1157 } catch (final RemoteException ignored) {
1162 * @param context The {@link Context} to use
1163 * @param id The song ID.
1165 public static void setRingtone(final Context context, final long id) {
1166 final ContentResolver resolver = context.getContentResolver();
1167 final Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
1169 final ContentValues values = new ContentValues(2);
1170 values.put(AudioColumns.IS_RINGTONE, "1");
1171 values.put(AudioColumns.IS_ALARM, "1");
1172 resolver.update(uri, values, null, null);
1173 } catch (final UnsupportedOperationException ingored) {
1177 final String[] projection = new String[] {
1178 BaseColumns._ID, MediaColumns.DATA, MediaColumns.TITLE
1181 final String selection = BaseColumns._ID + "=" + id;
1182 Cursor cursor = resolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection,
1183 selection, null, null);
1185 if (cursor != null && cursor.getCount() == 1) {
1186 cursor.moveToFirst();
1187 Settings.System.putString(resolver, Settings.System.RINGTONE, uri.toString());
1188 final String message = context.getString(R.string.set_as_ringtone,
1189 cursor.getString(2));
1190 CustomToast.makeText((Activity)context, message, CustomToast.LENGTH_SHORT).show();
1193 if (cursor != null) {
1201 * @param context The {@link Context} to use.
1202 * @param id The id of the album.
1203 * @return The song count for an album.
1205 public static final int getSongCountForAlbumInt(final Context context, final long id) {
1207 if (id == -1) { return songCount; }
1209 Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, id);
1210 Cursor cursor = context.getContentResolver().query(uri,
1211 new String[] { AlbumColumns.NUMBER_OF_SONGS }, null, null, null);
1212 if (cursor != null) {
1213 cursor.moveToFirst();
1214 if (!cursor.isAfterLast()) {
1215 if(!cursor.isNull(0)) {
1216 songCount = cursor.getInt(0);
1227 * Gets the number of songs for a playlist
1228 * @param context The {@link Context} to use.
1229 * @param playlistId the id of the playlist
1230 * @return the # of songs in the playlist
1232 public static final int getSongCountForPlaylist(final Context context, final long playlistId) {
1233 Cursor c = context.getContentResolver().query(
1234 MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId),
1235 new String[]{BaseColumns._ID}, MusicUtils.MUSIC_ONLY_SELECTION, null, null);
1239 if (c.moveToFirst()) {
1240 count = c.getCount();
1250 public static final AlbumArtistDetails getAlbumArtDetails(final Context context, final long trackId) {
1251 final StringBuilder selection = new StringBuilder();
1252 selection.append(MediaStore.Audio.AudioColumns.IS_MUSIC + "=1");
1253 selection.append(" AND " + BaseColumns._ID + " = '" + trackId + "'");
1255 Cursor cursor = context.getContentResolver().query(
1256 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1259 MediaStore.Audio.AudioColumns.ALBUM_ID,
1261 MediaStore.Audio.AudioColumns.ALBUM,
1263 MediaStore.Audio.AlbumColumns.ARTIST,
1264 }, selection.toString(), null, null
1267 if (!cursor.moveToFirst()) {
1272 AlbumArtistDetails result = new AlbumArtistDetails();
1273 result.mAudioId = trackId;
1274 result.mAlbumId = cursor.getLong(0);
1275 result.mAlbumName = cursor.getString(1);
1276 result.mArtistName = cursor.getString(2);
1283 * @param context The {@link Context} to use.
1284 * @param id The id of the album.
1285 * @return The release date for an album.
1287 public static final String getReleaseDateForAlbum(final Context context, final long id) {
1291 Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, id);
1292 Cursor cursor = context.getContentResolver().query(uri, new String[] {
1293 AlbumColumns.FIRST_YEAR
1294 }, null, null, null);
1295 String releaseDate = null;
1296 if (cursor != null) {
1297 cursor.moveToFirst();
1298 if (!cursor.isAfterLast()) {
1299 releaseDate = cursor.getString(0);
1308 * @return The path to the currently playing file as {@link String}
1310 public static final String getFilePath() {
1312 if (mService != null) {
1313 return mService.getPath();
1315 } catch (final RemoteException ignored) {
1321 * @param from The index the item is currently at.
1322 * @param to The index the item is moving to.
1324 public static void moveQueueItem(final int from, final int to) {
1326 if (mService != null) {
1327 mService.moveQueueItem(from, to);
1330 } catch (final RemoteException ignored) {
1335 * @param context The {@link Context} to sue
1336 * @param playlistId The playlist Id
1337 * @return The track list for a playlist
1339 public static final long[] getSongListForPlaylist(final Context context, final long playlistId) {
1340 Cursor cursor = PlaylistSongLoader.makePlaylistSongCursor(context, playlistId);
1342 if (cursor != null) {
1343 final long[] list = getSongListForCursor(cursor);
1352 * Plays a user created playlist.
1354 * @param context The {@link Context} to use.
1355 * @param playlistId The playlist Id.
1357 public static void playPlaylist(final Context context, final long playlistId, boolean shuffle) {
1358 final long[] playlistList = getSongListForPlaylist(context, playlistId);
1359 if (playlistList != null) {
1360 playAll(context, playlistList, -1, playlistId, IdType.Playlist, shuffle);
1365 * @param context The {@link Context} to use
1366 * @param type The Smart Playlist Type
1367 * @return The song list for the last added playlist
1369 public static final long[] getSongListForSmartPlaylist(final Context context,
1370 final SmartPlaylistType type) {
1371 Cursor cursor = null;
1375 cursor = LastAddedLoader.makeLastAddedCursor(context);
1377 case RecentlyPlayed:
1378 cursor = TopTracksLoader.makeRecentTracksCursor(context);
1381 cursor = TopTracksLoader.makeTopTracksCursor(context);
1384 return MusicUtils.getSongListForCursor(cursor);
1386 if (cursor != null) {
1394 * Plays the smart playlist
1395 * @param context The {@link Context} to use
1396 * @param position the position to start playing from
1397 * @param type The Smart Playlist Type
1399 public static void playSmartPlaylist(final Context context, final int position,
1400 final SmartPlaylistType type, final boolean shuffle) {
1401 final long[] list = getSongListForSmartPlaylist(context, type);
1402 MusicUtils.playAll(context, list, position, type.mId, IdType.Playlist, shuffle);
1406 * Creates a sub menu used to add items to a new playlist or an existsing
1409 * @param context The {@link Context} to use.
1410 * @param groupId The group Id of the menu.
1411 * @param menu The {@link Menu} to add to.
1413 public static void makePlaylistMenu(final Context context, final int groupId,
1416 menu.add(groupId, FragmentMenuItems.NEW_PLAYLIST, Menu.NONE, R.string.new_playlist);
1417 Cursor cursor = PlaylistLoader.makePlaylistCursor(context);
1418 if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) {
1419 while (!cursor.isAfterLast()) {
1420 final Intent intent = new Intent();
1421 String name = cursor.getString(1);
1423 intent.putExtra("playlist", getIdForPlaylist(context, name));
1424 menu.add(groupId, FragmentMenuItems.PLAYLIST_SELECTED, Menu.NONE,
1425 name).setIntent(intent);
1427 cursor.moveToNext();
1430 if (cursor != null) {
1437 * Called when one of the lists should refresh or requery.
1439 public static void refresh() {
1441 if (mService != null) {
1444 } catch (final RemoteException ignored) {
1449 * Called when one of playlists have changed
1451 public static void playlistChanged() {
1453 if (mService != null) {
1454 mService.playlistChanged();
1456 } catch (final RemoteException ignored) {
1461 * Seeks the current track to a desired position
1463 * @param position The position to seek to
1465 public static void seek(final long position) {
1466 if (mService != null) {
1468 mService.seek(position);
1469 } catch (final RemoteException ignored) {
1475 * Seeks the current track to a desired relative position. This can be used
1476 * to simulate fastforward and rewind
1478 * @param deltaInMs The delta in ms to seek from the current position
1480 public static void seekRelative(final long deltaInMs) {
1481 if (mService != null) {
1483 mService.seekRelative(deltaInMs);
1484 } catch (final RemoteException ignored) {
1485 } catch (final IllegalStateException ignored) {
1486 // Illegal State Exception message is empty so logging will actually throw an
1487 // exception. We should come back and figure out why we get an exception in the
1488 // first place and make sure we understand it completely. I will use
1489 // https://cyanogen.atlassian.net/browse/MUSIC-125 to track investigating this more
1495 * @return The current position time of the track
1497 public static final long position() {
1498 if (mService != null) {
1500 return mService.position();
1501 } catch (final RemoteException ignored) {
1502 } catch (final IllegalStateException ex) {
1503 // Illegal State Exception message is empty so logging will actually throw an
1504 // exception. We should come back and figure out why we get an exception in the
1505 // first place and make sure we understand it completely. I will use
1506 // https://cyanogen.atlassian.net/browse/MUSIC-125 to track investigating this more
1513 * @return The total length of the current track
1515 public static final long duration() {
1516 if (mService != null) {
1518 return mService.duration();
1519 } catch (final RemoteException ignored) {
1520 } catch (final IllegalStateException ignored) {
1521 // Illegal State Exception message is empty so logging will actually throw an
1522 // exception. We should come back and figure out why we get an exception in the
1523 // first place and make sure we understand it completely. I will use
1524 // https://cyanogen.atlassian.net/browse/MUSIC-125 to track investigating this more
1531 * @param position The position to move the queue to
1533 public static void setQueuePosition(final int position) {
1534 if (mService != null) {
1536 mService.setQueuePosition(position);
1537 } catch (final RemoteException ignored) {
1545 public static void clearQueue() {
1547 mService.removeTracks(0, Integer.MAX_VALUE);
1548 } catch (final RemoteException ignored) {
1553 * Used to build and show a notification when Apollo is sent into the
1556 * @param context The {@link Context} to use.
1558 public static void notifyForegroundStateChanged(final Context context, boolean inForeground) {
1559 int old = sForegroundActivities;
1561 sForegroundActivities++;
1563 sForegroundActivities--;
1566 if (old == 0 || sForegroundActivities == 0) {
1567 final Intent intent = new Intent(context, MusicPlaybackService.class);
1568 intent.setAction(MusicPlaybackService.FOREGROUND_STATE_CHANGED);
1569 intent.putExtra(MusicPlaybackService.NOW_IN_FOREGROUND, sForegroundActivities != 0);
1570 context.startService(intent);
1575 * Perminately deletes item(s) from the user's device
1577 * @param context The {@link Context} to use.
1578 * @param list The item(s) to delete.
1580 public static void deleteTracks(final Context context, final long[] list) {
1581 final String[] projection = new String[] {
1582 BaseColumns._ID, MediaColumns.DATA, AudioColumns.ALBUM_ID
1584 final StringBuilder selection = new StringBuilder();
1585 selection.append(BaseColumns._ID + " IN (");
1586 for (int i = 0; i < list.length; i++) {
1587 selection.append(list[i]);
1588 if (i < list.length - 1) {
1589 selection.append(",");
1592 selection.append(")");
1593 final Cursor c = context.getContentResolver().query(
1594 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection.toString(),
1597 // Step 1: Remove selected tracks from the current playlist, as well
1598 // as from the album art cache
1600 while (!c.isAfterLast()) {
1601 // Remove from current playlist
1602 final long id = c.getLong(0);
1604 // Remove the track from the play count
1605 SongPlayCount.getInstance(context).removeItem(id);
1606 // Remove any items in the recents database
1607 RecentStore.getInstance(context).removeItem(id);
1611 // Step 2: Remove selected tracks from the database
1612 context.getContentResolver().delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1613 selection.toString(), null);
1615 // Step 3: Remove files from card
1617 while (!c.isAfterLast()) {
1618 final String name = c.getString(1);
1619 final File f = new File(name);
1620 try { // File.delete can throw a security exception
1622 // I'm not sure if we'd ever get here (deletion would
1623 // have to fail, but no exception thrown)
1624 Log.e("MusicUtils", "Failed to delete file " + name);
1627 } catch (final SecurityException ex) {
1634 final String message = makeLabel(context, R.plurals.NNNtracksdeleted, list.length);
1636 CustomToast.makeText((Activity)context, message, CustomToast.LENGTH_SHORT).show();
1637 // We deleted a number of tracks, which could affect any number of
1639 // in the media content domain, so update everything.
1640 context.getContentResolver().notifyChange(Uri.parse("content://media"), null);
1641 // Notify the lists to update
1646 * Simple function used to determine if the song/album year is invalid
1647 * @param year value to test
1648 * @return true if the app considers it valid
1650 public static boolean isInvalidYear(int year) {
1651 return year < MIN_VALID_YEAR;
1655 * A snippet is taken from MediaStore.Audio.keyFor method
1656 * This will take a name, removes things like "the", "an", etc
1657 * as well as special characters and return it
1658 * @param name the string to trim
1659 * @return the trimmed name
1661 public static String getTrimmedName(String name) {
1662 if (name == null || name.length() == 0) {
1666 name = name.trim().toLowerCase();
1667 if (name.startsWith("the ")) {
1668 name = name.substring(4);
1670 if (name.startsWith("an ")) {
1671 name = name.substring(3);
1673 if (name.startsWith("a ")) {
1674 name = name.substring(2);
1676 if (name.endsWith(", the") || name.endsWith(",the") ||
1677 name.endsWith(", an") || name.endsWith(",an") ||
1678 name.endsWith(", a") || name.endsWith(",a")) {
1679 name = name.substring(0, name.lastIndexOf(','));
1681 name = name.replaceAll("[\\[\\]\\(\\)\"'.,?!]", "").trim();
1687 * A snippet is taken from MediaStore.Audio.keyFor method
1688 * This will take a name, removes things like "the", "an", etc
1689 * as well as special characters, then find the localized label
1690 * @param name Name to get the label of
1691 * @return the localized label of the bucket that the name falls into
1693 public static String getLocalizedBucketLetter(String name) {
1694 if (name == null || name.length() == 0) {
1698 name = getTrimmedName(name);
1700 if (name.length() > 0) {
1701 return LocaleUtils.getInstance().getLabel(name);
1707 /** @return true if a string is null, empty, or contains only whitespace */
1708 public static boolean isBlank(String s) {
1709 if(s == null) { return true; }
1710 if(s.isEmpty()) { return true; }
1711 for(int i = 0; i < s.length(); i++) {
1712 char c = s.charAt(i);
1713 if(!Character.isWhitespace(c)) { return false; }
1719 * Removes the header image from the cache.
1721 public static void removeFromCache(Activity activity, String key) {
1722 ImageFetcher imageFetcher = ApolloUtils.getImageFetcher(activity);
1723 imageFetcher.removeFromCache(key);
1724 // Give the disk cache a little time before requesting a new image.
1725 SystemClock.sleep(80);
1729 * Removes image from cache so that the stock image is retrieved on reload
1731 public static void selectOldPhoto(Activity activity, String key) {
1732 // First remove the old image
1733 removeFromCache(activity, key);
1734 MusicUtils.refresh();
1739 * @param sortOrder values are mostly derived from SortOrder.class or could also be any sql
1743 public static boolean isSortOrderDesending(String sortOrder) {
1744 return sortOrder.endsWith(" DESC");
1748 * Takes a collection of items and builds a comma-separated list of them
1749 * @param items collection of items
1750 * @return comma-separted list of items
1752 public static final <E> String buildCollectionAsString(Collection<E> items) {
1753 Iterator<E> iterator = items.iterator();
1754 StringBuilder str = new StringBuilder();
1755 if (iterator.hasNext()) {
1756 str.append(iterator.next());
1757 while (iterator.hasNext()) {
1759 str.append(iterator.next());
1763 return str.toString();