package gilday.android.powerhour.data;
import gilday.android.powerhour.data.PowerHour.NowPlaying;
import java.util.Collections;
import java.util.Random;
import java.util.Stack;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.os.AsyncTask;
import android.provider.MediaStore;
import android.util.Log;
/**
* Implements all the background leg work for initializing the Power Hour playlist from the
* Android MediaStore. Can initialize the playlist based on all the songs in the MediaStore
* or from a playlist saved in the MediaStore. Leaves the onProgressUpdate and onPostExecute
* unimplemented so that a subclass of this abstract class may define how progress is conveyed
* to the user.
* @author jgilday
*
*/
public abstract class InitializePlaylistTask extends AsyncTask<Void, Void, Integer> {
private Cursor importCursor;
private static final int QUICKLOAD_THRESHOLD = 180;
protected Context context;
protected int songsToImportCount = 0;
protected int reportInterval = 5;
/**
* Creates an InitializePlaylistTask which will create a new Power Hour playlist from
* all the songs stored in the Android MediaStore
* @param context Need a context to get the MediaStore content resolver
*/
public InitializePlaylistTask(Context context) {
this.context = context;
if(context == null) {
throw new IllegalArgumentException();
}
importCursor = context.getContentResolver().query(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
new String[] {
MediaStore.Audio.Media._ID,
MediaStore.Audio.Media.ARTIST,
MediaStore.Audio.Media.ALBUM,
MediaStore.Audio.Media.ALBUM_ID,
MediaStore.Audio.Media.TITLE
},
MediaStore.Audio.Media.IS_MUSIC + "=1",
null, null);
setSongsToImportCount();
}
/**
* Creates an InitializePlaylistTask which will create a new Power Hour playlist from
* a specific playlist in the Android MediaStore
* @param context Need a context to get the MediaStore content resolver
* @param playlistId The ID of the MediaStore.Audio.Playlists
*/
public InitializePlaylistTask(Context context, int playlistId) {
this.context = context;
if(context == null) {
throw new IllegalArgumentException();
}
final String[] ccols = new String[] {
MediaStore.Audio.Playlists.Members.AUDIO_ID,
MediaStore.Audio.Media.ARTIST,
MediaStore.Audio.Media.ALBUM,
MediaStore.Audio.Media.ALBUM_ID,
MediaStore.Audio.Media.TITLE };
importCursor = context.getContentResolver().query(
MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId),
ccols,
MediaStore.Audio.Media.IS_MUSIC + "=1",
null,
MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER);
setSongsToImportCount();
}
private void setSongsToImportCount()
{
if(importCursor == null) {
// The query has failed
throw new IllegalStateException("Could not find any songs");
}
songsToImportCount = importCursor.getCount();
if(songsToImportCount <= 0) {
throw new IllegalStateException("Cannot initialize a power hour with no songs to import");
}
PreferenceRepository prefsRepo = new PreferenceRepository(context);
if(songsToImportCount > (QUICKLOAD_THRESHOLD * 2) && prefsRepo.getQuickLoad())
{
songsToImportCount = QUICKLOAD_THRESHOLD;
}
}
/**
* Does all the leg work for loading in the Power Hour playlist from the Android
* MediaStore. Clears out the current playlist repository. Copies the Android playlist
* into the Power Hour playlist table. Copying the data seems redundant but the Power
* Hour playlist table has a unique schema with columns such as "omit".
*/
@Override
protected Integer doInBackground(Void... params) {
final int sourcePlaylistSize = importCursor.getCount();
if(importCursor == null || sourcePlaylistSize <= 0) {
throw new IllegalArgumentException("There are no songs in this playlist");
}
// Clear the now playing list since we are initializing a new one
context.getContentResolver().delete(NowPlaying.CONTENT_URI, null, null);
int i = 0;
final int sourceIdColumn = 0;
final int sourceArtistColumn = importCursor.getColumnIndex(MediaStore.Audio.Media.ARTIST);
final int sourceAlbumColumn = importCursor.getColumnIndex(MediaStore.Audio.Media.ALBUM);
final int sourceTitleColumn = importCursor.getColumnIndex(MediaStore.Audio.Media.TITLE);
boolean quickLoad = new PreferenceRepository(context).getQuickLoad() && importCursor.getCount() > (QUICKLOAD_THRESHOLD * 2);
// Build the playlist positions in shuffled order
// Will decide what shuffled order means up front instead of shuffling each item on the fly
// this allows users to see what the jumbled playlist looks like
Stack<Integer> shuffledPositions = getShuffledOrder();
// Will build a list of ContentValues to give to the ContentProvider's bulkInsert
ContentValues[] values = new ContentValues[songsToImportCount];
// Decide which implementation of ImportCursorIterator to use
ImportCursorIterator importCursorIterator = quickLoad
? new QuickLoadImportCursorIterator()
: new LoadAllImportCursorIterator();
// Set importCursor to first position
importCursorIterator.moveToInitialSongImport();
long now = System.currentTimeMillis();
while(i < songsToImportCount) {
// Import
int songId = importCursor.getInt(sourceIdColumn);
String artist = importCursor.getString(sourceArtistColumn);
String album = importCursor.getString(sourceAlbumColumn);
String title = importCursor.getString(sourceTitleColumn);
int omit = 0;
int played = 0;
int shufflePosition = shuffledPositions.pop();
ContentValues set = new ContentValues();
set.put(NowPlaying._ID, songId);
// Power Hour will not 0-index playlist positions so that we don't
// have to bump this number up one in the user interface. Optimization
// since this string to int and back conversion will happen a lot in a
// list view
set.put(NowPlaying.POSITION, i + 1);
set.put(NowPlaying.SHUFFLE_POSITION, shufflePosition + 1);
set.put(NowPlaying.ARTIST, artist);
set.put(NowPlaying.ALBUM, album);
set.put(NowPlaying.TITLE, title);
set.put(NowPlaying.OMIT, omit);
set.put(NowPlaying.PLAYED, played);
values[i] = set;
if(i % reportInterval == 0)
{
publishProgress();
}
// iterate
i++;
importCursorIterator.moveToNextSongImport(i);
}
long elapsed = System.currentTimeMillis() - now;
Log.d("DB IMPORT", "" + elapsed);
importCursor.close();
// Now write everything to the content provider with a bulk insert
int importedCount = context.getContentResolver().bulkInsert(NowPlaying.CONTENT_URI, values);
return importedCount;
}
/**
* This needs to return a list of random integers in [0,songsToImportCount] with
* no duplicates as fast as possible. These will be pop'd off with each imported song
* to form the playlist's shuffled order
* @return
*/
private Stack<Integer> getShuffledOrder() {
// Build a list of sequential integers [0, songsToImportCount]
Stack<Integer> listToShuffle = new Stack<Integer>();
for(int i = 0; i < songsToImportCount; i++) {
listToShuffle.push(i);
}
// Use Java's built in shuffle algorithm to shuffle list in linear time
Collections.shuffle(listToShuffle);
return listToShuffle;
}
private interface ImportCursorIterator
{
void moveToInitialSongImport();
void moveToNextSongImport(int iteration);
}
private class QuickLoadImportCursorIterator implements ImportCursorIterator
{
Random rand;
int bucketSize;
public QuickLoadImportCursorIterator() {
int importSize = importCursor.getCount();
bucketSize = importSize / QUICKLOAD_THRESHOLD;
rand = new Random();
}
public void moveToInitialSongImport() {
moveToRandom(0);
}
public void moveToNextSongImport(int iteration) {
moveToRandom(iteration);
}
private void moveToRandom(int iteration) {
// Move the cursor to the next source
int bucketIndex = rand.nextInt(bucketSize);
int sourcePosition = (iteration * bucketSize) + bucketIndex;
importCursor.moveToPosition(sourcePosition);
}
}
private class LoadAllImportCursorIterator implements ImportCursorIterator
{
public void moveToInitialSongImport() {
importCursor.moveToFirst();
}
public void moveToNextSongImport(int iteration) {
importCursor.moveToNext();
}
}
}