From 50f73c1dde9a3f506131881eaadd7e0401cabab6 Mon Sep 17 00:00:00 2001 From: Karim Abou Zeid Date: Sun, 21 Jun 2015 21:17:22 +0200 Subject: [PATCH] All smart playlists are working now. --- app/src/main/AndroidManifest.xml | 7 +- .../album/LastFMAlbumImageUrlLoader.java | 2 +- .../lastfm/album/LastFMAlbumInfoUtil.java | 8 +- .../lastfm/artist/LastFMArtistInfoUtil.java | 12 +- .../gramophone/loader/SortedCursor.java | 186 ++++++++ .../TopAndRecentlyPlayedTracksLoader.java | 133 ++++++ .../smartplaylist/MyTopTracksPlaylist.java | 11 +- .../smartplaylist/RecentlyPlayedPlaylist.java | 11 +- .../gramophone/provider/AlbumJSONStore.java | 40 +- .../gramophone/provider/ArtistJSONStore.java | 30 +- .../provider/RecentlyPlayedStore.java | 143 +++++++ .../provider/SongPlayCountStore.java | 400 ++++++++++++++++++ .../gramophone/service/MusicService.java | 13 +- .../ui/activities/AlbumDetailActivity.java | 5 +- .../ui/activities/ArtistDetailActivity.java | 9 +- .../ui/activities/PlaylistDetailActivity.java | 111 +++-- .../SmartPlaylistDetailActivity.java | 158 ------- .../AbsMainActivityRecyclerViewFragment.java | 3 + .../gramophone/util/NavigationUtil.java | 8 +- 19 files changed, 1019 insertions(+), 271 deletions(-) create mode 100644 app/src/main/java/com/kabouzeid/gramophone/loader/SortedCursor.java create mode 100644 app/src/main/java/com/kabouzeid/gramophone/loader/TopAndRecentlyPlayedTracksLoader.java create mode 100644 app/src/main/java/com/kabouzeid/gramophone/provider/RecentlyPlayedStore.java create mode 100644 app/src/main/java/com/kabouzeid/gramophone/provider/SongPlayCountStore.java delete mode 100644 app/src/main/java/com/kabouzeid/gramophone/ui/activities/SmartPlaylistDetailActivity.java 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 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 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) {