From 44c1bb63b6ab44832f813179b23dd35e843d8a85 Mon Sep 17 00:00:00 2001 From: dkanada Date: Sun, 8 Nov 2020 13:12:19 +0900 Subject: [PATCH] replace queue store with room database --- app/build.gradle | 3 + .../main/java/com/dkanada/gramophone/App.java | 15 ++ .../gramophone/database/JellyDatabase.java | 18 ++ .../gramophone/database/QueueSong.java | 34 ++++ .../gramophone/database/QueueSongDao.java | 45 +++++ .../dkanada/gramophone/database/SongDao.java | 22 ++ .../dkanada/gramophone/loader/SongLoader.java | 112 ----------- .../com/dkanada/gramophone/model/Song.java | 23 +-- .../gramophone/provider/QueueStore.java | 190 ------------------ .../gramophone/service/MusicService.java | 12 +- 10 files changed, 150 insertions(+), 324 deletions(-) create mode 100644 app/src/main/java/com/dkanada/gramophone/database/JellyDatabase.java create mode 100644 app/src/main/java/com/dkanada/gramophone/database/QueueSong.java create mode 100644 app/src/main/java/com/dkanada/gramophone/database/QueueSongDao.java create mode 100644 app/src/main/java/com/dkanada/gramophone/database/SongDao.java delete mode 100644 app/src/main/java/com/dkanada/gramophone/loader/SongLoader.java delete mode 100644 app/src/main/java/com/dkanada/gramophone/provider/QueueStore.java diff --git a/app/build.gradle b/app/build.gradle index 66a98344..b8666ba7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -78,6 +78,9 @@ dependencies { implementation 'com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:1.0.0' implementation 'com.android.support:multidex:1.0.3' + implementation 'androidx.room:room-runtime:2.2.5' + annotationProcessor 'androidx.room:room-compiler:2.2.5' + implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' diff --git a/app/src/main/java/com/dkanada/gramophone/App.java b/app/src/main/java/com/dkanada/gramophone/App.java index d5ec3738..c54e2202 100644 --- a/app/src/main/java/com/dkanada/gramophone/App.java +++ b/app/src/main/java/com/dkanada/gramophone/App.java @@ -6,6 +6,9 @@ import android.content.Context; import android.os.Build; import android.provider.Settings; +import androidx.room.Room; + +import com.dkanada.gramophone.database.JellyDatabase; import com.dkanada.gramophone.helper.EventListener; import com.dkanada.gramophone.util.PreferenceUtil; import com.kabouzeid.appthemehelper.ThemeStore; @@ -22,6 +25,7 @@ import org.jellyfin.apiclient.logging.ILogger; public class App extends Application { private static App app; + private static JellyDatabase database; private static ApiClient apiClient; @Override @@ -29,6 +33,7 @@ public class App extends Application { super.onCreate(); app = this; + database = createDatabase(this); apiClient = createApiClient(this); // default theme @@ -42,6 +47,12 @@ public class App extends Application { } } + public static JellyDatabase createDatabase(Context context) { + return Room.databaseBuilder(context, JellyDatabase.class, "database") + .allowMainThreadQueries() + .build(); + } + public static ApiClient createApiClient(Context context) { String appName = context.getString(R.string.app_name); String appVersion = BuildConfig.VERSION_NAME; @@ -59,6 +70,10 @@ public class App extends Application { return new ApiClient(httpClient, logger, server, appName, appVersion, device, eventListener); } + public static JellyDatabase getDatabase() { + return database; + } + public static ApiClient getApiClient() { return apiClient; } diff --git a/app/src/main/java/com/dkanada/gramophone/database/JellyDatabase.java b/app/src/main/java/com/dkanada/gramophone/database/JellyDatabase.java new file mode 100644 index 00000000..1c8706c6 --- /dev/null +++ b/app/src/main/java/com/dkanada/gramophone/database/JellyDatabase.java @@ -0,0 +1,18 @@ +package com.dkanada.gramophone.database; + +import androidx.room.RoomDatabase; + +import com.dkanada.gramophone.model.Song; + +@androidx.room.Database( + entities = { + Song.class, + QueueSong.class + }, + version = 1, + exportSchema = false +) +public abstract class JellyDatabase extends RoomDatabase { + public abstract QueueSongDao queueSongDao(); + public abstract SongDao songDao(); +} diff --git a/app/src/main/java/com/dkanada/gramophone/database/QueueSong.java b/app/src/main/java/com/dkanada/gramophone/database/QueueSong.java new file mode 100644 index 00000000..8cd66f0b --- /dev/null +++ b/app/src/main/java/com/dkanada/gramophone/database/QueueSong.java @@ -0,0 +1,34 @@ +package com.dkanada.gramophone.database; + +import androidx.room.Entity; +import androidx.room.ForeignKey; + +import com.dkanada.gramophone.model.Song; + +@Entity( + tableName = "queueSongs", + primaryKeys = { + "index", + "queue" + } +) +public class QueueSong { + public int index; + + public int queue; + + @ForeignKey( + entity = Song.class, + parentColumns = {"id"}, + childColumns = {"songId"}, + onDelete = ForeignKey.CASCADE + ) + public String songId; + + public QueueSong(String songId, int index, int queue) { + this.songId = songId; + + this.index = index; + this.queue = queue; + } +} diff --git a/app/src/main/java/com/dkanada/gramophone/database/QueueSongDao.java b/app/src/main/java/com/dkanada/gramophone/database/QueueSongDao.java new file mode 100644 index 00000000..a1a7fbbf --- /dev/null +++ b/app/src/main/java/com/dkanada/gramophone/database/QueueSongDao.java @@ -0,0 +1,45 @@ +package com.dkanada.gramophone.database; + +import androidx.room.Dao; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; + +import com.dkanada.gramophone.App; +import com.dkanada.gramophone.model.Song; + +import java.util.ArrayList; +import java.util.List; + +@Dao +public abstract class QueueSongDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + public abstract void insertQueueSongs(List queueSongs); + + @Query("DELETE FROM queueSongs") + public abstract void deleteQueueSongs(); + + @Query("SELECT * from queueSongs WHERE queue = :queue ORDER BY `index`") + public abstract List getQueueSongs(int queue); + + public List getQueue(int queue) { + List queueSongs = getQueueSongs(queue); + List songs = new ArrayList<>(); + + for (QueueSong queueSong : queueSongs) { + Song song = App.getDatabase().songDao().getSong(queueSong.songId); + if (song != null) songs.add(song); + } + + return songs; + } + + public void setQueue(List songs, int queue) { + List queueSongs = new ArrayList<>(); + for (int i = 0; i < songs.size(); i++) { + queueSongs.add(new QueueSong(songs.get(i).id, i, queue)); + } + + insertQueueSongs(queueSongs); + } +} diff --git a/app/src/main/java/com/dkanada/gramophone/database/SongDao.java b/app/src/main/java/com/dkanada/gramophone/database/SongDao.java new file mode 100644 index 00000000..1248bd68 --- /dev/null +++ b/app/src/main/java/com/dkanada/gramophone/database/SongDao.java @@ -0,0 +1,22 @@ +package com.dkanada.gramophone.database; + +import androidx.room.Dao; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; + +import com.dkanada.gramophone.model.Song; + +import java.util.List; + +@Dao +public interface SongDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insertSongs(List songs); + + @Query("DELETE FROM songs") + void deleteSongs(); + + @Query("SELECT * FROM songs WHERE id = :id") + Song getSong(String id); +} diff --git a/app/src/main/java/com/dkanada/gramophone/loader/SongLoader.java b/app/src/main/java/com/dkanada/gramophone/loader/SongLoader.java deleted file mode 100644 index aef59533..00000000 --- a/app/src/main/java/com/dkanada/gramophone/loader/SongLoader.java +++ /dev/null @@ -1,112 +0,0 @@ -package com.dkanada.gramophone.loader; - -import android.content.Context; -import android.database.Cursor; -import android.provider.BaseColumns; -import android.provider.MediaStore; -import android.provider.MediaStore.Audio.AudioColumns; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.dkanada.gramophone.model.Song; -import com.dkanada.gramophone.util.PreferenceUtil; - -import java.util.ArrayList; -import java.util.List; - -public class SongLoader { - public static final String QUEUE_PRIMARY = "image"; - public static final String QUEUE_FAVORITE = "favorite"; - - protected static final String BASE_SELECTION = AudioColumns.IS_MUSIC + "=1" + " AND " + AudioColumns.TITLE + " != ''"; - protected static final String[] BASE_PROJECTION = new String[]{ - BaseColumns._ID, - AudioColumns.TITLE, - AudioColumns.TRACK, - AudioColumns.YEAR, - AudioColumns.DURATION, - AudioColumns.ALBUM_ID, - AudioColumns.ALBUM, - AudioColumns.ARTIST_ID, - AudioColumns.ARTIST, - QUEUE_PRIMARY, - QUEUE_FAVORITE, - }; - - @NonNull - public static List getAllSongs(@NonNull Context context) { - Cursor cursor = makeSongCursor(context, null, null); - return getSongs(cursor); - } - - @NonNull - public static List getSongs(@Nullable final Cursor cursor) { - List songs = new ArrayList<>(); - if (cursor != null && cursor.moveToFirst()) { - do { - songs.add(getSongFromCursorImpl(cursor)); - } while (cursor.moveToNext()); - } - - if (cursor != null) cursor.close(); - return songs; - } - - @NonNull - public static Song getSong(@Nullable Cursor cursor) { - Song song; - if (cursor != null && cursor.moveToFirst()) { - song = getSongFromCursorImpl(cursor); - } else { - song = Song.EMPTY; - } - - if (cursor != null) { - cursor.close(); - } - - return song; - } - - @NonNull - private static Song getSongFromCursorImpl(@NonNull Cursor cursor) { - final String id = cursor.getString(0); - final String title = cursor.getString(1); - final int trackNumber = cursor.getInt(2); - final int year = cursor.getInt(3); - final long duration = cursor.getLong(4); - - final String albumId = cursor.getString(5); - final String albumName = cursor.getString(6); - - final String artistId = cursor.getString(7); - final String artistName = cursor.getString(8); - - final String primary = cursor.getString(9); - final boolean favorite = Boolean.valueOf(cursor.getString(10)); - - return new Song(id, title, trackNumber, 1, year, duration, albumId, albumName, artistId, artistName, primary, favorite); - } - - @Nullable - public static Cursor makeSongCursor(@NonNull final Context context, @Nullable final String selection, final String[] selectionValues) { - return makeSongCursor(context, selection, selectionValues, PreferenceUtil.getInstance(context).getSongSortOrder()); - } - - @Nullable - public static Cursor makeSongCursor(@NonNull final Context context, @Nullable String selection, String[] selectionValues, final String sortOrder) { - if (selection != null && !selection.trim().equals("")) { - selection = BASE_SELECTION + " AND " + selection; - } else { - selection = BASE_SELECTION; - } - - try { - return context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, - BASE_PROJECTION, selection, selectionValues, sortOrder); - } catch (SecurityException e) { - return null; - } - } -} diff --git a/app/src/main/java/com/dkanada/gramophone/model/Song.java b/app/src/main/java/com/dkanada/gramophone/model/Song.java index 2612d953..57bcc26b 100644 --- a/app/src/main/java/com/dkanada/gramophone/model/Song.java +++ b/app/src/main/java/com/dkanada/gramophone/model/Song.java @@ -4,6 +4,8 @@ import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; +import androidx.room.Entity; +import androidx.room.PrimaryKey; import org.jellyfin.apiclient.model.dto.BaseItemDto; import org.jellyfin.apiclient.model.dto.MediaSourceInfo; @@ -12,9 +14,12 @@ import org.jellyfin.apiclient.model.entities.MediaStream; import java.util.UUID; +@Entity(tableName = "songs") public class Song implements Parcelable { public static final Song EMPTY = new Song(); + @NonNull + @PrimaryKey public String id; public String title; public int trackNumber; @@ -93,24 +98,6 @@ public class Song implements Parcelable { } } - public Song(String id, String title, int trackNumber, int discNumber, int year, long duration, String albumId, String albumName, String artistId, String artistName, String primary, boolean favorite) { - this.id = id; - this.title = title; - this.trackNumber = trackNumber; - this.discNumber = discNumber; - this.year = year; - this.duration = duration; - - this.albumId = albumId; - this.albumName = albumName; - - this.artistId = artistId; - this.artistName = artistName; - - this.primary = primary; - this.favorite = favorite; - } - @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/app/src/main/java/com/dkanada/gramophone/provider/QueueStore.java b/app/src/main/java/com/dkanada/gramophone/provider/QueueStore.java deleted file mode 100644 index fc34f6ab..00000000 --- a/app/src/main/java/com/dkanada/gramophone/provider/QueueStore.java +++ /dev/null @@ -1,190 +0,0 @@ -/* -* 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.dkanada.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.provider.BaseColumns; -import android.provider.MediaStore.Audio.AudioColumns; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.dkanada.gramophone.App; -import com.dkanada.gramophone.loader.SongLoader; -import com.dkanada.gramophone.model.Song; -import com.dkanada.gramophone.util.PreferenceUtil; - -import java.util.ArrayList; -import java.util.List; - -public class QueueStore extends SQLiteOpenHelper { - @Nullable - private static QueueStore sInstance = null; - public static final String DATABASE_NAME = "music_playback_state.db"; - public static final String PLAYING_QUEUE_TABLE_NAME = "playing_queue"; - public static final String ORIGINAL_PLAYING_QUEUE_TABLE_NAME = "original_playing_queue"; - private static final int VERSION = 5; - - public QueueStore(final Context context) { - super(context, DATABASE_NAME, null, VERSION); - } - - @Override - public void onCreate(@NonNull final SQLiteDatabase db) { - createTable(db, PLAYING_QUEUE_TABLE_NAME); - createTable(db, ORIGINAL_PLAYING_QUEUE_TABLE_NAME); - } - - private void createTable(@NonNull final SQLiteDatabase db, final String tableName) { - // noinspection StringBufferReplaceableByString - StringBuilder builder = new StringBuilder(); - builder.append("CREATE TABLE IF NOT EXISTS "); - builder.append(tableName); - builder.append("("); - - builder.append(BaseColumns._ID); - builder.append(" INT NOT NULL,"); - - builder.append(AudioColumns.TITLE); - builder.append(" STRING NOT NULL,"); - - builder.append(AudioColumns.TRACK); - builder.append(" INT NOT NULL,"); - - builder.append(AudioColumns.YEAR); - builder.append(" INT NOT NULL,"); - - builder.append(AudioColumns.DURATION); - builder.append(" LONG NOT NULL,"); - - builder.append(AudioColumns.ALBUM_ID); - builder.append(" INT NOT NULL,"); - - builder.append(AudioColumns.ALBUM); - builder.append(" STRING NOT NULL,"); - - builder.append(AudioColumns.ARTIST_ID); - builder.append(" INT NOT NULL,"); - - builder.append(AudioColumns.ARTIST); - builder.append(" STRING NOT NULL,"); - - builder.append(SongLoader.QUEUE_PRIMARY); - builder.append(" STRING NOT NULL,"); - - builder.append(SongLoader.QUEUE_FAVORITE); - builder.append(" STRING NOT NULL);"); - - db.execSQL(builder.toString()); - } - - @Override - public void onUpgrade(@NonNull final SQLiteDatabase db, final int oldVersion, final int newVersion) { - db.execSQL("DROP TABLE IF EXISTS " + PLAYING_QUEUE_TABLE_NAME); - db.execSQL("DROP TABLE IF EXISTS " + ORIGINAL_PLAYING_QUEUE_TABLE_NAME); - onCreate(db); - } - - @Override - public void onDowngrade(@NonNull SQLiteDatabase db, int oldVersion, int newVersion) { - db.execSQL("DROP TABLE IF EXISTS " + PLAYING_QUEUE_TABLE_NAME); - db.execSQL("DROP TABLE IF EXISTS " + ORIGINAL_PLAYING_QUEUE_TABLE_NAME); - onCreate(db); - } - - @NonNull - public static synchronized QueueStore getInstance(@NonNull final Context context) { - if (sInstance == null) { - sInstance = new QueueStore(context.getApplicationContext()); - } - - return sInstance; - } - - public synchronized void saveQueues(@NonNull final List playingQueue, @NonNull final List originalPlayingQueue) { - saveQueue(PLAYING_QUEUE_TABLE_NAME, playingQueue); - saveQueue(ORIGINAL_PLAYING_QUEUE_TABLE_NAME, originalPlayingQueue); - } - - private synchronized void saveQueue(final String tableName, @NonNull final List queue) { - final SQLiteDatabase database = getWritableDatabase(); - database.beginTransaction(); - - try { - database.delete(tableName, null, null); - database.setTransactionSuccessful(); - } finally { - database.endTransaction(); - } - - final int NUM_PROCESS = 20; - int position = 0; - while (position < queue.size()) { - database.beginTransaction(); - try { - for (int i = position; i < queue.size() && i < position + NUM_PROCESS; i++) { - Song song = queue.get(i); - ContentValues values = new ContentValues(4); - - values.put(BaseColumns._ID, song.id); - values.put(AudioColumns.TITLE, song.title); - values.put(AudioColumns.TRACK, song.trackNumber); - values.put(AudioColumns.YEAR, song.year); - values.put(AudioColumns.DURATION, song.duration); - values.put(AudioColumns.ALBUM_ID, song.albumId); - values.put(AudioColumns.ALBUM, song.albumName); - values.put(AudioColumns.ARTIST_ID, song.artistId); - values.put(AudioColumns.ARTIST, song.artistName); - values.put(SongLoader.QUEUE_PRIMARY, song.primary); - values.put(SongLoader.QUEUE_FAVORITE, song.favorite); - - database.insert(tableName, null, values); - } - - database.setTransactionSuccessful(); - } finally { - database.endTransaction(); - position += NUM_PROCESS; - } - } - } - - @NonNull - public List getSavedPlayingQueue() { - return getQueue(PLAYING_QUEUE_TABLE_NAME); - } - - @NonNull - public List getSavedOriginalPlayingQueue() { - return getQueue(ORIGINAL_PLAYING_QUEUE_TABLE_NAME); - } - - @NonNull - private List getQueue(@NonNull final String tableName) { - if (!PreferenceUtil.getInstance(App.getInstance()).getRememberQueue()) { - return new ArrayList<>(); - } - - Cursor cursor = getReadableDatabase().query(tableName, null, - null, null, null, null, null); - return SongLoader.getSongs(cursor); - } -} diff --git a/app/src/main/java/com/dkanada/gramophone/service/MusicService.java b/app/src/main/java/com/dkanada/gramophone/service/MusicService.java index 3dddb66f..cae54511 100644 --- a/app/src/main/java/com/dkanada/gramophone/service/MusicService.java +++ b/app/src/main/java/com/dkanada/gramophone/service/MusicService.java @@ -42,7 +42,6 @@ import com.dkanada.gramophone.glide.CustomGlideRequest; import com.dkanada.gramophone.helper.ShuffleHelper; import com.dkanada.gramophone.model.Playlist; import com.dkanada.gramophone.model.Song; -import com.dkanada.gramophone.provider.QueueStore; import com.dkanada.gramophone.service.notification.PlayingNotification; import com.dkanada.gramophone.service.notification.PlayingNotificationImpl; import com.dkanada.gramophone.service.notification.PlayingNotificationImpl24; @@ -395,7 +394,12 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP } private void saveQueue() { - QueueStore.getInstance(this).saveQueues(playingQueue, originalPlayingQueue); + App.getDatabase().songDao().deleteSongs(); + App.getDatabase().songDao().insertSongs(playingQueue); + + App.getDatabase().queueSongDao().deleteQueueSongs(); + App.getDatabase().queueSongDao().setQueue(playingQueue, 0); + App.getDatabase().queueSongDao().setQueue(originalPlayingQueue, 1); } private void savePosition() { @@ -427,8 +431,8 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP private synchronized void restoreQueuesAndPositionIfNecessary() { if (!queuesRestored && playingQueue.isEmpty()) { - List restoredQueue = QueueStore.getInstance(this).getSavedPlayingQueue(); - List restoredOriginalQueue = QueueStore.getInstance(this).getSavedOriginalPlayingQueue(); + List restoredQueue = App.getDatabase().queueSongDao().getQueue(0); + List restoredOriginalQueue = App.getDatabase().queueSongDao().getQueue(1); int restoredPosition = PreferenceManager.getDefaultSharedPreferences(this).getInt(PreferenceUtil.POSITION, -1); int restoredPositionInTrack = PreferenceManager.getDefaultSharedPreferences(this).getInt(PreferenceUtil.PROGRESS, -1);