diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index cec4f813..5c15ab8d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -132,15 +132,12 @@
android:resource="@xml/music_player_widget_info" />
-
+ android:name=".ui.activities.PlaylistDetailActivity"
+ android:label="@string/title_activity_smart_playlist_detail" />
diff --git a/app/src/main/java/com/kabouzeid/gramophone/lastfm/album/LastFMAlbumImageUrlLoader.java b/app/src/main/java/com/kabouzeid/gramophone/lastfm/album/LastFMAlbumImageUrlLoader.java
index a2c01428..cffc39f1 100644
--- a/app/src/main/java/com/kabouzeid/gramophone/lastfm/album/LastFMAlbumImageUrlLoader.java
+++ b/app/src/main/java/com/kabouzeid/gramophone/lastfm/album/LastFMAlbumImageUrlLoader.java
@@ -18,7 +18,7 @@ public class LastFMAlbumImageUrlLoader {
public static void loadAlbumImageUrl(Context context, String queryAlbum, String queryArtist, final AlbumImageUrlLoaderCallback callback) {
if (queryAlbum != null) {
- String albumJSON = AlbumJSONStore.getInstance(context).getAlbumJSON(queryAlbum + queryArtist);
+ String albumJSON = AlbumJSONStore.getInstance(context).getJSONData(queryAlbum + queryArtist);
if (albumJSON != null) {
try {
loadAlbumImageUrlFromJSON(new JSONObject(albumJSON), callback);
diff --git a/app/src/main/java/com/kabouzeid/gramophone/lastfm/album/LastFMAlbumInfoUtil.java b/app/src/main/java/com/kabouzeid/gramophone/lastfm/album/LastFMAlbumInfoUtil.java
index 32a05d33..cc771fb6 100644
--- a/app/src/main/java/com/kabouzeid/gramophone/lastfm/album/LastFMAlbumInfoUtil.java
+++ b/app/src/main/java/com/kabouzeid/gramophone/lastfm/album/LastFMAlbumInfoUtil.java
@@ -45,7 +45,7 @@ public class LastFMAlbumInfoUtil {
try {
return rootJSON.getJSONObject("album").getString("name");
} catch (JSONException e) {
- //Log.e(TAG, "Error while getting album name from JSON parameter!", e);
+ //Log.e(TAG, "Error while getting album name from JSON_DATA parameter!", e);
return "";
}
}
@@ -60,7 +60,7 @@ public class LastFMAlbumInfoUtil {
}
return images.getJSONObject(0).getString("#text");
} catch (JSONException | NullPointerException e) {
- //Log.e(TAG, "Error while getting album thumbnail image from JSON parameter!", e);
+ //Log.e(TAG, "Error while getting album thumbnail image from JSON_DATA parameter!", e);
return "";
}
}
@@ -69,7 +69,7 @@ public class LastFMAlbumInfoUtil {
try {
return rootJSON.getJSONObject("album").getJSONArray("image");
} catch (JSONException e) {
- //Log.e(TAG, "Error while getting album image array from JSON parameter!", e);
+ //Log.e(TAG, "Error while getting album image array from JSON_DATA parameter!", e);
return null;
}
}
@@ -79,7 +79,7 @@ public class LastFMAlbumInfoUtil {
JSONArray images = getAlbumImageArrayFromJSON(rootJSON);
return images.getJSONObject(images.length() - 1).getString("#text");
} catch (JSONException | NullPointerException e) {
- //Log.e(TAG, "Error while getting album image from JSON parameter!", e);
+ //Log.e(TAG, "Error while getting album image from JSON_DATA parameter!", e);
return "";
}
}
diff --git a/app/src/main/java/com/kabouzeid/gramophone/lastfm/artist/LastFMArtistInfoUtil.java b/app/src/main/java/com/kabouzeid/gramophone/lastfm/artist/LastFMArtistInfoUtil.java
index 0feee8f4..de8651a3 100644
--- a/app/src/main/java/com/kabouzeid/gramophone/lastfm/artist/LastFMArtistInfoUtil.java
+++ b/app/src/main/java/com/kabouzeid/gramophone/lastfm/artist/LastFMArtistInfoUtil.java
@@ -45,7 +45,7 @@ public class LastFMArtistInfoUtil {
try {
return rootJSON.getJSONObject("artist").getString("name");
} catch (JSONException e) {
- //Log.e(TAG, "Error while getting artist name from JSON parameter!", e);
+ //Log.e(TAG, "Error while getting artist name from JSON_DATA parameter!", e);
return "";
}
}
@@ -60,7 +60,7 @@ public class LastFMArtistInfoUtil {
}
return images.getJSONObject(0).getString("#text");
} catch (JSONException | NullPointerException e) {
- //Log.e(TAG, "Error while getting artist thumbnail image from JSON parameter!", e);
+ //Log.e(TAG, "Error while getting artist thumbnail image from JSON_DATA parameter!", e);
return "";
}
}
@@ -69,7 +69,7 @@ public class LastFMArtistInfoUtil {
try {
return rootJSON.getJSONObject("artist").getJSONArray("image");
} catch (JSONException e) {
- //Log.e(TAG, "Error while getting artist image array from JSON parameter!", e);
+ //Log.e(TAG, "Error while getting artist image array from JSON_DATA parameter!", e);
return null;
}
}
@@ -79,7 +79,7 @@ public class LastFMArtistInfoUtil {
JSONArray images = getArtistImageArrayFromJSON(rootJSON);
return images.getJSONObject(images.length() - 1).getString("#text");
} catch (JSONException | NullPointerException e) {
- //Log.e(TAG, "Error while getting artist image from JSON parameter!", e);
+ //Log.e(TAG, "Error while getting artist image from JSON_DATA parameter!", e);
return "";
}
}
@@ -88,13 +88,13 @@ public class LastFMArtistInfoUtil {
try {
return rootJSON.getJSONObject("artist").getJSONObject("bio").getString("content");
} catch (JSONException e) {
- //Log.e(TAG, "Error while getting artist biography from JSON parameter!", e);
+ //Log.e(TAG, "Error while getting artist biography from JSON_DATA parameter!", e);
return "";
}
}
public static void saveArtistJSONDataToCacheAndDisk(Context context, String artist, JSONObject jsonObject) {
- ArtistJSONStore.getInstance(context).removeItem(artist);
+ ArtistJSONStore.getInstance(context).removeArtistJSON(artist);
ArtistJSONStore.getInstance(context).addArtistJSON(artist, jsonObject.toString());
}
diff --git a/app/src/main/java/com/kabouzeid/gramophone/loader/SortedCursor.java b/app/src/main/java/com/kabouzeid/gramophone/loader/SortedCursor.java
new file mode 100644
index 00000000..88647710
--- /dev/null
+++ b/app/src/main/java/com/kabouzeid/gramophone/loader/SortedCursor.java
@@ -0,0 +1,186 @@
+/*
+* Copyright (C) 2014 The CyanogenMod Project
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+package com.kabouzeid.gramophone.loader;
+
+import android.database.AbstractCursor;
+import android.database.Cursor;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * This cursor basically wraps a song cursor and is given a list of the order of the ids of the
+ * contents of the cursor. It wraps the Cursor and simulates the internal cursor being sorted
+ * by moving the point to the appropriate spot
+ */
+public class SortedCursor extends AbstractCursor {
+ // cursor to wrap
+ private final Cursor mCursor;
+ // the map of external indices to internal indices
+ private ArrayList mOrderedPositions;
+ // this contains the ids that weren't found in the underlying cursor
+ private ArrayList mMissingIds;
+ // this contains the mapped cursor positions and afterwards the extra ids that weren't found
+ private HashMap mMapCursorPositions;
+ // extra we want to store with the cursor
+ private ArrayList mExtraData;
+
+ /**
+ * @param cursor to wrap
+ * @param order the list of unique ids in sorted order to display
+ * @param columnName the column name of the id to look up in the internal cursor
+ */
+ public SortedCursor(final Cursor cursor, final long[] order, final String columnName,
+ final List extends Object> extraData) {
+ if (cursor == null) {
+ throw new IllegalArgumentException("Non-null cursor is needed");
+ }
+
+ mCursor = cursor;
+ mMissingIds = buildCursorPositionMapping(order, columnName, extraData);
+ }
+
+ /**
+ * This function populates mOrderedPositions with the cursor positions in the order based
+ * on the order passed in
+ *
+ * @param order the target order of the internal cursor
+ * @param extraData Extra data we want to add to the cursor
+ * @return returns the ids that aren't found in the underlying cursor
+ */
+ private ArrayList buildCursorPositionMapping(final long[] order,
+ final String columnName, final List extends Object> extraData) {
+ ArrayList missingIds = new ArrayList();
+
+ mOrderedPositions = new ArrayList(mCursor.getCount());
+ mExtraData = new ArrayList();
+
+ mMapCursorPositions = new HashMap(mCursor.getCount());
+ final int idPosition = mCursor.getColumnIndex(columnName);
+
+ if (mCursor.moveToFirst()) {
+ // first figure out where each of the ids are in the cursor
+ do {
+ mMapCursorPositions.put(mCursor.getLong(idPosition), mCursor.getPosition());
+ } while (mCursor.moveToNext());
+
+ // now create the ordered positions to map to the internal cursor given the
+ // external sort order
+ for (int i = 0; order != null && i < order.length; i++) {
+ final long id = order[i];
+ if (mMapCursorPositions.containsKey(id)) {
+ mOrderedPositions.add(mMapCursorPositions.get(id));
+ mMapCursorPositions.remove(id);
+ if (extraData != null) {
+ mExtraData.add(extraData.get(i));
+ }
+ } else {
+ missingIds.add(id);
+ }
+ }
+
+ mCursor.moveToFirst();
+ }
+
+ return missingIds;
+ }
+
+ /**
+ * @return the list of ids that weren't found in the underlying cursor
+ */
+ public ArrayList getMissingIds() {
+ return mMissingIds;
+ }
+
+ /**
+ * @return the list of ids that were in the underlying cursor but not part of the ordered list
+ */
+ public Collection getExtraIds() {
+ return mMapCursorPositions.keySet();
+ }
+
+ /**
+ * @return the extra object data that was passed in to be attached to the current row
+ */
+ public Object getExtraData() {
+ int position = getPosition();
+ return position < mExtraData.size() ? mExtraData.get(position) : null;
+ }
+
+ @Override
+ public void close() {
+ mCursor.close();
+
+ super.close();
+ }
+
+ @Override
+ public int getCount() {
+ return mOrderedPositions.size();
+ }
+
+ @Override
+ public String[] getColumnNames() {
+ return mCursor.getColumnNames();
+ }
+
+ @Override
+ public String getString(int column) {
+ return mCursor.getString(column);
+ }
+
+ @Override
+ public short getShort(int column) {
+ return mCursor.getShort(column);
+ }
+
+ @Override
+ public int getInt(int column) {
+ return mCursor.getInt(column);
+ }
+
+ @Override
+ public long getLong(int column) {
+ return mCursor.getLong(column);
+ }
+
+ @Override
+ public float getFloat(int column) {
+ return mCursor.getFloat(column);
+ }
+
+ @Override
+ public double getDouble(int column) {
+ return mCursor.getDouble(column);
+ }
+
+ @Override
+ public boolean isNull(int column) {
+ return mCursor.isNull(column);
+ }
+
+ @Override
+ public boolean onMove(int oldPosition, int newPosition) {
+ if (newPosition >= 0 && newPosition < getCount()) {
+ mCursor.moveToPosition(mOrderedPositions.get(newPosition));
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/app/src/main/java/com/kabouzeid/gramophone/loader/TopAndRecentlyPlayedTracksLoader.java b/app/src/main/java/com/kabouzeid/gramophone/loader/TopAndRecentlyPlayedTracksLoader.java
new file mode 100644
index 00000000..811a4171
--- /dev/null
+++ b/app/src/main/java/com/kabouzeid/gramophone/loader/TopAndRecentlyPlayedTracksLoader.java
@@ -0,0 +1,133 @@
+/*
+* Copyright (C) 2014 The CyanogenMod Project
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+package com.kabouzeid.gramophone.loader;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.BaseColumns;
+
+import com.kabouzeid.gramophone.model.Song;
+import com.kabouzeid.gramophone.provider.RecentlyPlayedStore;
+import com.kabouzeid.gramophone.provider.SongPlayCountStore;
+
+import java.util.ArrayList;
+
+public class TopAndRecentlyPlayedTracksLoader {
+ public static final int NUMBER_OF_TOP_TRACKS = 99;
+
+ public static ArrayList getRecentlyPlayedTracks(Context context) {
+ return SongLoader.getSongs(makeRecentTracksCursorAndClearUpDatabase(context));
+ }
+
+ public static ArrayList getTopTracks(Context context) {
+ return SongLoader.getSongs(makeTopTracksCursorAndClearUpDatabase(context));
+ }
+
+ public static Cursor makeRecentTracksCursorAndClearUpDatabase(final Context context) {
+ SortedCursor retCursor = makeRecentTracksCursorImpl(context);
+
+ // clean up the databases with any ids not found
+ if (retCursor != null) {
+ ArrayList missingIds = retCursor.getMissingIds();
+ if (missingIds != null && missingIds.size() > 0) {
+ for (long id : missingIds) {
+ RecentlyPlayedStore.getInstance(context).removeSongId(id);
+ }
+ }
+ }
+ return retCursor;
+ }
+
+ public static Cursor makeTopTracksCursorAndClearUpDatabase(final Context context) {
+ SortedCursor retCursor = makeTopTracksCursorImpl(context);
+
+ // clean up the databases with any ids not found
+ if (retCursor != null) {
+ ArrayList missingIds = retCursor.getMissingIds();
+ if (missingIds != null && missingIds.size() > 0) {
+ for (long id : missingIds) {
+ SongPlayCountStore.getInstance(context).removeItem(id);
+ }
+ }
+ }
+ return retCursor;
+ }
+
+ private static SortedCursor makeRecentTracksCursorImpl(final Context context) {
+ // first get the top results ids from the internal database
+ Cursor songs = RecentlyPlayedStore.getInstance(context).queryRecentIds();
+
+ try {
+ return makeSortedCursor(context, songs,
+ songs.getColumnIndex(RecentlyPlayedStore.RecentStoreColumns.ID));
+ } finally {
+ if (songs != null) {
+ songs.close();
+ }
+ }
+ }
+
+ private static SortedCursor makeTopTracksCursorImpl(final Context context) {
+ // first get the top results ids from the internal database
+ Cursor songs = SongPlayCountStore.getInstance(context).getTopPlayedResults(NUMBER_OF_TOP_TRACKS);
+
+ try {
+ return makeSortedCursor(context, songs,
+ songs.getColumnIndex(SongPlayCountStore.SongPlayCountColumns.ID));
+ } finally {
+ if (songs != null) {
+ songs.close();
+ }
+ }
+ }
+
+ private static SortedCursor makeSortedCursor(final Context context, final Cursor cursor,
+ final int idColumn) {
+ if (cursor != null && cursor.moveToFirst()) {
+ // create the list of ids to select against
+ StringBuilder selection = new StringBuilder();
+ selection.append(BaseColumns._ID);
+ selection.append(" IN (");
+
+ // this tracks the order of the ids
+ long[] order = new long[cursor.getCount()];
+
+ long id = cursor.getLong(idColumn);
+ selection.append(id);
+ order[cursor.getPosition()] = id;
+
+ while (cursor.moveToNext()) {
+ selection.append(",");
+
+ id = cursor.getLong(idColumn);
+ order[cursor.getPosition()] = id;
+ selection.append(String.valueOf(id));
+ }
+
+ selection.append(")");
+
+ // get a list of songs with the data given the selection statement
+ Cursor songCursor = SongLoader.makeSongCursor(context, selection.toString(), null);
+ if (songCursor != null) {
+ // now return the wrapped TopTracksCursor to handle sorting given order
+ return new SortedCursor(songCursor, order, BaseColumns._ID, null);
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/app/src/main/java/com/kabouzeid/gramophone/model/smartplaylist/MyTopTracksPlaylist.java b/app/src/main/java/com/kabouzeid/gramophone/model/smartplaylist/MyTopTracksPlaylist.java
index beb167b5..86a770b9 100644
--- a/app/src/main/java/com/kabouzeid/gramophone/model/smartplaylist/MyTopTracksPlaylist.java
+++ b/app/src/main/java/com/kabouzeid/gramophone/model/smartplaylist/MyTopTracksPlaylist.java
@@ -4,12 +4,15 @@ import android.content.Context;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
+import com.kabouzeid.gramophone.App;
import com.kabouzeid.gramophone.R;
import com.kabouzeid.gramophone.adapter.songadapter.smartplaylist.CannotDeleteSingleSongsSongAdapter;
import com.kabouzeid.gramophone.adapter.songadapter.smartplaylist.SmartPlaylistSongAdapter;
import com.kabouzeid.gramophone.interfaces.CabHolder;
-import com.kabouzeid.gramophone.loader.LastAddedLoader;
+import com.kabouzeid.gramophone.loader.TopAndRecentlyPlayedTracksLoader;
+import com.kabouzeid.gramophone.model.DataBaseChangedEvent;
import com.kabouzeid.gramophone.model.Song;
+import com.kabouzeid.gramophone.provider.SongPlayCountStore;
import java.util.ArrayList;
@@ -24,8 +27,7 @@ public class MyTopTracksPlaylist extends SmartPlaylist {
@Override
public ArrayList getSongs(Context context) {
- // TODO replace with getSongs() for top tracks. This is just a place holder
- return LastAddedLoader.getLastAddedSongs(context);
+ return TopAndRecentlyPlayedTracksLoader.getTopTracks(context);
}
@Override
@@ -35,6 +37,7 @@ public class MyTopTracksPlaylist extends SmartPlaylist {
@Override
public void clear(Context context) {
- // TODO
+ SongPlayCountStore.getInstance(context).clear();
+ App.bus.post(new DataBaseChangedEvent(DataBaseChangedEvent.PLAYLISTS_CHANGED));
}
}
diff --git a/app/src/main/java/com/kabouzeid/gramophone/model/smartplaylist/RecentlyPlayedPlaylist.java b/app/src/main/java/com/kabouzeid/gramophone/model/smartplaylist/RecentlyPlayedPlaylist.java
index 3d908a23..1b59cdb1 100644
--- a/app/src/main/java/com/kabouzeid/gramophone/model/smartplaylist/RecentlyPlayedPlaylist.java
+++ b/app/src/main/java/com/kabouzeid/gramophone/model/smartplaylist/RecentlyPlayedPlaylist.java
@@ -4,12 +4,15 @@ import android.content.Context;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
+import com.kabouzeid.gramophone.App;
import com.kabouzeid.gramophone.R;
import com.kabouzeid.gramophone.adapter.songadapter.smartplaylist.CannotDeleteSingleSongsSongAdapter;
import com.kabouzeid.gramophone.adapter.songadapter.smartplaylist.SmartPlaylistSongAdapter;
import com.kabouzeid.gramophone.interfaces.CabHolder;
-import com.kabouzeid.gramophone.loader.LastAddedLoader;
+import com.kabouzeid.gramophone.loader.TopAndRecentlyPlayedTracksLoader;
+import com.kabouzeid.gramophone.model.DataBaseChangedEvent;
import com.kabouzeid.gramophone.model.Song;
+import com.kabouzeid.gramophone.provider.RecentlyPlayedStore;
import java.util.ArrayList;
@@ -24,8 +27,7 @@ public class RecentlyPlayedPlaylist extends SmartPlaylist {
@Override
public ArrayList getSongs(Context context) {
- // TODO replace with getSongs() for recently played. This is just a place holder
- return LastAddedLoader.getLastAddedSongs(context);
+ return TopAndRecentlyPlayedTracksLoader.getRecentlyPlayedTracks(context);
}
@Override
@@ -35,6 +37,7 @@ public class RecentlyPlayedPlaylist extends SmartPlaylist {
@Override
public void clear(Context context) {
- // TODO
+ RecentlyPlayedStore.getInstance(context).clear();
+ App.bus.post(new DataBaseChangedEvent(DataBaseChangedEvent.PLAYLISTS_CHANGED));
}
}
diff --git a/app/src/main/java/com/kabouzeid/gramophone/provider/AlbumJSONStore.java b/app/src/main/java/com/kabouzeid/gramophone/provider/AlbumJSONStore.java
index 6b08c117..116affdc 100644
--- a/app/src/main/java/com/kabouzeid/gramophone/provider/AlbumJSONStore.java
+++ b/app/src/main/java/com/kabouzeid/gramophone/provider/AlbumJSONStore.java
@@ -8,7 +8,7 @@ import android.database.sqlite.SQLiteOpenHelper;
public class AlbumJSONStore extends SQLiteOpenHelper {
- public static final String DATABASE_NAME = "albumJSONLastFM.db";
+ public static final String DATABASE_NAME = "albums_last_fm.db";
private static final int VERSION = 1;
private static AlbumJSONStore sInstance = null;
@@ -23,12 +23,8 @@ public class AlbumJSONStore extends SQLiteOpenHelper {
return sInstance;
}
- public static void deleteDatabase(final Context context) {
- context.deleteDatabase(DATABASE_NAME);
- }
-
- public void addAlbumJSON(final String albumAndArtistName, final String JSON) {
- if (albumAndArtistName == null || JSON == null) {
+ public void addAlbumJSON(final String albumAndArtistName, final String json) {
+ if (albumAndArtistName == null || json == null) {
return;
}
@@ -37,34 +33,34 @@ public class AlbumJSONStore extends SQLiteOpenHelper {
database.beginTransaction();
- values.put(AlbumJSONColumns.ALBUMANDARTIST_NAME, albumAndArtistName.trim().toLowerCase());
- values.put(AlbumJSONColumns.JSON, JSON);
+ values.put(AlbumJSONColumns.ALBUM_PLUS_ARTIST_NAME, albumAndArtistName.trim().toLowerCase());
+ values.put(AlbumJSONColumns.JSON_DATA, json);
database.insert(AlbumJSONColumns.NAME, null, values);
database.setTransactionSuccessful();
database.endTransaction();
}
- public String getAlbumJSON(final String albumAndArtistName) {
+ public String getJSONData(final String albumAndArtistName) {
if (albumAndArtistName == null) {
return null;
}
final SQLiteDatabase database = getReadableDatabase();
final String[] projection = new String[]{
- AlbumJSONColumns.JSON,
- AlbumJSONColumns.ALBUMANDARTIST_NAME
+ AlbumJSONColumns.JSON_DATA,
+ AlbumJSONColumns.ALBUM_PLUS_ARTIST_NAME
};
- final String selection = AlbumJSONColumns.ALBUMANDARTIST_NAME + "=?";
+ final String selection = AlbumJSONColumns.ALBUM_PLUS_ARTIST_NAME + "=?";
final String[] having = new String[]{
albumAndArtistName.trim().toLowerCase()
};
Cursor cursor = database.query(AlbumJSONColumns.NAME, projection, selection, having, null,
null, null, null);
if (cursor != null && cursor.moveToFirst()) {
- final String JSON = cursor.getString(cursor.getColumnIndexOrThrow(AlbumJSONColumns.JSON));
+ final String json = cursor.getString(cursor.getColumnIndexOrThrow(AlbumJSONColumns.JSON_DATA));
cursor.close();
- return JSON;
+ return json;
}
if (cursor != null) {
cursor.close();
@@ -72,25 +68,25 @@ public class AlbumJSONStore extends SQLiteOpenHelper {
return null;
}
- public void removeItem(final String albumAndArtistName) {
+ public void removeAlbumJSON(final String albumAndArtistName) {
final SQLiteDatabase database = getReadableDatabase();
- database.delete(AlbumJSONColumns.NAME, AlbumJSONColumns.ALBUMANDARTIST_NAME + " = ?", new String[]{
+ database.delete(AlbumJSONColumns.NAME, AlbumJSONColumns.ALBUM_PLUS_ARTIST_NAME + " = ?", new String[]{
albumAndArtistName.trim().toLowerCase()
});
}
public interface AlbumJSONColumns {
- String NAME = "AlbumJSON";
- String ALBUMANDARTIST_NAME = "AlbumAndArtistName";
- String JSON = "JSON";
+ String NAME = "album_json";
+ String ALBUM_PLUS_ARTIST_NAME = "album_plus_artist_name";
+ String JSON_DATA = "json_data";
}
@Override
public void onCreate(final SQLiteDatabase db) {
db.execSQL("CREATE TABLE IF NOT EXISTS " + AlbumJSONColumns.NAME +
- " (" + AlbumJSONColumns.ALBUMANDARTIST_NAME + " TEXT NOT NULL," +
- AlbumJSONColumns.JSON + " TEXT NOT NULL);"
+ " (" + AlbumJSONColumns.ALBUM_PLUS_ARTIST_NAME + " TEXT NOT NULL," +
+ AlbumJSONColumns.JSON_DATA + " TEXT NOT NULL);"
);
}
diff --git a/app/src/main/java/com/kabouzeid/gramophone/provider/ArtistJSONStore.java b/app/src/main/java/com/kabouzeid/gramophone/provider/ArtistJSONStore.java
index 2d69c94d..86695710 100644
--- a/app/src/main/java/com/kabouzeid/gramophone/provider/ArtistJSONStore.java
+++ b/app/src/main/java/com/kabouzeid/gramophone/provider/ArtistJSONStore.java
@@ -8,7 +8,7 @@ import android.database.sqlite.SQLiteOpenHelper;
public class ArtistJSONStore extends SQLiteOpenHelper {
- public static final String DATABASE_NAME = "artistJSONLastFM.db";
+ public static final String DATABASE_NAME = "artists_last_fm.db";
private static final int VERSION = 1;
private static ArtistJSONStore sInstance = null;
@@ -23,12 +23,8 @@ public class ArtistJSONStore extends SQLiteOpenHelper {
return sInstance;
}
- public static void deleteDatabase(final Context context) {
- context.deleteDatabase(DATABASE_NAME);
- }
-
- public void addArtistJSON(final String artistName, final String JSON) {
- if (artistName == null || JSON == null) {
+ public void addArtistJSON(final String artistName, final String json) {
+ if (artistName == null || json == null) {
return;
}
@@ -38,7 +34,7 @@ public class ArtistJSONStore extends SQLiteOpenHelper {
database.beginTransaction();
values.put(ArtistJSONColumns.ARTIST_NAME, artistName.trim().toLowerCase());
- values.put(ArtistJSONColumns.JSON, JSON);
+ values.put(ArtistJSONColumns.JSON_DATA, json);
database.insert(ArtistJSONColumns.NAME, null, values);
database.setTransactionSuccessful();
@@ -52,7 +48,7 @@ public class ArtistJSONStore extends SQLiteOpenHelper {
final SQLiteDatabase database = getReadableDatabase();
final String[] projection = new String[]{
- ArtistJSONColumns.JSON,
+ ArtistJSONColumns.JSON_DATA,
ArtistJSONColumns.ARTIST_NAME
};
final String selection = ArtistJSONColumns.ARTIST_NAME + "=?";
@@ -62,9 +58,9 @@ public class ArtistJSONStore extends SQLiteOpenHelper {
Cursor cursor = database.query(ArtistJSONColumns.NAME, projection, selection, having, null,
null, null, null);
if (cursor != null && cursor.moveToFirst()) {
- final String JSON = cursor.getString(cursor.getColumnIndexOrThrow(ArtistJSONColumns.JSON));
+ final String json = cursor.getString(cursor.getColumnIndexOrThrow(ArtistJSONColumns.JSON_DATA));
cursor.close();
- return JSON;
+ return json;
}
if (cursor != null) {
cursor.close();
@@ -72,7 +68,7 @@ public class ArtistJSONStore extends SQLiteOpenHelper {
return null;
}
- public void removeItem(final String artistName) {
+ public void removeArtistJSON(final String artistName) {
final SQLiteDatabase database = getReadableDatabase();
database.delete(ArtistJSONColumns.NAME, ArtistJSONColumns.ARTIST_NAME + "=?", new String[]{
artistName.trim().toLowerCase()
@@ -81,16 +77,16 @@ public class ArtistJSONStore extends SQLiteOpenHelper {
}
public interface ArtistJSONColumns {
- String NAME = "ArtistJSON";
- String ARTIST_NAME = "ArtistName";
- String JSON = "JSON";
+ String NAME = "artist_json";
+ String ARTIST_NAME = "artist_name";
+ String JSON_DATA = "json_data";
}
@Override
public void onCreate(final SQLiteDatabase db) {
db.execSQL("CREATE TABLE IF NOT EXISTS " + ArtistJSONColumns.NAME +
" (" + ArtistJSONColumns.ARTIST_NAME + " TEXT NOT NULL," +
- ArtistJSONColumns.JSON + " TEXT NOT NULL);"
+ ArtistJSONColumns.JSON_DATA + " TEXT NOT NULL);"
);
}
@@ -100,6 +96,4 @@ public class ArtistJSONStore extends SQLiteOpenHelper {
db.execSQL("DROP TABLE IF EXISTS " + ArtistJSONColumns.NAME);
onCreate(db);
}
-
-
}
diff --git a/app/src/main/java/com/kabouzeid/gramophone/provider/RecentlyPlayedStore.java b/app/src/main/java/com/kabouzeid/gramophone/provider/RecentlyPlayedStore.java
new file mode 100644
index 00000000..e0a30fd6
--- /dev/null
+++ b/app/src/main/java/com/kabouzeid/gramophone/provider/RecentlyPlayedStore.java
@@ -0,0 +1,143 @@
+/*
+* Copyright (C) 2014 The CyanogenMod Project
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+package com.kabouzeid.gramophone.provider;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+public class RecentlyPlayedStore extends SQLiteOpenHelper {
+ private static final int MAX_ITEMS_IN_DB = 100;
+
+ public static final String DATABASE_NAME = "recently_played.db";
+ private static final int VERSION = 1;
+ private static RecentlyPlayedStore sInstance = null;
+
+ public RecentlyPlayedStore(final Context context) {
+ super(context, DATABASE_NAME, null, VERSION);
+ }
+
+ @Override
+ public void onCreate(final SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE IF NOT EXISTS " + RecentStoreColumns.NAME + " ("
+ + RecentStoreColumns.ID + " LONG NOT NULL," + RecentStoreColumns.TIME_PLAYED
+ + " LONG NOT NULL);");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ // nothing to do here yet
+ }
+
+ @Override
+ public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ db.execSQL("DROP TABLE IF EXISTS " + RecentStoreColumns.NAME);
+ onCreate(db);
+ }
+
+ public static synchronized RecentlyPlayedStore getInstance(final Context context) {
+ if (sInstance == null) {
+ sInstance = new RecentlyPlayedStore(context.getApplicationContext());
+ }
+ return sInstance;
+ }
+
+ public void addSongId(final long songId) {
+ final SQLiteDatabase database = getWritableDatabase();
+ database.beginTransaction();
+
+ try {
+ removeSongId(songId);
+
+ // add the entry
+ final ContentValues values = new ContentValues(2);
+ values.put(RecentStoreColumns.ID, songId);
+ values.put(RecentStoreColumns.TIME_PLAYED, System.currentTimeMillis());
+ database.insert(RecentStoreColumns.NAME, null, values);
+
+ // if our db is too large, delete the extra items
+ Cursor oldest = null;
+ try {
+ oldest = database.query(RecentStoreColumns.NAME,
+ new String[]{RecentStoreColumns.TIME_PLAYED}, null, null, null, null,
+ RecentStoreColumns.TIME_PLAYED + " ASC");
+
+ if (oldest != null && oldest.getCount() > MAX_ITEMS_IN_DB) {
+ oldest.moveToPosition(oldest.getCount() - MAX_ITEMS_IN_DB);
+ long timeOfRecordToKeep = oldest.getLong(0);
+
+ database.delete(RecentStoreColumns.NAME,
+ RecentStoreColumns.TIME_PLAYED + " < ?",
+ new String[]{String.valueOf(timeOfRecordToKeep)});
+
+ }
+ } finally {
+ if (oldest != null) {
+ oldest.close();
+ }
+ }
+ } finally {
+ database.setTransactionSuccessful();
+ database.endTransaction();
+ }
+ }
+
+ public void removeSongId(final long songId) {
+ final SQLiteDatabase database = getWritableDatabase();
+ database.delete(RecentStoreColumns.NAME, RecentStoreColumns.ID + " = ?", new String[]{
+ String.valueOf(songId)
+ });
+
+ }
+
+ public void clear() {
+ final SQLiteDatabase database = getWritableDatabase();
+ database.delete(RecentStoreColumns.NAME, null, null);
+ }
+
+ public boolean contains(long id) {
+ final SQLiteDatabase database = getReadableDatabase();
+ Cursor cursor = database.query(RecentStoreColumns.NAME,
+ new String[]{RecentStoreColumns.ID},
+ RecentStoreColumns.ID + "=?",
+ new String[]{String.valueOf(id)},
+ null, null, null, null);
+
+ boolean containsId = cursor != null && cursor.moveToFirst();
+ if (cursor != null) {
+ cursor.close();
+ }
+ return containsId;
+ }
+
+ public Cursor queryRecentIds() {
+ final SQLiteDatabase database = getReadableDatabase();
+ return database.query(RecentStoreColumns.NAME,
+ new String[]{RecentStoreColumns.ID}, null, null, null, null,
+ RecentStoreColumns.TIME_PLAYED + " DESC");
+ }
+
+ public interface RecentStoreColumns {
+ String NAME = "recent_history";
+
+ String ID = "song_id";
+
+ String TIME_PLAYED = "time_played";
+ }
+}
diff --git a/app/src/main/java/com/kabouzeid/gramophone/provider/SongPlayCountStore.java b/app/src/main/java/com/kabouzeid/gramophone/provider/SongPlayCountStore.java
new file mode 100644
index 00000000..d1225d62
--- /dev/null
+++ b/app/src/main/java/com/kabouzeid/gramophone/provider/SongPlayCountStore.java
@@ -0,0 +1,400 @@
+/*
+* Copyright (C) 2014 The CyanogenMod Project
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+package com.kabouzeid.gramophone.provider;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.Interpolator;
+
+/**
+ * This database tracks the number of play counts for an individual song. This is used to drive
+ * the top played tracks as well as the playlist images
+ */
+public class SongPlayCountStore extends SQLiteOpenHelper {
+ private static SongPlayCountStore sInstance = null;
+
+ public static final String DATABASE_NAME = "song_play_count.db";
+ private static final int VERSION = 1;
+
+ // interpolator curve applied for measuring the curve
+ private static Interpolator sInterpolator = new AccelerateInterpolator(1.5f);
+
+ // how many weeks worth of playback to track
+ private static final int NUM_WEEKS = 52;
+
+ // how high to multiply the interpolation curve
+ @SuppressWarnings("FieldCanBeLocal")
+ private static int INTERPOLATOR_HEIGHT = 50;
+
+ // how high the base value is. The ratio of the Height to Base is what really matters
+ @SuppressWarnings("FieldCanBeLocal")
+ private static int INTERPOLATOR_BASE = 25;
+
+ @SuppressWarnings("FieldCanBeLocal")
+ private static int ONE_WEEK_IN_MS = 1000 * 60 * 60 * 24 * 7;
+
+ private static String WHERE_ID_EQUALS = SongPlayCountColumns.ID + "=?";
+
+ // number of weeks since epoch time
+ private int mNumberOfWeeksSinceEpoch;
+
+ // used to track if we've walked through the db and updated all the rows
+ private boolean mDatabaseUpdated;
+
+ public SongPlayCountStore(final Context context) {
+ super(context, DATABASE_NAME, null, VERSION);
+
+ long msSinceEpoch = System.currentTimeMillis();
+ mNumberOfWeeksSinceEpoch = (int) (msSinceEpoch / ONE_WEEK_IN_MS);
+
+ mDatabaseUpdated = false;
+ }
+
+ @Override
+ public void onCreate(final SQLiteDatabase db) {
+ // create the play count table
+ // WARNING: If you change the order of these columns
+ // please update getColumnIndexForWeek
+ StringBuilder builder = new StringBuilder();
+ builder.append("CREATE TABLE IF NOT EXISTS ");
+ builder.append(SongPlayCountColumns.NAME);
+ builder.append("(");
+ builder.append(SongPlayCountColumns.ID);
+ builder.append(" INT UNIQUE,");
+
+ for (int i = 0; i < NUM_WEEKS; i++) {
+ builder.append(getColumnNameForWeek(i));
+ builder.append(" INT DEFAULT 0,");
+ }
+
+ builder.append(SongPlayCountColumns.LAST_UPDATED_WEEK_INDEX);
+ builder.append(" INT NOT NULL,");
+
+ builder.append(SongPlayCountColumns.PLAY_COUNT_SCORE);
+ builder.append(" REAL DEFAULT 0);");
+
+ db.execSQL(builder.toString());
+ }
+
+ @Override
+ public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
+ // No upgrade path needed yet
+ }
+
+ @Override
+ public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ // If we ever have downgrade, drop the table to be safe
+ db.execSQL("DROP TABLE IF EXISTS " + SongPlayCountColumns.NAME);
+ onCreate(db);
+ }
+
+ /**
+ * @param context The {@link Context} to use
+ * @return A new instance of this class.
+ */
+ public static synchronized SongPlayCountStore getInstance(final Context context) {
+ if (sInstance == null) {
+ sInstance = new SongPlayCountStore(context.getApplicationContext());
+ }
+ return sInstance;
+ }
+
+ /**
+ * Increases the play count of a song by 1
+ *
+ * @param songId The song id to increase the play count
+ */
+ public void bumpSongCount(final long songId) {
+ if (songId < 0) {
+ return;
+ }
+
+ final SQLiteDatabase database = getWritableDatabase();
+ updateExistingRow(database, songId, true);
+ }
+
+ /**
+ * This creates a new entry that indicates a song has been played once as well as its score
+ *
+ * @param database a write able database
+ * @param songId the id of the track
+ */
+ private void createNewPlayedEntry(final SQLiteDatabase database, final long songId) {
+ // no row exists, create a new one
+ float newScore = getScoreMultiplierForWeek(0);
+ int newPlayCount = 1;
+
+ final ContentValues values = new ContentValues(3);
+ values.put(SongPlayCountColumns.ID, songId);
+ values.put(SongPlayCountColumns.PLAY_COUNT_SCORE, newScore);
+ values.put(SongPlayCountColumns.LAST_UPDATED_WEEK_INDEX, mNumberOfWeeksSinceEpoch);
+ values.put(getColumnNameForWeek(0), newPlayCount);
+
+ database.insert(SongPlayCountColumns.NAME, null, values);
+ }
+
+ /**
+ * This function will take a song entry and update it to the latest week and increase the count
+ * for the current week by 1 if necessary
+ *
+ * @param database a writeable database
+ * @param id the id of the track to bump
+ * @param bumpCount whether to bump the current's week play count by 1 and adjust the score
+ */
+ private void updateExistingRow(final SQLiteDatabase database, final long id, boolean bumpCount) {
+ String stringId = String.valueOf(id);
+
+ // begin the transaction
+ database.beginTransaction();
+
+ // get the cursor of this content inside the transaction
+ final Cursor cursor = database.query(SongPlayCountColumns.NAME, null, WHERE_ID_EQUALS,
+ new String[]{stringId}, null, null, null);
+
+ // if we have a result
+ if (cursor != null && cursor.moveToFirst()) {
+ // figure how many weeks since we last updated
+ int lastUpdatedIndex = cursor.getColumnIndex(SongPlayCountColumns.LAST_UPDATED_WEEK_INDEX);
+ int lastUpdatedWeek = cursor.getInt(lastUpdatedIndex);
+ int weekDiff = mNumberOfWeeksSinceEpoch - lastUpdatedWeek;
+
+ // if it's more than the number of weeks we track, delete it and create a new entry
+ if (Math.abs(weekDiff) >= NUM_WEEKS) {
+ // this entry needs to be dropped since it is too outdated
+ deleteEntry(database, stringId);
+ if (bumpCount) {
+ createNewPlayedEntry(database, id);
+ }
+ } else if (weekDiff != 0) {
+ // else, shift the weeks
+ int[] playCounts = new int[NUM_WEEKS];
+
+ if (weekDiff > 0) {
+ // time is shifted forwards
+ for (int i = 0; i < NUM_WEEKS - weekDiff; i++) {
+ playCounts[i + weekDiff] = cursor.getInt(getColumnIndexForWeek(i));
+ }
+ } else if (weekDiff < 0) {
+ // time is shifted backwards (by user) - nor typical behavior but we
+ // will still handle it
+
+ // since weekDiff is -ve, NUM_WEEKS + weekDiff is the real # of weeks we have to
+ // transfer. Then we transfer the old week i - weekDiff to week i
+ // for example if the user shifted back 2 weeks, ie -2, then for 0 to
+ // NUM_WEEKS + (-2) we set the new week i = old week i - (-2) or i+2
+ for (int i = 0; i < NUM_WEEKS + weekDiff; i++) {
+ playCounts[i] = cursor.getInt(getColumnIndexForWeek(i - weekDiff));
+ }
+ }
+
+ // bump the count
+ if (bumpCount) {
+ playCounts[0]++;
+ }
+
+ float score = calculateScore(playCounts);
+
+ // if the score is non-existant, then delete it
+ if (score < .01f) {
+ deleteEntry(database, stringId);
+ } else {
+ // create the content values
+ ContentValues values = new ContentValues(NUM_WEEKS + 2);
+ values.put(SongPlayCountColumns.LAST_UPDATED_WEEK_INDEX, mNumberOfWeeksSinceEpoch);
+ values.put(SongPlayCountColumns.PLAY_COUNT_SCORE, score);
+
+ for (int i = 0; i < NUM_WEEKS; i++) {
+ values.put(getColumnNameForWeek(i), playCounts[i]);
+ }
+
+ // update the entry
+ database.update(SongPlayCountColumns.NAME, values, WHERE_ID_EQUALS,
+ new String[]{stringId});
+ }
+ } else if (bumpCount) {
+ // else no shifting, just update the scores
+ ContentValues values = new ContentValues(2);
+
+ // increase the score by a single score amount
+ int scoreIndex = cursor.getColumnIndex(SongPlayCountColumns.PLAY_COUNT_SCORE);
+ float score = cursor.getFloat(scoreIndex) + getScoreMultiplierForWeek(0);
+ values.put(SongPlayCountColumns.PLAY_COUNT_SCORE, score);
+
+ // increase the play count by 1
+ values.put(getColumnNameForWeek(0), cursor.getInt(getColumnIndexForWeek(0)) + 1);
+
+ // update the entry
+ database.update(SongPlayCountColumns.NAME, values, WHERE_ID_EQUALS,
+ new String[]{stringId});
+ }
+
+ cursor.close();
+ } else if (bumpCount) {
+ // if we have no existing results, create a new one
+ createNewPlayedEntry(database, id);
+ }
+
+ database.setTransactionSuccessful();
+ database.endTransaction();
+ }
+
+ public void clear() {
+ final SQLiteDatabase database = getWritableDatabase();
+ database.delete(SongPlayCountColumns.NAME, null, null);
+ }
+
+ /**
+ * Gets a cursor containing the top songs played. Note this only returns songs that have been
+ * played at least once in the past NUM_WEEKS
+ *
+ * @param numResults number of results to limit by. If <= 0 it returns all results
+ * @return the top tracks
+ */
+ public Cursor getTopPlayedResults(int numResults) {
+ updateResults();
+
+ final SQLiteDatabase database = getReadableDatabase();
+ return database.query(SongPlayCountColumns.NAME, new String[]{SongPlayCountColumns.ID},
+ null, null, null, null, SongPlayCountColumns.PLAY_COUNT_SCORE + " DESC",
+ (numResults <= 0 ? null : String.valueOf(numResults)));
+ }
+
+ /**
+ * This updates all the results for the getTopPlayedResults so that we can get an
+ * accurate list of the top played results
+ */
+ private synchronized void updateResults() {
+ if (mDatabaseUpdated) {
+ return;
+ }
+
+ final SQLiteDatabase database = getWritableDatabase();
+
+ database.beginTransaction();
+
+ int oldestWeekWeCareAbout = mNumberOfWeeksSinceEpoch - NUM_WEEKS + 1;
+ // delete rows we don't care about anymore
+ database.delete(SongPlayCountColumns.NAME, SongPlayCountColumns.LAST_UPDATED_WEEK_INDEX
+ + " < " + oldestWeekWeCareAbout, null);
+
+ // get the remaining rows
+ Cursor cursor = database.query(SongPlayCountColumns.NAME,
+ new String[]{SongPlayCountColumns.ID},
+ null, null, null, null, null);
+
+ if (cursor != null && cursor.moveToFirst()) {
+ // for each row, update it
+ do {
+ updateExistingRow(database, cursor.getLong(0), false);
+ } while (cursor.moveToNext());
+
+ cursor.close();
+ }
+
+ mDatabaseUpdated = true;
+ database.setTransactionSuccessful();
+ database.endTransaction();
+ }
+
+ /**
+ * @param songId The song Id to remove.
+ */
+ public void removeItem(final long songId) {
+ final SQLiteDatabase database = getWritableDatabase();
+ deleteEntry(database, String.valueOf(songId));
+ }
+
+ /**
+ * Deletes the entry
+ *
+ * @param database database to use
+ * @param stringId id to delete
+ */
+ private void deleteEntry(final SQLiteDatabase database, final String stringId) {
+ database.delete(SongPlayCountColumns.NAME, WHERE_ID_EQUALS, new String[]{stringId});
+ }
+
+ /**
+ * Calculates the score of the song given the play counts
+ *
+ * @param playCounts an array of the # of times a song has been played for each week
+ * where playCounts[N] is the # of times it was played N weeks ago
+ * @return the score
+ */
+ private static float calculateScore(final int[] playCounts) {
+ if (playCounts == null) {
+ return 0;
+ }
+
+ float score = 0;
+ for (int i = 0; i < Math.min(playCounts.length, NUM_WEEKS); i++) {
+ score += playCounts[i] * getScoreMultiplierForWeek(i);
+ }
+
+ return score;
+ }
+
+ /**
+ * Gets the column name for each week #
+ *
+ * @param week number
+ * @return the column name
+ */
+ private static String getColumnNameForWeek(final int week) {
+ return SongPlayCountColumns.WEEK_PLAY_COUNT + String.valueOf(week);
+ }
+
+ /**
+ * Gets the score multiplier for each week
+ *
+ * @param week number
+ * @return the multiplier to apply
+ */
+ private static float getScoreMultiplierForWeek(final int week) {
+ return sInterpolator.getInterpolation(1 - (week / (float) NUM_WEEKS)) * INTERPOLATOR_HEIGHT
+ + INTERPOLATOR_BASE;
+ }
+
+ /**
+ * For some performance gain, return a static value for the column index for a week
+ * WARNING: This function assumes you have selected all columns for it to work
+ *
+ * @param week number
+ * @return column index of that week
+ */
+ private static int getColumnIndexForWeek(final int week) {
+ // ID, followed by the weeks columns
+ return 1 + week;
+ }
+
+ public interface SongPlayCountColumns {
+
+ String NAME = "song_play_count";
+
+ String ID = "song_id";
+
+ String WEEK_PLAY_COUNT = "week";
+
+ String LAST_UPDATED_WEEK_INDEX = "week_index";
+
+ String PLAY_COUNT_SCORE = "play_count_score";
+ }
+}
diff --git a/app/src/main/java/com/kabouzeid/gramophone/service/MusicService.java b/app/src/main/java/com/kabouzeid/gramophone/service/MusicService.java
index b2b9b7b4..2122151d 100644
--- a/app/src/main/java/com/kabouzeid/gramophone/service/MusicService.java
+++ b/app/src/main/java/com/kabouzeid/gramophone/service/MusicService.java
@@ -33,6 +33,8 @@ import com.kabouzeid.gramophone.helper.PlayingNotificationHelper;
import com.kabouzeid.gramophone.helper.ShuffleHelper;
import com.kabouzeid.gramophone.misc.AppKeys;
import com.kabouzeid.gramophone.model.Song;
+import com.kabouzeid.gramophone.provider.RecentlyPlayedStore;
+import com.kabouzeid.gramophone.provider.SongPlayCountStore;
import com.kabouzeid.gramophone.util.InternalStorageUtil;
import com.kabouzeid.gramophone.util.MusicUtil;
import com.kabouzeid.gramophone.util.PreferenceUtils;
@@ -110,6 +112,8 @@ public class MusicService extends Service {
private MusicPlayerHandler playerHandler;
private boolean isFadingDown = false;
private HandlerThread handlerThread;
+ private RecentlyPlayedStore recentlyPlayedStore;
+ private SongPlayCountStore songPlayCountStore;
private final BroadcastReceiver becomingNoisyReceiver = new BroadcastReceiver() {
@Override
@@ -145,6 +149,9 @@ public class MusicService extends Service {
playingNotificationHelper = new PlayingNotificationHelper(this);
+ recentlyPlayedStore = RecentlyPlayedStore.getInstance(this);
+ songPlayCountStore = SongPlayCountStore.getInstance(this);
+
shuffleMode = PreferenceManager.getDefaultSharedPreferences(this).getInt(AppKeys.SP_SHUFFLE_MODE, 0);
repeatMode = PreferenceManager.getDefaultSharedPreferences(this).getInt(AppKeys.SP_REPEAT_MODE, 0);
@@ -543,7 +550,9 @@ public class MusicService extends Service {
this.originalPlayingQueue = restoredOriginalQueue;
this.playingQueue = restoredQueue;
- openTrackAndPrepareNextAt(restoredPosition);
+ setPosition(restoredPosition);
+ openCurrent();
+ prepareNext();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
@@ -808,6 +817,8 @@ public class MusicService extends Service {
updateNotification();
updateWidgets();
updateRemoteControlClient();
+ recentlyPlayedStore.addSongId(currentSong.id);
+ songPlayCountStore.bumpSongCount(currentSong.id);
}
}
diff --git a/app/src/main/java/com/kabouzeid/gramophone/ui/activities/AlbumDetailActivity.java b/app/src/main/java/com/kabouzeid/gramophone/ui/activities/AlbumDetailActivity.java
index bc5bf596..632781ed 100644
--- a/app/src/main/java/com/kabouzeid/gramophone/ui/activities/AlbumDetailActivity.java
+++ b/app/src/main/java/com/kabouzeid/gramophone/ui/activities/AlbumDetailActivity.java
@@ -438,6 +438,9 @@ public class AlbumDetailActivity extends AbsFabActivity implements PaletteColorH
@Override
public void onBackPressed() {
if (cab != null && cab.isActive()) cab.finish();
- else super.onBackPressed();
+ else {
+ recyclerView.stopScroll();
+ super.onBackPressed();
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/kabouzeid/gramophone/ui/activities/ArtistDetailActivity.java b/app/src/main/java/com/kabouzeid/gramophone/ui/activities/ArtistDetailActivity.java
index d1dd1ddd..69c2b86b 100644
--- a/app/src/main/java/com/kabouzeid/gramophone/ui/activities/ArtistDetailActivity.java
+++ b/app/src/main/java/com/kabouzeid/gramophone/ui/activities/ArtistDetailActivity.java
@@ -69,7 +69,7 @@ import butterknife.InjectView;
/**
* A lot of hackery is done in this activity. Changing things may will brake the whole activity.
- *
+ *
* Should be kinda stable ONLY AS IT IS!!!
*/
public class ArtistDetailActivity extends AbsFabActivity implements PaletteColorHolder, CabHolder {
@@ -423,6 +423,7 @@ public class ArtistDetailActivity extends AbsFabActivity implements PaletteColor
super.enableViews();
songListView.setEnabled(true);
toolbar.setEnabled(true);
+ albumRecyclerView.setEnabled(true);
}
@Override
@@ -430,6 +431,7 @@ public class ArtistDetailActivity extends AbsFabActivity implements PaletteColor
super.disableViews();
songListView.setEnabled(false);
toolbar.setEnabled(false);
+ albumRecyclerView.setEnabled(false);
}
@@ -524,6 +526,9 @@ public class ArtistDetailActivity extends AbsFabActivity implements PaletteColor
@Override
public void onBackPressed() {
if (cab != null && cab.isActive()) cab.finish();
- else super.onBackPressed();
+ else {
+ albumRecyclerView.stopScroll();
+ super.onBackPressed();
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/kabouzeid/gramophone/ui/activities/PlaylistDetailActivity.java b/app/src/main/java/com/kabouzeid/gramophone/ui/activities/PlaylistDetailActivity.java
index 18ec6842..08b65bdd 100644
--- a/app/src/main/java/com/kabouzeid/gramophone/ui/activities/PlaylistDetailActivity.java
+++ b/app/src/main/java/com/kabouzeid/gramophone/ui/activities/PlaylistDetailActivity.java
@@ -7,17 +7,21 @@ import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
+import android.widget.TextView;
import com.afollestad.materialcab.MaterialCab;
import com.kabouzeid.gramophone.App;
import com.kabouzeid.gramophone.R;
+import com.kabouzeid.gramophone.adapter.songadapter.AbsPlaylistSongAdapter;
import com.kabouzeid.gramophone.adapter.songadapter.PlaylistSongAdapter;
+import com.kabouzeid.gramophone.adapter.songadapter.smartplaylist.SmartPlaylistSongAdapter;
import com.kabouzeid.gramophone.interfaces.CabHolder;
import com.kabouzeid.gramophone.loader.PlaylistSongLoader;
import com.kabouzeid.gramophone.misc.DragSortRecycler;
import com.kabouzeid.gramophone.model.DataBaseChangedEvent;
import com.kabouzeid.gramophone.model.Playlist;
import com.kabouzeid.gramophone.model.PlaylistSong;
+import com.kabouzeid.gramophone.model.smartplaylist.SmartPlaylist;
import com.kabouzeid.gramophone.ui.activities.base.AbsFabActivity;
import com.kabouzeid.gramophone.util.NavigationUtil;
import com.kabouzeid.gramophone.util.PlaylistsUtil;
@@ -26,56 +30,40 @@ import com.squareup.otto.Subscribe;
import java.util.ArrayList;
+import butterknife.ButterKnife;
+import butterknife.InjectView;
+
public class PlaylistDetailActivity extends AbsFabActivity implements CabHolder {
public static final String TAG = PlaylistDetailActivity.class.getSimpleName();
public static String EXTRA_PLAYLIST = "extra_playlist";
+ @InjectView(R.id.recycler_view)
+ RecyclerView recyclerView;
+ @InjectView(R.id.toolbar)
+ Toolbar toolbar;
+ @InjectView(android.R.id.empty)
+ TextView empty;
+
private Playlist playlist;
private MaterialCab cab;
- private PlaylistSongAdapter adapter;
+ private AbsPlaylistSongAdapter adapter;
private ArrayList songs;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_playlist_detail);
+ ButterKnife.inject(this);
getIntentExtras();
- RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
- songs = PlaylistSongLoader.getPlaylistSongList(this, playlist.id);
- adapter = new PlaylistSongAdapter(this, songs, this);
- recyclerView.setLayoutManager(new GridLayoutManager(this, 1));
- recyclerView.setAdapter(adapter);
+ setUpRecyclerView();
- findViewById(android.R.id.empty).setVisibility(
- songs.size() == 0 ? View.VISIBLE : View.GONE
- );
+ checkIsEmpty();
- DragSortRecycler dragSortRecycler = new DragSortRecycler();
- dragSortRecycler.setViewHandleId(R.id.album_art);
-
- dragSortRecycler.setOnItemMovedListener(new DragSortRecycler.OnItemMovedListener() {
- @Override
- public void onItemMoved(int from, int to) {
- PlaylistSong song = songs.remove(from);
- songs.add(to, song);
- adapter.notifyDataSetChanged();
- PlaylistsUtil.moveItem(PlaylistDetailActivity.this, playlist.id, from, to);
- }
- });
-
- recyclerView.addItemDecoration(dragSortRecycler);
- recyclerView.addOnItemTouchListener(dragSortRecycler);
- recyclerView.setOnScrollListener(dragSortRecycler.getScrollListener());
-
- Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
- toolbar.setBackgroundColor(PreferenceUtils.getInstance(this).getThemeColorPrimary());
- setSupportActionBar(toolbar);
- getSupportActionBar().setTitle(playlist.name);
- getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ setUpToolBar();
if (PreferenceUtils.getInstance(this).coloredNavigationBarPlaylist())
setNavigationBarThemeColor();
@@ -84,6 +72,41 @@ public class PlaylistDetailActivity extends AbsFabActivity implements CabHolder
App.bus.register(this);
}
+ private void setUpRecyclerView() {
+ recyclerView.setLayoutManager(new GridLayoutManager(this, 1));
+ if (playlist instanceof SmartPlaylist) {
+ adapter = ((SmartPlaylist) playlist).createAdapter(this, this);
+ } else {
+ songs = PlaylistSongLoader.getPlaylistSongList(this, playlist.id);
+ adapter = new PlaylistSongAdapter(this, songs, this);
+
+ DragSortRecycler dragSortRecycler = new DragSortRecycler();
+ dragSortRecycler.setViewHandleId(R.id.album_art);
+ dragSortRecycler.setOnItemMovedListener(new DragSortRecycler.OnItemMovedListener() {
+ @Override
+ public void onItemMoved(int from, int to) {
+ PlaylistSong song = songs.remove(from);
+ songs.add(to, song);
+ adapter.notifyDataSetChanged();
+ PlaylistsUtil.moveItem(PlaylistDetailActivity.this, playlist.id, from, to);
+ }
+ });
+
+ recyclerView.addItemDecoration(dragSortRecycler);
+ recyclerView.addOnItemTouchListener(dragSortRecycler);
+ recyclerView.setOnScrollListener(dragSortRecycler.getScrollListener());
+ }
+ recyclerView.setAdapter(adapter);
+ }
+
+ private void setUpToolBar() {
+ toolbar.setBackgroundColor(getThemeColorPrimary());
+ setSupportActionBar(toolbar);
+ //noinspection ConstantConditions
+ getSupportActionBar().setTitle(playlist.name);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ }
+
private void getIntentExtras() {
Bundle intentExtras = getIntent().getExtras();
try {
@@ -114,7 +137,7 @@ public class PlaylistDetailActivity extends AbsFabActivity implements CabHolder
NavigationUtil.openEqualizer(this);
return true;
case android.R.id.home:
- super.onBackPressed();
+ onBackPressed();
return true;
case R.id.action_current_playing:
NavigationUtil.openCurrentPlayingIfPossible(this, getSharedViewsWithFab(null));
@@ -147,11 +170,14 @@ public class PlaylistDetailActivity extends AbsFabActivity implements CabHolder
switch (event.getAction()) {
case DataBaseChangedEvent.PLAYLISTS_CHANGED:
case DataBaseChangedEvent.DATABASE_CHANGED:
- songs = PlaylistSongLoader.getPlaylistSongList(this, playlist.id);
- adapter.updateDataSet(songs);
- findViewById(android.R.id.empty).setVisibility(
- songs.size() == 0 ? View.VISIBLE : View.GONE
- );
+ if (adapter instanceof SmartPlaylistSongAdapter) {
+ ((SmartPlaylistSongAdapter) adapter).updateDataSet();
+ } else {
+ songs = PlaylistSongLoader.getPlaylistSongList(this, playlist.id);
+ //noinspection unchecked
+ adapter.updateDataSet(songs);
+ }
+ checkIsEmpty();
break;
}
}
@@ -159,6 +185,15 @@ public class PlaylistDetailActivity extends AbsFabActivity implements CabHolder
@Override
public void onBackPressed() {
if (cab != null && cab.isActive()) cab.finish();
- else super.onBackPressed();
+ else {
+ recyclerView.stopScroll();
+ super.onBackPressed();
+ }
+ }
+
+ private void checkIsEmpty() {
+ empty.setVisibility(
+ adapter.getItemCount() == 0 ? View.VISIBLE : View.GONE
+ );
}
}
diff --git a/app/src/main/java/com/kabouzeid/gramophone/ui/activities/SmartPlaylistDetailActivity.java b/app/src/main/java/com/kabouzeid/gramophone/ui/activities/SmartPlaylistDetailActivity.java
deleted file mode 100644
index b24a5b42..00000000
--- a/app/src/main/java/com/kabouzeid/gramophone/ui/activities/SmartPlaylistDetailActivity.java
+++ /dev/null
@@ -1,158 +0,0 @@
-package com.kabouzeid.gramophone.ui.activities;
-
-import android.os.Bundle;
-import android.support.v7.widget.GridLayoutManager;
-import android.support.v7.widget.RecyclerView;
-import android.support.v7.widget.Toolbar;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.widget.TextView;
-
-import com.afollestad.materialcab.MaterialCab;
-import com.kabouzeid.gramophone.App;
-import com.kabouzeid.gramophone.R;
-import com.kabouzeid.gramophone.adapter.songadapter.smartplaylist.SmartPlaylistSongAdapter;
-import com.kabouzeid.gramophone.interfaces.CabHolder;
-import com.kabouzeid.gramophone.model.DataBaseChangedEvent;
-import com.kabouzeid.gramophone.model.smartplaylist.SmartPlaylist;
-import com.kabouzeid.gramophone.ui.activities.base.AbsFabActivity;
-import com.kabouzeid.gramophone.util.NavigationUtil;
-import com.kabouzeid.gramophone.util.PreferenceUtils;
-import com.squareup.otto.Subscribe;
-
-import butterknife.ButterKnife;
-import butterknife.InjectView;
-
-public class SmartPlaylistDetailActivity extends AbsFabActivity implements CabHolder {
-
- public static final String TAG = SmartPlaylistDetailActivity.class.getSimpleName();
-
- @InjectView(R.id.recycler_view)
- RecyclerView recyclerView;
- @InjectView(R.id.toolbar)
- Toolbar toolbar;
- @InjectView(android.R.id.empty)
- TextView empty;
-
- private SmartPlaylist playlist;
- private MaterialCab cab;
- private SmartPlaylistSongAdapter adapter;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_playlist_detail);
- ButterKnife.inject(this);
-
- getIntentExtras();
-
- setUpRecyclerView();
-
- checkIsEmpty();
-
- setUpToolBar();
-
- if (PreferenceUtils.getInstance(this).coloredNavigationBarPlaylist())
- setNavigationBarThemeColor();
- setStatusBarThemeColor();
-
- App.bus.register(this);
- }
-
- private void setUpRecyclerView() {
- adapter = playlist.createAdapter(this, this);
-
- recyclerView.setLayoutManager(new GridLayoutManager(this, 1));
- recyclerView.setAdapter(adapter);
- }
-
- private void setUpToolBar() {
- toolbar.setBackgroundColor(getThemeColorPrimary());
- setSupportActionBar(toolbar);
- //noinspection ConstantConditions
- getSupportActionBar().setTitle(playlist.name);
- getSupportActionBar().setDisplayHomeAsUpEnabled(true);
- }
-
- private void getIntentExtras() {
- Bundle intentExtras = getIntent().getExtras();
- try {
- playlist = (SmartPlaylist) intentExtras.getSerializable(PlaylistDetailActivity.EXTRA_PLAYLIST);
- } catch (ClassCastException ignored) {
- }
- if (playlist == null) {
- finish();
- }
- }
-
- @Override
- public String getTag() {
- return TAG;
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- getMenuInflater().inflate(R.menu.menu_playlist_detail, menu);
- return true;
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- int id = item.getItemId();
- switch (id) {
- case R.id.action_equalizer:
- NavigationUtil.openEqualizer(this);
- return true;
- case android.R.id.home:
- super.onBackPressed();
- return true;
- case R.id.action_current_playing:
- NavigationUtil.openCurrentPlayingIfPossible(this, getSharedViewsWithFab(null));
- return true;
- case R.id.action_playing_queue:
- NavigationUtil.openPlayingQueueDialog(this);
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-
- @Override
- public MaterialCab openCab(final int menu, final MaterialCab.Callback callback) {
- if (cab != null && cab.isActive()) cab.finish();
- cab = new MaterialCab(this, R.id.cab_stub)
- .setMenu(menu)
- .setBackgroundColor(PreferenceUtils.getInstance(this).getThemeColorPrimary())
- .start(callback);
- return cab;
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- App.bus.unregister(this);
- }
-
- @Subscribe
- public void onDataBaseEvent(DataBaseChangedEvent event) {
- switch (event.getAction()) {
- case DataBaseChangedEvent.PLAYLISTS_CHANGED:
- case DataBaseChangedEvent.DATABASE_CHANGED:
- adapter.updateDataSet();
- checkIsEmpty();
- break;
- }
- }
-
- @Override
- public void onBackPressed() {
- if (cab != null && cab.isActive()) cab.finish();
- else super.onBackPressed();
- }
-
- private void checkIsEmpty() {
- empty.setVisibility(
- adapter.getItemCount() == 0 ? View.VISIBLE : View.GONE
- );
- }
-}
diff --git a/app/src/main/java/com/kabouzeid/gramophone/ui/fragments/mainactivityfragments/AbsMainActivityRecyclerViewFragment.java b/app/src/main/java/com/kabouzeid/gramophone/ui/fragments/mainactivityfragments/AbsMainActivityRecyclerViewFragment.java
index f8606821..3c6ed09f 100644
--- a/app/src/main/java/com/kabouzeid/gramophone/ui/fragments/mainactivityfragments/AbsMainActivityRecyclerViewFragment.java
+++ b/app/src/main/java/com/kabouzeid/gramophone/ui/fragments/mainactivityfragments/AbsMainActivityRecyclerViewFragment.java
@@ -2,6 +2,7 @@ package com.kabouzeid.gramophone.ui.fragments.mainactivityfragments;
import android.os.Bundle;
import android.support.annotation.LayoutRes;
+import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.AppBarLayout.OnOffsetChangedListener;
@@ -29,9 +30,11 @@ public abstract class AbsMainActivityRecyclerViewFragment extends AbsMainActivit
@InjectView(R.id.recycler_view)
RecyclerView recyclerView;
+ @Nullable
@Optional
@InjectView(android.R.id.empty)
TextView empty;
+ @Nullable
@Optional
@InjectView(R.id.fast_scroller)
FastScroller fastScroller;
diff --git a/app/src/main/java/com/kabouzeid/gramophone/util/NavigationUtil.java b/app/src/main/java/com/kabouzeid/gramophone/util/NavigationUtil.java
index b229bdef..caa45b1f 100644
--- a/app/src/main/java/com/kabouzeid/gramophone/util/NavigationUtil.java
+++ b/app/src/main/java/com/kabouzeid/gramophone/util/NavigationUtil.java
@@ -16,12 +16,10 @@ import com.kabouzeid.gramophone.helper.MusicPlayerRemote;
import com.kabouzeid.gramophone.interfaces.KabViewsDisableAble;
import com.kabouzeid.gramophone.misc.AppKeys;
import com.kabouzeid.gramophone.model.Playlist;
-import com.kabouzeid.gramophone.model.smartplaylist.SmartPlaylist;
import com.kabouzeid.gramophone.ui.activities.AlbumDetailActivity;
import com.kabouzeid.gramophone.ui.activities.ArtistDetailActivity;
import com.kabouzeid.gramophone.ui.activities.MusicControllerActivity;
import com.kabouzeid.gramophone.ui.activities.PlaylistDetailActivity;
-import com.kabouzeid.gramophone.ui.activities.SmartPlaylistDetailActivity;
/**
* @author Karim Abou Zeid (kabouzeid)
@@ -72,11 +70,7 @@ public class NavigationUtil {
((KabViewsDisableAble) activity).disableViews();
final Intent intent;
- if (playlist instanceof SmartPlaylist) {
- intent = new Intent(activity, SmartPlaylistDetailActivity.class);
- } else {
- intent = new Intent(activity, PlaylistDetailActivity.class);
- }
+ intent = new Intent(activity, PlaylistDetailActivity.class);
intent.putExtra(PlaylistDetailActivity.EXTRA_PLAYLIST, playlist);
if (sharedViews != null) {