replace queue store with room database

This commit is contained in:
dkanada 2020-11-08 13:12:19 +09:00
commit 44c1bb63b6
10 changed files with 150 additions and 324 deletions

View file

@ -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'

View file

@ -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;
}

View file

@ -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();
}

View file

@ -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;
}
}

View file

@ -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<QueueSong> queueSongs);
@Query("DELETE FROM queueSongs")
public abstract void deleteQueueSongs();
@Query("SELECT * from queueSongs WHERE queue = :queue ORDER BY `index`")
public abstract List<QueueSong> getQueueSongs(int queue);
public List<Song> getQueue(int queue) {
List<QueueSong> queueSongs = getQueueSongs(queue);
List<Song> 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<Song> songs, int queue) {
List<QueueSong> queueSongs = new ArrayList<>();
for (int i = 0; i < songs.size(); i++) {
queueSongs.add(new QueueSong(songs.get(i).id, i, queue));
}
insertQueueSongs(queueSongs);
}
}

View file

@ -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<Song> songs);
@Query("DELETE FROM songs")
void deleteSongs();
@Query("SELECT * FROM songs WHERE id = :id")
Song getSong(String id);
}

View file

@ -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<Song> getAllSongs(@NonNull Context context) {
Cursor cursor = makeSongCursor(context, null, null);
return getSongs(cursor);
}
@NonNull
public static List<Song> getSongs(@Nullable final Cursor cursor) {
List<Song> 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;
}
}
}

View file

@ -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;

View file

@ -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<Song> playingQueue, @NonNull final List<Song> originalPlayingQueue) {
saveQueue(PLAYING_QUEUE_TABLE_NAME, playingQueue);
saveQueue(ORIGINAL_PLAYING_QUEUE_TABLE_NAME, originalPlayingQueue);
}
private synchronized void saveQueue(final String tableName, @NonNull final List<Song> 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<Song> getSavedPlayingQueue() {
return getQueue(PLAYING_QUEUE_TABLE_NAME);
}
@NonNull
public List<Song> getSavedOriginalPlayingQueue() {
return getQueue(ORIGINAL_PLAYING_QUEUE_TABLE_NAME);
}
@NonNull
private List<Song> 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);
}
}

View file

@ -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<Song> restoredQueue = QueueStore.getInstance(this).getSavedPlayingQueue();
List<Song> restoredOriginalQueue = QueueStore.getInstance(this).getSavedOriginalPlayingQueue();
List<Song> restoredQueue = App.getDatabase().queueSongDao().getQueue(0);
List<Song> restoredOriginalQueue = App.getDatabase().queueSongDao().getQueue(1);
int restoredPosition = PreferenceManager.getDefaultSharedPreferences(this).getInt(PreferenceUtil.POSITION, -1);
int restoredPositionInTrack = PreferenceManager.getDefaultSharedPreferences(this).getInt(PreferenceUtil.PROGRESS, -1);