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 org.lineageos.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 org.lineageos.eleven.model.Song;
28 import org.lineageos.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 Id of the playlist the songs belong to.
50 private final long mPlaylistID;
53 * Constructor of <code>SongLoader</code>
55 * @param context The {@link Context} to use
56 * @param playlistId The Id of the playlist the songs belong to.
58 public PlaylistSongLoader(final Context context, final long playlistId) {
60 mPlaylistID = playlistId;
67 public List<Song> loadInBackground() {
68 final int playlistCount = countPlaylist(getContext(), mPlaylistID);
71 Cursor cursor = makePlaylistSongCursor(getContext(), mPlaylistID);
74 boolean runCleanup = false;
76 // if the raw playlist count differs from the mapped playlist count (ie the raw mapping
77 // table vs the mapping table join the audio table) that means the playlist mapping table
79 if (cursor.getCount() != playlistCount) {
80 Log.w(TAG, "Count Differs - raw is: " + playlistCount + " while cursor is " +
86 // check if the play order is already messed up by duplicates
87 if (!runCleanup && cursor.moveToFirst()) {
88 final int playOrderCol = cursor.getColumnIndexOrThrow(Playlists.Members.PLAY_ORDER);
90 int lastPlayOrder = -1;
92 int playOrder = cursor.getInt(playOrderCol);
93 // if we have duplicate play orders, we need to recreate the playlist
94 if (playOrder == lastPlayOrder) {
98 lastPlayOrder = playOrder;
99 } while (cursor.moveToNext());
103 Log.w(TAG, "Playlist order has flaws - recreating playlist");
105 // cleanup the playlist
106 cleanupPlaylist(getContext(), mPlaylistID, cursor);
108 // create a new cursor
110 cursor = makePlaylistSongCursor(getContext(), mPlaylistID);
111 if (cursor != null) {
112 Log.d(TAG, "New Count is: " + cursor.getCount());
118 if (cursor != null && cursor.moveToFirst()) {
121 final long id = cursor.getLong(cursor
122 .getColumnIndexOrThrow(MediaStore.Audio.Playlists.Members.AUDIO_ID));
124 // Copy the song name
125 final String songName = cursor.getString(cursor
126 .getColumnIndexOrThrow(AudioColumns.TITLE));
128 // Copy the artist name
129 final String artist = cursor.getString(cursor
130 .getColumnIndexOrThrow(AudioColumns.ARTIST));
133 final long albumId = cursor.getLong(cursor
134 .getColumnIndexOrThrow(AudioColumns.ALBUM_ID));
136 // Copy the album name
137 final String album = cursor.getString(cursor
138 .getColumnIndexOrThrow(AudioColumns.ALBUM));
141 final long duration = cursor.getLong(cursor
142 .getColumnIndexOrThrow(AudioColumns.DURATION));
144 // Convert the duration into seconds
145 final int durationInSecs = (int) duration / 1000;
147 // Grab the Song Year
148 final int year = cursor.getInt(cursor
149 .getColumnIndexOrThrow(AudioColumns.YEAR));
152 final Song song = new Song(id, songName, artist, album, albumId, durationInSecs, year);
156 } while (cursor.moveToNext());
159 if (cursor != null) {
167 * Cleans up the playlist based on the passed in cursor's data
168 * @param context The {@link Context} to use
169 * @param playlistId playlistId to clean up
170 * @param cursor data to repopulate the playlist with
172 private static void cleanupPlaylist(final Context context, final long playlistId,
173 final Cursor cursor) {
174 Log.w(TAG, "Cleaning up playlist: " + playlistId);
176 final int idCol = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.Members.AUDIO_ID);
177 final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
179 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
181 // Delete all results in the playlist
182 ops.add(ContentProviderOperation.newDelete(uri).build());
184 // yield the db every 100 records to prevent ANRs
185 final int YIELD_FREQUENCY = 100;
187 // for each item, reset the play order position
188 if (cursor.moveToFirst() && cursor.getCount() > 0) {
190 final ContentProviderOperation.Builder builder =
191 ContentProviderOperation.newInsert(uri)
192 .withValue(Playlists.Members.PLAY_ORDER, cursor.getPosition())
193 .withValue(Playlists.Members.AUDIO_ID, cursor.getLong(idCol));
195 // yield at the end and not at 0 by incrementing by 1
196 if ((cursor.getPosition() + 1) % YIELD_FREQUENCY == 0) {
197 builder.withYieldAllowed(true);
199 ops.add(builder.build());
200 } while (cursor.moveToNext());
204 // run the batch operation
205 context.getContentResolver().applyBatch(MediaStore.AUTHORITY, ops);
206 } catch (RemoteException e) {
207 Log.e(TAG, "RemoteException " + e + " while cleaning up playlist " + playlistId);
208 } catch (OperationApplicationException e) {
209 Log.e(TAG, "OperationApplicationException " + e + " while cleaning up playlist "
215 * Returns the playlist count for the raw playlist mapping table
216 * @param context The {@link Context} to use
217 * @param playlistId playlistId to count
218 * @return the number of tracks in the raw playlist mapping table
220 private static int countPlaylist(final Context context, final long playlistId) {
223 // when we query using only the audio_id column we will get the raw mapping table
224 // results - which will tell us if the table has rows that don't exist in the normal
226 c = context.getContentResolver().query(
227 MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId),
229 MediaStore.Audio.Playlists.Members.AUDIO_ID,
231 MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER);
247 * Creates the {@link Cursor} used to run the query.
249 * @param context The {@link Context} to use.
250 * @param playlistID The playlist the songs belong to.
251 * @return The {@link Cursor} used to run the song query.
253 public static final Cursor makePlaylistSongCursor(final Context context, final Long playlistID) {
254 String mSelection = (AudioColumns.IS_MUSIC + "=1") +
255 " AND " + AudioColumns.TITLE + " != ''";
256 return context.getContentResolver().query(
257 MediaStore.Audio.Playlists.Members.getContentUri("external", playlistID),
260 MediaStore.Audio.Playlists.Members._ID,
262 MediaStore.Audio.Playlists.Members.AUDIO_ID,
268 AudioColumns.ALBUM_ID,
272 AudioColumns.DURATION,
276 Playlists.Members.PLAY_ORDER,
278 MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER);