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.loaders;
16 import android.content.ContentProviderOperation;
17 import android.content.Context;
18 import android.content.OperationApplicationException;
19 import android.database.Cursor;
20 import android.net.Uri;
21 import android.os.RemoteException;
22 import android.provider.MediaStore;
23 import android.provider.MediaStore.Audio.AudioColumns;
24 import android.provider.MediaStore.Audio.Playlists;
25 import android.util.Log;
27 import com.cyanogenmod.eleven.model.Song;
28 import com.cyanogenmod.eleven.utils.Lists;
30 import java.util.ArrayList;
31 import java.util.List;
34 * Used to query {@link MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI} and
35 * return the songs for a particular playlist.
37 * @author Andrew Neal (andrewdneal@gmail.com)
39 public class PlaylistSongLoader extends WrappedAsyncTaskLoader<List<Song>> {
40 private static final String TAG = PlaylistSongLoader.class.getSimpleName();
45 private final ArrayList<Song> mSongList = Lists.newArrayList();
48 * The {@link Cursor} used to run the query.
50 private Cursor mCursor;
53 * The Id of the playlist the songs belong to.
55 private final long mPlaylistID;
58 * Constructor of <code>SongLoader</code>
60 * @param context The {@link Context} to use
61 * @param playlistId The Id of the playlist the songs belong to.
63 public PlaylistSongLoader(final Context context, final long playlistId) {
65 mPlaylistID = playlistId;
72 public List<Song> loadInBackground() {
73 final int playlistCount = countPlaylist(getContext(), mPlaylistID);
76 mCursor = makePlaylistSongCursor(getContext(), mPlaylistID);
78 if (mCursor != null) {
79 boolean runCleanup = false;
81 // if the raw playlist count differs from the mapped playlist count (ie the raw mapping
82 // table vs the mapping table join the audio table) that means the playlist mapping table
84 if (mCursor.getCount() != playlistCount) {
85 Log.w(TAG, "Count Differs - raw is: " + playlistCount + " while cursor is " +
91 // check if the play order is already messed up by duplicates
92 if (!runCleanup && mCursor.moveToFirst()) {
93 final int playOrderCol = mCursor.getColumnIndexOrThrow(Playlists.Members.PLAY_ORDER);
95 int lastPlayOrder = -1;
97 int playOrder = mCursor.getInt(playOrderCol);
98 // if we have duplicate play orders, we need to recreate the playlist
99 if (playOrder == lastPlayOrder) {
103 lastPlayOrder = playOrder;
104 } while (mCursor.moveToNext());
108 Log.w(TAG, "Playlist order has flaws - recreating playlist");
110 // cleanup the playlist
111 cleanupPlaylist(getContext(), mPlaylistID, mCursor);
113 // create a new cursor
115 mCursor = makePlaylistSongCursor(getContext(), mPlaylistID);
116 if (mCursor != null) {
117 Log.d(TAG, "New Count is: " + mCursor.getCount());
123 if (mCursor != null && mCursor.moveToFirst()) {
126 final long id = mCursor.getLong(mCursor
127 .getColumnIndexOrThrow(MediaStore.Audio.Playlists.Members.AUDIO_ID));
129 // Copy the song name
130 final String songName = mCursor.getString(mCursor
131 .getColumnIndexOrThrow(AudioColumns.TITLE));
133 // Copy the artist name
134 final String artist = mCursor.getString(mCursor
135 .getColumnIndexOrThrow(AudioColumns.ARTIST));
138 final long albumId = mCursor.getLong(mCursor
139 .getColumnIndexOrThrow(AudioColumns.ALBUM_ID));
141 // Copy the album name
142 final String album = mCursor.getString(mCursor
143 .getColumnIndexOrThrow(AudioColumns.ALBUM));
146 final long duration = mCursor.getLong(mCursor
147 .getColumnIndexOrThrow(AudioColumns.DURATION));
149 // Convert the duration into seconds
150 final int durationInSecs = (int) duration / 1000;
152 // Grab the Song Year
153 final int year = mCursor.getInt(mCursor
154 .getColumnIndexOrThrow(AudioColumns.YEAR));
157 final Song song = new Song(id, songName, artist, album, albumId, durationInSecs, year);
161 } while (mCursor.moveToNext());
164 if (mCursor != null) {
172 * Cleans up the playlist based on the passed in cursor's data
173 * @param context The {@link Context} to use
174 * @param playlistId playlistId to clean up
175 * @param cursor data to repopulate the playlist with
177 private static void cleanupPlaylist(final Context context, final long playlistId,
178 final Cursor cursor) {
179 Log.w(TAG, "Cleaning up playlist: " + playlistId);
181 final int idCol = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.Members.AUDIO_ID);
182 final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
184 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
186 // Delete all results in the playlist
187 ops.add(ContentProviderOperation.newDelete(uri).build());
189 // yield the db every 100 records to prevent ANRs
190 final int YIELD_FREQUENCY = 100;
192 // for each item, reset the play order position
193 if (cursor.moveToFirst() && cursor.getCount() > 0) {
195 final ContentProviderOperation.Builder builder =
196 ContentProviderOperation.newInsert(uri)
197 .withValue(Playlists.Members.PLAY_ORDER, cursor.getPosition())
198 .withValue(Playlists.Members.AUDIO_ID, cursor.getLong(idCol));
200 // yield at the end and not at 0 by incrementing by 1
201 if ((cursor.getPosition() + 1) % YIELD_FREQUENCY == 0) {
202 builder.withYieldAllowed(true);
204 ops.add(builder.build());
205 } while (cursor.moveToNext());
209 // run the batch operation
210 context.getContentResolver().applyBatch(MediaStore.AUTHORITY, ops);
211 } catch (RemoteException e) {
212 Log.e(TAG, "RemoteException " + e + " while cleaning up playlist " + playlistId);
213 } catch (OperationApplicationException e) {
214 Log.e(TAG, "OperationApplicationException " + e + " while cleaning up playlist "
220 * Returns the playlist count for the raw playlist mapping table
221 * @param context The {@link Context} to use
222 * @param playlistId playlistId to count
223 * @return the number of tracks in the raw playlist mapping table
225 private static int countPlaylist(final Context context, final long playlistId) {
228 // when we query using only the audio_id column we will get the raw mapping table
229 // results - which will tell us if the table has rows that don't exist in the normal
231 c = context.getContentResolver().query(
232 MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId),
234 MediaStore.Audio.Playlists.Members.AUDIO_ID,
236 MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER);
252 * Creates the {@link Cursor} used to run the query.
254 * @param context The {@link Context} to use.
255 * @param playlistID The playlist the songs belong to.
256 * @return The {@link Cursor} used to run the song query.
258 public static final Cursor makePlaylistSongCursor(final Context context, final Long playlistID) {
259 String mSelection = (AudioColumns.IS_MUSIC + "=1") +
260 " AND " + AudioColumns.TITLE + " != ''";
261 return context.getContentResolver().query(
262 MediaStore.Audio.Playlists.Members.getContentUri("external", playlistID),
265 MediaStore.Audio.Playlists.Members._ID,
267 MediaStore.Audio.Playlists.Members.AUDIO_ID,
273 AudioColumns.ALBUM_ID,
277 AudioColumns.DURATION,
281 Playlists.Members.PLAY_ORDER,
283 MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER);