summaryrefslogtreecommitdiff
path: root/storage
diff options
context:
space:
mode:
authorByteHamster <info@bytehamster.com>2022-02-27 12:12:24 +0100
committerByteHamster <info@bytehamster.com>2022-02-27 19:47:41 +0100
commitff3258c34a1797e75538b83baa1e867d0ea07ceb (patch)
tree82530a8ce01909638b143ae0d72f3862811c5321 /storage
parent42e36328e3bb6d4bda632da7586fdace912a34e0 (diff)
downloadAntennaPod-ff3258c34a1797e75538b83baa1e867d0ea07ceb.zip
Move database to its own module
Diffstat (limited to 'storage')
-rw-r--r--storage/README.md3
-rw-r--r--storage/database/README.md3
-rw-r--r--storage/database/build.gradle17
-rw-r--r--storage/database/src/main/AndroidManifest.xml1
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/DBUpgrader.java331
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/LongIntMap.java263
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java1430
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/ChapterCursorMapper.java32
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/DownloadStatusCursorMapper.java35
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedCursorMapper.java70
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedItemCursorMapper.java46
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedItemFilterQuery.java76
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedMediaCursorMapper.java67
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedPreferencesCursorMapper.java74
14 files changed, 2448 insertions, 0 deletions
diff --git a/storage/README.md b/storage/README.md
new file mode 100644
index 000000000..adabdc7a6
--- /dev/null
+++ b/storage/README.md
@@ -0,0 +1,3 @@
+# :storage
+
+Data storage for podcast data (subscriptions, playback state) and settings (but not settings UI).
diff --git a/storage/database/README.md b/storage/database/README.md
new file mode 100644
index 000000000..45af475f2
--- /dev/null
+++ b/storage/database/README.md
@@ -0,0 +1,3 @@
+# :storage:database
+
+AntennaPod's main database, containing subscriptions and playback state (but not user settings).
diff --git a/storage/database/build.gradle b/storage/database/build.gradle
new file mode 100644
index 000000000..141cdb086
--- /dev/null
+++ b/storage/database/build.gradle
@@ -0,0 +1,17 @@
+plugins {
+ id("com.android.library")
+}
+apply from: "../../common.gradle"
+
+android {
+ lintOptions {
+ disable "StaticFieldLeak"
+ }
+}
+
+dependencies {
+ implementation project(':model')
+
+ annotationProcessor "androidx.annotation:annotation:$annotationVersion"
+ implementation "commons-io:commons-io:$commonsioVersion"
+}
diff --git a/storage/database/src/main/AndroidManifest.xml b/storage/database/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..df0171d12
--- /dev/null
+++ b/storage/database/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+<manifest package="de.danoeh.antennapod.storage.database" />
diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/DBUpgrader.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/DBUpgrader.java
new file mode 100644
index 000000000..1954a5652
--- /dev/null
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/DBUpgrader.java
@@ -0,0 +1,331 @@
+package de.danoeh.antennapod.storage.database;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.media.MediaMetadataRetriever;
+import android.util.Log;
+
+import de.danoeh.antennapod.model.feed.FeedItem;
+
+import static de.danoeh.antennapod.model.feed.FeedPreferences.SPEED_USE_GLOBAL;
+
+class DBUpgrader {
+ /**
+ * Upgrades the given database to a new schema version
+ */
+ static void upgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
+ if (oldVersion <= 1) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + " ADD COLUMN "
+ + PodDBAdapter.KEY_TYPE + " TEXT");
+ }
+ if (oldVersion <= 2) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_SIMPLECHAPTERS
+ + " ADD COLUMN " + PodDBAdapter.KEY_LINK + " TEXT");
+ }
+ if (oldVersion <= 3) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS
+ + " ADD COLUMN " + PodDBAdapter.KEY_ITEM_IDENTIFIER + " TEXT");
+ }
+ if (oldVersion <= 4) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + " ADD COLUMN "
+ + PodDBAdapter.KEY_FEED_IDENTIFIER + " TEXT");
+ }
+ if (oldVersion <= 5) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_DOWNLOAD_LOG
+ + " ADD COLUMN " + PodDBAdapter.KEY_REASON_DETAILED + " TEXT");
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_DOWNLOAD_LOG
+ + " ADD COLUMN " + PodDBAdapter.KEY_DOWNLOADSTATUS_TITLE + " TEXT");
+ }
+ if (oldVersion <= 6) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_SIMPLECHAPTERS
+ + " ADD COLUMN type INTEGER");
+ }
+ if (oldVersion <= 7) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA
+ + " ADD COLUMN " + PodDBAdapter.KEY_PLAYBACK_COMPLETION_DATE
+ + " INTEGER");
+ }
+ if (oldVersion <= 8) {
+ final int KEY_ID_POSITION = 0;
+ final int KEY_MEDIA_POSITION = 1;
+
+ // Add feeditem column to feedmedia table
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA
+ + " ADD COLUMN " + PodDBAdapter.KEY_FEEDITEM
+ + " INTEGER");
+ Cursor feeditemCursor = db.query(PodDBAdapter.TABLE_NAME_FEED_ITEMS,
+ new String[]{PodDBAdapter.KEY_ID, PodDBAdapter.KEY_MEDIA}, "? > 0",
+ new String[]{PodDBAdapter.KEY_MEDIA}, null, null, null);
+ if (feeditemCursor.moveToFirst()) {
+ db.beginTransaction();
+ ContentValues contentValues = new ContentValues();
+ do {
+ long mediaId = feeditemCursor.getLong(KEY_MEDIA_POSITION);
+ contentValues.put(PodDBAdapter.KEY_FEEDITEM, feeditemCursor.getLong(KEY_ID_POSITION));
+ db.update(PodDBAdapter.TABLE_NAME_FEED_MEDIA, contentValues, PodDBAdapter.KEY_ID + "=?", new String[]{String.valueOf(mediaId)});
+ contentValues.clear();
+ } while (feeditemCursor.moveToNext());
+ db.setTransactionSuccessful();
+ db.endTransaction();
+ }
+ feeditemCursor.close();
+ }
+ if (oldVersion <= 9) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " ADD COLUMN " + PodDBAdapter.KEY_AUTO_DOWNLOAD_ENABLED
+ + " INTEGER DEFAULT 1");
+ }
+ if (oldVersion <= 10) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " ADD COLUMN flattr_status"
+ + " INTEGER");
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS
+ + " ADD COLUMN flattr_status"
+ + " INTEGER");
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA
+ + " ADD COLUMN " + PodDBAdapter.KEY_PLAYED_DURATION
+ + " INTEGER");
+ }
+ if (oldVersion <= 11) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " ADD COLUMN " + PodDBAdapter.KEY_USERNAME
+ + " TEXT");
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " ADD COLUMN " + PodDBAdapter.KEY_PASSWORD
+ + " TEXT");
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS
+ + " ADD COLUMN image"
+ + " INTEGER");
+ }
+ if (oldVersion <= 12) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " ADD COLUMN " + PodDBAdapter.KEY_IS_PAGED + " INTEGER DEFAULT 0");
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " ADD COLUMN " + PodDBAdapter.KEY_NEXT_PAGE_LINK + " TEXT");
+ }
+ if (oldVersion <= 13) {
+ // remove duplicate rows in "Chapters" table that were created because of a bug.
+ db.execSQL(String.format("DELETE FROM %s WHERE %s NOT IN " +
+ "(SELECT MIN(%s) as %s FROM %s GROUP BY %s,%s,%s,%s,%s)",
+ PodDBAdapter.TABLE_NAME_SIMPLECHAPTERS,
+ PodDBAdapter.KEY_ID,
+ PodDBAdapter.KEY_ID,
+ PodDBAdapter.KEY_ID,
+ PodDBAdapter.TABLE_NAME_SIMPLECHAPTERS,
+ PodDBAdapter.KEY_TITLE,
+ PodDBAdapter.KEY_START,
+ PodDBAdapter.KEY_FEEDITEM,
+ PodDBAdapter.KEY_LINK,
+ "type"));
+ }
+ if (oldVersion <= 14) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS
+ + " ADD COLUMN " + PodDBAdapter.KEY_AUTO_DOWNLOAD_ATTEMPTS + " INTEGER");
+ db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS
+ + " SET " + PodDBAdapter.KEY_AUTO_DOWNLOAD_ATTEMPTS + " = "
+ + "(SELECT " + PodDBAdapter.KEY_AUTO_DOWNLOAD_ENABLED
+ + " FROM " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " WHERE " + PodDBAdapter.TABLE_NAME_FEEDS + "." + PodDBAdapter.KEY_ID
+ + " = " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_FEED + ")");
+
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " ADD COLUMN " + PodDBAdapter.KEY_HIDE + " TEXT");
+
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " ADD COLUMN " + PodDBAdapter.KEY_LAST_UPDATE_FAILED + " INTEGER DEFAULT 0");
+
+ // create indexes
+ db.execSQL(PodDBAdapter.CREATE_INDEX_FEEDITEMS_FEED);
+ db.execSQL(PodDBAdapter.CREATE_INDEX_FEEDMEDIA_FEEDITEM);
+ db.execSQL(PodDBAdapter.CREATE_INDEX_QUEUE_FEEDITEM);
+ db.execSQL(PodDBAdapter.CREATE_INDEX_SIMPLECHAPTERS_FEEDITEM);
+ }
+ if (oldVersion <= 15) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA
+ + " ADD COLUMN " + PodDBAdapter.KEY_HAS_EMBEDDED_PICTURE + " INTEGER DEFAULT -1");
+ db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA
+ + " SET " + PodDBAdapter.KEY_HAS_EMBEDDED_PICTURE + "=0"
+ + " WHERE " + PodDBAdapter.KEY_DOWNLOADED + "=0");
+ Cursor c = db.rawQuery("SELECT " + PodDBAdapter.KEY_FILE_URL
+ + " FROM " + PodDBAdapter.TABLE_NAME_FEED_MEDIA
+ + " WHERE " + PodDBAdapter.KEY_DOWNLOADED + "=1 "
+ + " AND " + PodDBAdapter.KEY_HAS_EMBEDDED_PICTURE + "=-1", null);
+ if (c.moveToFirst()) {
+ MediaMetadataRetriever mmr = new MediaMetadataRetriever();
+ do {
+ String fileUrl = c.getString(0);
+ try {
+ mmr.setDataSource(fileUrl);
+ byte[] image = mmr.getEmbeddedPicture();
+ if (image != null) {
+ db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA
+ + " SET " + PodDBAdapter.KEY_HAS_EMBEDDED_PICTURE + "=1"
+ + " WHERE " + PodDBAdapter.KEY_FILE_URL + "='" + fileUrl + "'");
+ } else {
+ db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA
+ + " SET " + PodDBAdapter.KEY_HAS_EMBEDDED_PICTURE + "=0"
+ + " WHERE " + PodDBAdapter.KEY_FILE_URL + "='" + fileUrl + "'");
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ } while (c.moveToNext());
+ }
+ c.close();
+ }
+ if (oldVersion <= 16) {
+ String selectNew = "SELECT " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_ID
+ + " FROM " + PodDBAdapter.TABLE_NAME_FEED_ITEMS
+ + " INNER JOIN " + PodDBAdapter.TABLE_NAME_FEED_MEDIA + " ON "
+ + PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_ID + "="
+ + PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + PodDBAdapter.KEY_FEEDITEM
+ + " LEFT OUTER JOIN " + PodDBAdapter.TABLE_NAME_QUEUE + " ON "
+ + PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_ID + "="
+ + PodDBAdapter.TABLE_NAME_QUEUE + "." + PodDBAdapter.KEY_FEEDITEM
+ + " WHERE "
+ + PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_READ + " = 0 AND " // unplayed
+ + PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + PodDBAdapter.KEY_DOWNLOADED + " = 0 AND " // undownloaded
+ + PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + PodDBAdapter.KEY_POSITION + " = 0 AND " // not partially played
+ + PodDBAdapter.TABLE_NAME_QUEUE + "." + PodDBAdapter.KEY_ID + " IS NULL"; // not in queue
+ String sql = "UPDATE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS
+ + " SET " + PodDBAdapter.KEY_READ + "=" + FeedItem.NEW
+ + " WHERE " + PodDBAdapter.KEY_ID + " IN (" + selectNew + ")";
+ Log.d("Migration", "SQL: " + sql);
+ db.execSQL(sql);
+ }
+ if (oldVersion <= 17) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " ADD COLUMN " + PodDBAdapter.KEY_AUTO_DELETE_ACTION + " INTEGER DEFAULT 0");
+ }
+ if (oldVersion < 1030005) {
+ db.execSQL("UPDATE FeedItems SET auto_download=0 WHERE " +
+ "(read=1 OR id IN (SELECT feeditem FROM FeedMedia WHERE position>0 OR downloaded=1)) " +
+ "AND id NOT IN (SELECT feeditem FROM Queue)");
+ }
+ if (oldVersion < 1040001) {
+ db.execSQL(PodDBAdapter.CREATE_TABLE_FAVORITES);
+ }
+ if (oldVersion < 1040002) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA
+ + " ADD COLUMN " + PodDBAdapter.KEY_LAST_PLAYED_TIME + " INTEGER DEFAULT 0");
+ }
+ if (oldVersion < 1040013) {
+ db.execSQL(PodDBAdapter.CREATE_INDEX_FEEDITEMS_PUBDATE);
+ db.execSQL(PodDBAdapter.CREATE_INDEX_FEEDITEMS_READ);
+ }
+ if (oldVersion < 1050003) {
+ // Migrates feed list filter data
+
+ db.beginTransaction();
+
+ // Change to intermediate values to avoid overwriting in the following find/replace
+ db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEEDS + "\n" +
+ "SET " + PodDBAdapter.KEY_HIDE + " = replace(" + PodDBAdapter.KEY_HIDE + ", 'unplayed', 'noplay')");
+ db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEEDS + "\n" +
+ "SET " + PodDBAdapter.KEY_HIDE + " = replace(" + PodDBAdapter.KEY_HIDE + ", 'not_queued', 'noqueue')");
+ db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEEDS + "\n" +
+ "SET " + PodDBAdapter.KEY_HIDE + " = replace(" + PodDBAdapter.KEY_HIDE + ", 'not_downloaded', 'nodl')");
+
+ // Replace played, queued, and downloaded with their opposites
+ db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEEDS + "\n" +
+ "SET " + PodDBAdapter.KEY_HIDE + " = replace(" + PodDBAdapter.KEY_HIDE + ", 'played', 'unplayed')");
+ db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEEDS + "\n" +
+ "SET " + PodDBAdapter.KEY_HIDE + " = replace(" + PodDBAdapter.KEY_HIDE + ", 'queued', 'not_queued')");
+ db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEEDS + "\n" +
+ "SET " + PodDBAdapter.KEY_HIDE + " = replace(" + PodDBAdapter.KEY_HIDE + ", 'downloaded', 'not_downloaded')");
+
+ // Now replace intermediates for unplayed, not queued, etc. with their opposites
+ db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEEDS + "\n" +
+ "SET " + PodDBAdapter.KEY_HIDE + " = replace(" + PodDBAdapter.KEY_HIDE + ", 'noplay', 'played')");
+ db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEEDS + "\n" +
+ "SET " + PodDBAdapter.KEY_HIDE + " = replace(" + PodDBAdapter.KEY_HIDE + ", 'noqueue', 'queued')");
+ db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEEDS + "\n" +
+ "SET " + PodDBAdapter.KEY_HIDE + " = replace(" + PodDBAdapter.KEY_HIDE + ", 'nodl', 'downloaded')");
+
+ // Paused doesn't have an opposite, so unplayed is the next best option
+ db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEEDS + "\n" +
+ "SET " + PodDBAdapter.KEY_HIDE + " = replace(" + PodDBAdapter.KEY_HIDE + ", 'paused', 'unplayed')");
+
+ db.setTransactionSuccessful();
+ db.endTransaction();
+
+ // and now get ready for autodownload filters
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " ADD COLUMN " + PodDBAdapter.KEY_INCLUDE_FILTER + " TEXT DEFAULT ''");
+
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " ADD COLUMN " + PodDBAdapter.KEY_EXCLUDE_FILTER + " TEXT DEFAULT ''");
+
+ // and now auto refresh
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " ADD COLUMN " + PodDBAdapter.KEY_KEEP_UPDATED + " INTEGER DEFAULT 1");
+ }
+ if (oldVersion < 1050004) {
+ // prevent old timestamps to be misinterpreted as ETags
+ db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " SET " + PodDBAdapter.KEY_LASTUPDATE + "=NULL");
+ }
+ if (oldVersion < 1060200) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " ADD COLUMN " + PodDBAdapter.KEY_CUSTOM_TITLE + " TEXT");
+ }
+ if (oldVersion < 1060596) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " ADD COLUMN " + PodDBAdapter.KEY_IMAGE_URL + " TEXT");
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS
+ + " ADD COLUMN " + PodDBAdapter.KEY_IMAGE_URL + " TEXT");
+
+ db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + " SET " + PodDBAdapter.KEY_IMAGE_URL + " = ("
+ + " SELECT " + PodDBAdapter.KEY_DOWNLOAD_URL
+ + " FROM " + PodDBAdapter.TABLE_NAME_FEED_IMAGES
+ + " WHERE " + PodDBAdapter.TABLE_NAME_FEED_IMAGES + "." + PodDBAdapter.KEY_ID
+ + " = " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + ".image)");
+
+ db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEEDS + " SET " + PodDBAdapter.KEY_IMAGE_URL + " = ("
+ + " SELECT " + PodDBAdapter.KEY_DOWNLOAD_URL
+ + " FROM " + PodDBAdapter.TABLE_NAME_FEED_IMAGES
+ + " WHERE " + PodDBAdapter.TABLE_NAME_FEED_IMAGES + "." + PodDBAdapter.KEY_ID
+ + " = " + PodDBAdapter.TABLE_NAME_FEEDS + ".image)");
+
+ db.execSQL("DROP TABLE " + PodDBAdapter.TABLE_NAME_FEED_IMAGES);
+ }
+ if (oldVersion < 1070400) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " ADD COLUMN " + PodDBAdapter.KEY_FEED_PLAYBACK_SPEED + " REAL DEFAULT " + SPEED_USE_GLOBAL);
+ }
+ if (oldVersion < 1070401) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " ADD COLUMN " + PodDBAdapter.KEY_SORT_ORDER + " TEXT");
+ }
+ if (oldVersion < 1090000) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " ADD COLUMN " + PodDBAdapter.KEY_FEED_VOLUME_ADAPTION + " INTEGER DEFAULT 0");
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_SIMPLECHAPTERS
+ + " ADD COLUMN " + PodDBAdapter.KEY_IMAGE_URL + " TEXT DEFAULT NULL");
+ }
+ if (oldVersion < 1090001) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " ADD COLUMN " + PodDBAdapter.KEY_FEED_SKIP_INTRO + " INTEGER DEFAULT 0;");
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " ADD COLUMN " + PodDBAdapter.KEY_FEED_SKIP_ENDING + " INTEGER DEFAULT 0;");
+ }
+ if (oldVersion < 2020000) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " ADD COLUMN " + PodDBAdapter.KEY_EPISODE_NOTIFICATION + " INTEGER DEFAULT 0;");
+ }
+ if (oldVersion < 2030000) {
+ db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS
+ + " SET " + PodDBAdapter.KEY_DESCRIPTION + " = content_encoded, content_encoded = NULL "
+ + "WHERE length(" + PodDBAdapter.KEY_DESCRIPTION + ") < length(content_encoded)");
+ db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + " SET content_encoded = NULL");
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " ADD COLUMN " + PodDBAdapter.KEY_FEED_TAGS + " TEXT;");
+ }
+ if (oldVersion < 2050000) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " ADD COLUMN " + PodDBAdapter.KEY_MINIMAL_DURATION_FILTER + " INTEGER DEFAULT -1");
+ }
+ }
+
+}
diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/LongIntMap.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/LongIntMap.java
new file mode 100644
index 000000000..049b24627
--- /dev/null
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/LongIntMap.java
@@ -0,0 +1,263 @@
+package de.danoeh.antennapod.storage.database;
+
+
+import java.util.Arrays;
+
+/**
+ * Fast and memory efficient long to long map
+ */
+public class LongIntMap {
+
+ private long[] keys;
+ private int[] values;
+ private int size;
+
+ /**
+ * Creates a new LongLongMap containing no mappings.
+ */
+ public LongIntMap() {
+ this(10);
+ }
+
+ /**
+ * Creates a new SparseLongArray containing no mappings that will not
+ * require any additional memory allocation to store the specified
+ * number of mappings. If you supply an initial capacity of 0, the
+ * sparse array will be initialized with a light-weight representation
+ * not requiring any additional array allocations.
+ */
+ public LongIntMap(int initialCapacity) {
+ if(initialCapacity < 0) {
+ throw new IllegalArgumentException("initial capacity must be 0 or higher");
+ }
+ keys = new long[initialCapacity];
+ values = new int[initialCapacity];
+ size = 0;
+ }
+
+ /**
+ * Increases size of array if needed
+ */
+ private void growIfNeeded() {
+ if (size == keys.length) {
+ // Resize.
+ long[] newKeysArray = new long[size * 3 / 2 + 10];
+ int[] newValuesArray = new int[size * 3 / 2 + 10];
+ System.arraycopy(keys, 0, newKeysArray, 0, size);
+ System.arraycopy(values, 0, newValuesArray, 0, size);
+ keys = newKeysArray;
+ values = newValuesArray;
+ }
+ }
+
+ /**
+ * Gets the long mapped from the specified key, or <code>0</code>
+ * if no such mapping has been made.
+ */
+ public int get(long key) {
+ return get(key, 0);
+ }
+
+ /**
+ * Gets the long mapped from the specified key, or the specified value
+ * if no such mapping has been made.
+ */
+ public int get(long key, int valueIfKeyNotFound) {
+ int index = indexOfKey(key);
+ if(index >= 0) {
+ return values[index];
+ } else {
+ return valueIfKeyNotFound;
+ }
+ }
+
+ /**
+ * Removes the mapping from the specified key, if there was any.
+ */
+ public boolean delete(long key) {
+ int index = indexOfKey(key);
+
+ if (index >= 0) {
+ removeAt(index);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Removes the mapping at the given index.
+ */
+ private void removeAt(int index) {
+ System.arraycopy(keys, index + 1, keys, index, size - (index + 1));
+ System.arraycopy(values, index + 1, values, index, size - (index + 1));
+ size--;
+ }
+
+ /**
+ * Adds a mapping from the specified key to the specified value,
+ * replacing the previous mapping from the specified key if there
+ * was one.
+ */
+ public void put(long key, int value) {
+ int index = indexOfKey(key);
+
+ if (index >= 0) {
+ values[index] = value;
+ } else {
+ growIfNeeded();
+ keys[size] = key;
+ values[size] = value;
+ size++;
+ }
+ }
+
+ /**
+ * Returns the number of key-value mappings that this SparseIntArray
+ * currently stores.
+ */
+ public int size() {
+ return size;
+ }
+
+ /**
+ * Given an index in the range <code>0...size()-1</code>, returns
+ * the key from the <code>index</code>th key-value mapping that this
+ * SparseLongArray stores.
+ *
+ * <p>The keys corresponding to indices in ascending order are guaranteed to
+ * be in ascending order, e.g., <code>keyAt(0)</code> will return the
+ * smallest key and <code>keyAt(size()-1)</code> will return the largest
+ * key.</p>
+ */
+ private long keyAt(int index) {
+ if (index >= size) {
+ throw new IndexOutOfBoundsException("n >= size()");
+ } else if(index < 0) {
+ throw new IndexOutOfBoundsException("n < 0");
+ }
+ return keys[index];
+ }
+
+ /**
+ * Given an index in the range <code>0...size()-1</code>, returns
+ * the value from the <code>index</code>th key-value mapping that this
+ * SparseLongArray stores.
+ *
+ * <p>The values corresponding to indices in ascending order are guaranteed
+ * to be associated with keys in ascending order, e.g.,
+ * <code>valueAt(0)</code> will return the value associated with the
+ * smallest key and <code>valueAt(size()-1)</code> will return the value
+ * associated with the largest key.</p>
+ */
+ private int valueAt(int index) {
+ if (index >= size) {
+ throw new IndexOutOfBoundsException("n >= size()");
+ } else if(index < 0) {
+ throw new IndexOutOfBoundsException("n < 0");
+ }
+ return values[index];
+ }
+
+ /**
+ * Returns the index for which {@link #keyAt} would return the
+ * specified key, or a negative number if the specified
+ * key is not mapped.
+ */
+ public int indexOfKey(long key) {
+ for(int i=0; i < size; i++) {
+ if(keys[i] == key) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Returns an index for which {@link #valueAt} would return the
+ * specified key, or a negative number if no keys map to the
+ * specified value.
+ * Beware that this is a linear search, unlike lookups by key,
+ * and that multiple keys can map to the same value and this will
+ * find only one of them.
+ */
+ public int indexOfValue(long value) {
+ for (int i = 0; i < size; i++) {
+ if (values[i] == value) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Removes all key-value mappings from this SparseIntArray.
+ */
+ public void clear() {
+ keys = new long[10];
+ values = new int[10];
+ size = 0;
+ }
+
+ /**
+ * Returns a copy of the values contained in this map.
+ *
+ * @return a copy of the values contained in this map
+ */
+ public int[] values() {
+ return Arrays.copyOf(values, size);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+ if (! (other instanceof LongIntMap)) {
+ return false;
+ }
+ LongIntMap otherMap = (LongIntMap) other;
+ if (size != otherMap.size) {
+ return false;
+ }
+ for (int i = 0; i < size; i++) {
+ if (keys[i] != otherMap.keys[i] ||
+ values[i] != otherMap.values[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int hashCode = 1;
+ for (int i = 0; i < size; i++) {
+ long value = values[i];
+ hashCode = 31 * hashCode + (int)(value ^ (value >>> 32));
+ }
+ return hashCode;
+ }
+
+ @Override
+ public String toString() {
+ if (size() <= 0) {
+ return "LongLongMap{}";
+ }
+
+ StringBuilder buffer = new StringBuilder(size * 28);
+ buffer.append("LongLongMap{");
+ for (int i=0; i < size; i++) {
+ if (i > 0) {
+ buffer.append(", ");
+ }
+ long key = keyAt(i);
+ buffer.append(key);
+ buffer.append('=');
+ long value = valueAt(i);
+ buffer.append(value);
+ }
+ buffer.append('}');
+ return buffer.toString();
+ }
+}
diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java
new file mode 100644
index 000000000..7994861e8
--- /dev/null
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java
@@ -0,0 +1,1430 @@
+package de.danoeh.antennapod.storage.database;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.DatabaseErrorHandler;
+import android.database.DatabaseUtils;
+import android.database.DefaultDatabaseErrorHandler;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteDatabase.CursorFactory;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import de.danoeh.antennapod.model.feed.FeedCounter;
+import de.danoeh.antennapod.model.feed.FeedFunding;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import de.danoeh.antennapod.model.feed.Chapter;
+import de.danoeh.antennapod.model.feed.Feed;
+import de.danoeh.antennapod.model.feed.FeedItem;
+import de.danoeh.antennapod.model.feed.FeedItemFilter;
+import de.danoeh.antennapod.model.feed.FeedMedia;
+import de.danoeh.antennapod.model.feed.FeedPreferences;
+import de.danoeh.antennapod.model.download.DownloadStatus;
+import de.danoeh.antennapod.model.feed.SortOrder;
+import de.danoeh.antennapod.storage.database.mapper.FeedItemFilterQuery;
+import org.apache.commons.io.FileUtils;
+
+import static de.danoeh.antennapod.model.feed.FeedPreferences.SPEED_USE_GLOBAL;
+import static de.danoeh.antennapod.model.feed.SortOrder.toCodeString;
+
+/**
+ * Implements methods for accessing the database
+ */
+public class PodDBAdapter {
+
+ private static final String TAG = "PodDBAdapter";
+ public static final String DATABASE_NAME = "Antennapod.db";
+ public static final int VERSION = 2050000;
+
+ /**
+ * Maximum number of arguments for IN-operator.
+ */
+ private static final int IN_OPERATOR_MAXIMUM = 800;
+
+ // Key-constants
+ public static final String KEY_ID = "id";
+ public static final String KEY_TITLE = "title";
+ public static final String KEY_CUSTOM_TITLE = "custom_title";
+ public static final String KEY_LINK = "link";
+ public static final String KEY_DESCRIPTION = "description";
+ public static final String KEY_FILE_URL = "file_url";
+ public static final String KEY_DOWNLOAD_URL = "download_url";
+ public static final String KEY_PUBDATE = "pubDate";
+ public static final String KEY_READ = "read";
+ public static final String KEY_DURATION = "duration";
+ public static final String KEY_POSITION = "position";
+ public static final String KEY_SIZE = "filesize";
+ public static final String KEY_MIME_TYPE = "mime_type";
+ public static final String KEY_IMAGE_URL = "image_url";
+ public static final String KEY_FEED = "feed";
+ public static final String KEY_MEDIA = "media";
+ public static final String KEY_DOWNLOADED = "downloaded";
+ public static final String KEY_LASTUPDATE = "last_update";
+ public static final String KEY_FEEDFILE = "feedfile";
+ public static final String KEY_REASON = "reason";
+ public static final String KEY_SUCCESSFUL = "successful";
+ public static final String KEY_FEEDFILETYPE = "feedfile_type";
+ public static final String KEY_COMPLETION_DATE = "completion_date";
+ public static final String KEY_FEEDITEM = "feeditem";
+ public static final String KEY_PAYMENT_LINK = "payment_link";
+ public static final String KEY_START = "start";
+ public static final String KEY_LANGUAGE = "language";
+ public static final String KEY_AUTHOR = "author";
+ public static final String KEY_HAS_CHAPTERS = "has_simple_chapters";
+ public static final String KEY_TYPE = "type";
+ public static final String KEY_ITEM_IDENTIFIER = "item_identifier";
+ public static final String KEY_FEED_IDENTIFIER = "feed_identifier";
+ public static final String KEY_REASON_DETAILED = "reason_detailed";
+ public static final String KEY_DOWNLOADSTATUS_TITLE = "title";
+ public static final String KEY_PLAYBACK_COMPLETION_DATE = "playback_completion_date";
+ public static final String KEY_AUTO_DOWNLOAD_ATTEMPTS = "auto_download";
+ public static final String KEY_AUTO_DOWNLOAD_ENABLED = "auto_download"; // Both tables use the same key
+ public static final String KEY_KEEP_UPDATED = "keep_updated";
+ public static final String KEY_AUTO_DELETE_ACTION = "auto_delete_action";
+ public static final String KEY_FEED_VOLUME_ADAPTION = "feed_volume_adaption";
+ public static final String KEY_PLAYED_DURATION = "played_duration";
+ public static final String KEY_USERNAME = "username";
+ public static final String KEY_PASSWORD = "password";
+ public static final String KEY_IS_PAGED = "is_paged";
+ public static final String KEY_NEXT_PAGE_LINK = "next_page_link";
+ public static final String KEY_HIDE = "hide";
+ public static final String KEY_SORT_ORDER = "sort_order";
+ public static final String KEY_LAST_UPDATE_FAILED = "last_update_failed";
+ public static final String KEY_HAS_EMBEDDED_PICTURE = "has_embedded_picture";
+ public static final String KEY_LAST_PLAYED_TIME = "last_played_time";
+ public static final String KEY_INCLUDE_FILTER = "include_filter";
+ public static final String KEY_EXCLUDE_FILTER = "exclude_filter";
+ public static final String KEY_MINIMAL_DURATION_FILTER = "minimal_duration_filter";
+ public static final String KEY_FEED_PLAYBACK_SPEED = "feed_playback_speed";
+ public static final String KEY_FEED_SKIP_INTRO = "feed_skip_intro";
+ public static final String KEY_FEED_SKIP_ENDING = "feed_skip_ending";
+ public static final String KEY_FEED_TAGS = "tags";
+ public static final String KEY_EPISODE_NOTIFICATION = "episode_notification";
+
+ // Table names
+ public static final String TABLE_NAME_FEEDS = "Feeds";
+ public static final String TABLE_NAME_FEED_ITEMS = "FeedItems";
+ public static final String TABLE_NAME_FEED_IMAGES = "FeedImages";
+ public static final String TABLE_NAME_FEED_MEDIA = "FeedMedia";
+ public static final String TABLE_NAME_DOWNLOAD_LOG = "DownloadLog";
+ public static final String TABLE_NAME_QUEUE = "Queue";
+ public static final String TABLE_NAME_SIMPLECHAPTERS = "SimpleChapters";
+ public static final String TABLE_NAME_FAVORITES = "Favorites";
+
+ // SQL Statements for creating new tables
+ private static final String TABLE_PRIMARY_KEY = KEY_ID
+ + " INTEGER PRIMARY KEY AUTOINCREMENT ,";
+
+ private static final String CREATE_TABLE_FEEDS = "CREATE TABLE "
+ + TABLE_NAME_FEEDS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE
+ + " TEXT," + KEY_CUSTOM_TITLE + " TEXT," + KEY_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL + " TEXT,"
+ + KEY_DOWNLOADED + " INTEGER," + KEY_LINK + " TEXT,"
+ + KEY_DESCRIPTION + " TEXT," + KEY_PAYMENT_LINK + " TEXT,"
+ + KEY_LASTUPDATE + " TEXT," + KEY_LANGUAGE + " TEXT," + KEY_AUTHOR
+ + " TEXT," + KEY_IMAGE_URL + " TEXT," + KEY_TYPE + " TEXT,"
+ + KEY_FEED_IDENTIFIER + " TEXT," + KEY_AUTO_DOWNLOAD_ENABLED + " INTEGER DEFAULT 1,"
+ + KEY_USERNAME + " TEXT,"
+ + KEY_PASSWORD + " TEXT,"
+ + KEY_INCLUDE_FILTER + " TEXT DEFAULT '',"
+ + KEY_EXCLUDE_FILTER + " TEXT DEFAULT '',"
+ + KEY_MINIMAL_DURATION_FILTER + " INTEGER DEFAULT -1,"
+ + KEY_KEEP_UPDATED + " INTEGER DEFAULT 1,"
+ + KEY_IS_PAGED + " INTEGER DEFAULT 0,"
+ + KEY_NEXT_PAGE_LINK + " TEXT,"
+ + KEY_HIDE + " TEXT,"
+ + KEY_SORT_ORDER + " TEXT,"
+ + KEY_LAST_UPDATE_FAILED + " INTEGER DEFAULT 0,"
+ + KEY_AUTO_DELETE_ACTION + " INTEGER DEFAULT 0,"
+ + KEY_FEED_PLAYBACK_SPEED + " REAL DEFAULT " + SPEED_USE_GLOBAL + ","
+ + KEY_FEED_VOLUME_ADAPTION + " INTEGER DEFAULT 0,"
+ + KEY_FEED_TAGS + " TEXT,"
+ + KEY_FEED_SKIP_INTRO + " INTEGER DEFAULT 0,"
+ + KEY_FEED_SKIP_ENDING + " INTEGER DEFAULT 0,"
+ + KEY_EPISODE_NOTIFICATION + " INTEGER DEFAULT 0)";
+
+ private static final String CREATE_TABLE_FEED_ITEMS = "CREATE TABLE "
+ + TABLE_NAME_FEED_ITEMS + " (" + TABLE_PRIMARY_KEY
+ + KEY_TITLE + " TEXT," + KEY_PUBDATE + " INTEGER,"
+ + KEY_READ + " INTEGER," + KEY_LINK + " TEXT,"
+ + KEY_DESCRIPTION + " TEXT," + KEY_PAYMENT_LINK + " TEXT,"
+ + KEY_MEDIA + " INTEGER," + KEY_FEED + " INTEGER,"
+ + KEY_HAS_CHAPTERS + " INTEGER," + KEY_ITEM_IDENTIFIER + " TEXT,"
+ + KEY_IMAGE_URL + " TEXT,"
+ + KEY_AUTO_DOWNLOAD_ATTEMPTS + " INTEGER)";
+
+ private static final String CREATE_TABLE_FEED_MEDIA = "CREATE TABLE "
+ + TABLE_NAME_FEED_MEDIA + " (" + TABLE_PRIMARY_KEY + KEY_DURATION
+ + " INTEGER," + KEY_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL
+ + " TEXT," + KEY_DOWNLOADED + " INTEGER," + KEY_POSITION
+ + " INTEGER," + KEY_SIZE + " INTEGER," + KEY_MIME_TYPE + " TEXT,"
+ + KEY_PLAYBACK_COMPLETION_DATE + " INTEGER,"
+ + KEY_FEEDITEM + " INTEGER,"
+ + KEY_PLAYED_DURATION + " INTEGER,"
+ + KEY_HAS_EMBEDDED_PICTURE + " INTEGER,"
+ + KEY_LAST_PLAYED_TIME + " INTEGER" + ")";
+
+ private static final String CREATE_TABLE_DOWNLOAD_LOG = "CREATE TABLE "
+ + TABLE_NAME_DOWNLOAD_LOG + " (" + TABLE_PRIMARY_KEY + KEY_FEEDFILE
+ + " INTEGER," + KEY_FEEDFILETYPE + " INTEGER," + KEY_REASON
+ + " INTEGER," + KEY_SUCCESSFUL + " INTEGER," + KEY_COMPLETION_DATE
+ + " INTEGER," + KEY_REASON_DETAILED + " TEXT,"
+ + KEY_DOWNLOADSTATUS_TITLE + " TEXT)";
+
+ private static final String CREATE_TABLE_QUEUE = "CREATE TABLE "
+ + TABLE_NAME_QUEUE + "(" + KEY_ID + " INTEGER PRIMARY KEY,"
+ + KEY_FEEDITEM + " INTEGER," + KEY_FEED + " INTEGER)";
+
+ private static final String CREATE_TABLE_SIMPLECHAPTERS = "CREATE TABLE "
+ + TABLE_NAME_SIMPLECHAPTERS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE
+ + " TEXT," + KEY_START + " INTEGER," + KEY_FEEDITEM + " INTEGER,"
+ + KEY_LINK + " TEXT," + KEY_IMAGE_URL + " TEXT)";
+
+ // SQL Statements for creating indexes
+ static final String CREATE_INDEX_FEEDITEMS_FEED = "CREATE INDEX "
+ + TABLE_NAME_FEED_ITEMS + "_" + KEY_FEED + " ON " + TABLE_NAME_FEED_ITEMS + " ("
+ + KEY_FEED + ")";
+
+ static final String CREATE_INDEX_FEEDITEMS_PUBDATE = "CREATE INDEX "
+ + TABLE_NAME_FEED_ITEMS + "_" + KEY_PUBDATE + " ON " + TABLE_NAME_FEED_ITEMS + " ("
+ + KEY_PUBDATE + ")";
+
+ static final String CREATE_INDEX_FEEDITEMS_READ = "CREATE INDEX "
+ + TABLE_NAME_FEED_ITEMS + "_" + KEY_READ + " ON " + TABLE_NAME_FEED_ITEMS + " ("
+ + KEY_READ + ")";
+
+ static final String CREATE_INDEX_QUEUE_FEEDITEM = "CREATE INDEX "
+ + TABLE_NAME_QUEUE + "_" + KEY_FEEDITEM + " ON " + TABLE_NAME_QUEUE + " ("
+ + KEY_FEEDITEM + ")";
+
+ static final String CREATE_INDEX_FEEDMEDIA_FEEDITEM = "CREATE INDEX "
+ + TABLE_NAME_FEED_MEDIA + "_" + KEY_FEEDITEM + " ON " + TABLE_NAME_FEED_MEDIA + " ("
+ + KEY_FEEDITEM + ")";
+
+ static final String CREATE_INDEX_SIMPLECHAPTERS_FEEDITEM = "CREATE INDEX "
+ + TABLE_NAME_SIMPLECHAPTERS + "_" + KEY_FEEDITEM + " ON " + TABLE_NAME_SIMPLECHAPTERS + " ("
+ + KEY_FEEDITEM + ")";
+
+ static final String CREATE_TABLE_FAVORITES = "CREATE TABLE "
+ + TABLE_NAME_FAVORITES + "(" + KEY_ID + " INTEGER PRIMARY KEY,"
+ + KEY_FEEDITEM + " INTEGER," + KEY_FEED + " INTEGER)";
+
+ /**
+ * Select all columns from the feed-table
+ */
+ private static final String[] FEED_SEL_STD = {
+ TABLE_NAME_FEEDS + "." + KEY_ID,
+ TABLE_NAME_FEEDS + "." + KEY_TITLE,
+ TABLE_NAME_FEEDS + "." + KEY_CUSTOM_TITLE,
+ TABLE_NAME_FEEDS + "." + KEY_FILE_URL,
+ TABLE_NAME_FEEDS + "." + KEY_DOWNLOAD_URL,
+ TABLE_NAME_FEEDS + "." + KEY_DOWNLOADED,
+ TABLE_NAME_FEEDS + "." + KEY_LINK,
+ TABLE_NAME_FEEDS + "." + KEY_DESCRIPTION,
+ TABLE_NAME_FEEDS + "." + KEY_PAYMENT_LINK,
+ TABLE_NAME_FEEDS + "." + KEY_LASTUPDATE,
+ TABLE_NAME_FEEDS + "." + KEY_LANGUAGE,
+ TABLE_NAME_FEEDS + "." + KEY_AUTHOR,
+ TABLE_NAME_FEEDS + "." + KEY_IMAGE_URL,
+ TABLE_NAME_FEEDS + "." + KEY_TYPE,
+ TABLE_NAME_FEEDS + "." + KEY_FEED_IDENTIFIER,
+ TABLE_NAME_FEEDS + "." + KEY_AUTO_DOWNLOAD_ENABLED,
+ TABLE_NAME_FEEDS + "." + KEY_KEEP_UPDATED,
+ TABLE_NAME_FEEDS + "." + KEY_IS_PAGED,
+ TABLE_NAME_FEEDS + "." + KEY_NEXT_PAGE_LINK,
+ TABLE_NAME_FEEDS + "." + KEY_USERNAME,
+ TABLE_NAME_FEEDS + "." + KEY_PASSWORD,
+ TABLE_NAME_FEEDS + "." + KEY_HIDE,
+ TABLE_NAME_FEEDS + "." + KEY_SORT_ORDER,
+ TABLE_NAME_FEEDS + "." + KEY_LAST_UPDATE_FAILED,
+ TABLE_NAME_FEEDS + "." + KEY_AUTO_DELETE_ACTION,
+ TABLE_NAME_FEEDS + "." + KEY_FEED_VOLUME_ADAPTION,
+ TABLE_NAME_FEEDS + "." + KEY_INCLUDE_FILTER,
+ TABLE_NAME_FEEDS + "." + KEY_EXCLUDE_FILTER,
+ TABLE_NAME_FEEDS + "." + KEY_MINIMAL_DURATION_FILTER,
+ TABLE_NAME_FEEDS + "." + KEY_FEED_PLAYBACK_SPEED,
+ TABLE_NAME_FEEDS + "." + KEY_FEED_TAGS,
+ TABLE_NAME_FEEDS + "." + KEY_FEED_SKIP_INTRO,
+ TABLE_NAME_FEEDS + "." + KEY_FEED_SKIP_ENDING,
+ TABLE_NAME_FEEDS + "." + KEY_EPISODE_NOTIFICATION
+ };
+
+ /**
+ * All the tables in the database
+ */
+ private static final String[] ALL_TABLES = {
+ TABLE_NAME_FEEDS,
+ TABLE_NAME_FEED_ITEMS,
+ TABLE_NAME_FEED_MEDIA,
+ TABLE_NAME_DOWNLOAD_LOG,
+ TABLE_NAME_QUEUE,
+ TABLE_NAME_SIMPLECHAPTERS,
+ TABLE_NAME_FAVORITES
+ };
+
+ public static final String SELECT_KEY_ITEM_ID = "item_id";
+ public static final String SELECT_KEY_MEDIA_ID = "media_id";
+
+ private static final String KEYS_FEED_ITEM_WITHOUT_DESCRIPTION =
+ TABLE_NAME_FEED_ITEMS + "." + KEY_ID + " AS " + SELECT_KEY_ITEM_ID + ", "
+ + TABLE_NAME_FEED_ITEMS + "." + KEY_TITLE + ", "
+ + TABLE_NAME_FEED_ITEMS + "." + KEY_PUBDATE + ", "
+ + TABLE_NAME_FEED_ITEMS + "." + KEY_READ + ", "
+ + TABLE_NAME_FEED_ITEMS + "." + KEY_LINK + ", "
+ + TABLE_NAME_FEED_ITEMS + "." + KEY_PAYMENT_LINK + ", "
+ + TABLE_NAME_FEED_ITEMS + "." + KEY_MEDIA + ", "
+ + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + ", "
+ + TABLE_NAME_FEED_ITEMS + "." + KEY_HAS_CHAPTERS + ", "
+ + TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER + ", "
+ + TABLE_NAME_FEED_ITEMS + "." + KEY_IMAGE_URL + ", "
+ + TABLE_NAME_FEED_ITEMS + "." + KEY_AUTO_DOWNLOAD_ATTEMPTS;
+
+ private static final String KEYS_FEED_MEDIA =
+ TABLE_NAME_FEED_MEDIA + "." + KEY_ID + " AS " + SELECT_KEY_MEDIA_ID + ", "
+ + TABLE_NAME_FEED_MEDIA + "." + KEY_DURATION + ", "
+ + TABLE_NAME_FEED_MEDIA + "." + KEY_FILE_URL + ", "
+ + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOAD_URL + ", "
+ + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + ", "
+ + TABLE_NAME_FEED_MEDIA + "." + KEY_POSITION + ", "
+ + TABLE_NAME_FEED_MEDIA + "." + KEY_SIZE + ", "
+ + TABLE_NAME_FEED_MEDIA + "." + KEY_MIME_TYPE + ", "
+ + TABLE_NAME_FEED_MEDIA + "." + KEY_PLAYBACK_COMPLETION_DATE + ", "
+ + TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM + ", "
+ + TABLE_NAME_FEED_MEDIA + "." + KEY_PLAYED_DURATION + ", "
+ + TABLE_NAME_FEED_MEDIA + "." + KEY_HAS_EMBEDDED_PICTURE + ", "
+ + TABLE_NAME_FEED_MEDIA + "." + KEY_LAST_PLAYED_TIME;
+
+ private static final String JOIN_FEED_ITEM_AND_MEDIA = " LEFT JOIN " + TABLE_NAME_FEED_MEDIA
+ + " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" + TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM + " ";
+
+ private static final String SELECT_FEED_ITEMS_AND_MEDIA_WITH_DESCRIPTION =
+ "SELECT " + KEYS_FEED_ITEM_WITHOUT_DESCRIPTION + ", " + KEYS_FEED_MEDIA + ", "
+ + TABLE_NAME_FEED_ITEMS + "." + KEY_DESCRIPTION
+ + " FROM " + TABLE_NAME_FEED_ITEMS
+ + JOIN_FEED_ITEM_AND_MEDIA;
+ private static final String SELECT_FEED_ITEMS_AND_MEDIA =
+ "SELECT " + KEYS_FEED_ITEM_WITHOUT_DESCRIPTION + ", " + KEYS_FEED_MEDIA
+ + " FROM " + TABLE_NAME_FEED_ITEMS
+ + JOIN_FEED_ITEM_AND_MEDIA;
+
+ private static Context context;
+ private static PodDBAdapter instance;
+
+ private final SQLiteDatabase db;
+ private final PodDBHelper dbHelper;
+
+ public static void init(Context context) {
+ PodDBAdapter.context = context.getApplicationContext();
+ }
+
+ public static synchronized PodDBAdapter getInstance() {
+ if (instance == null) {
+ instance = new PodDBAdapter();
+ }
+ return instance;
+ }
+
+ private PodDBAdapter() {
+ dbHelper = new PodDBHelper(PodDBAdapter.context, DATABASE_NAME, null);
+ db = openDb();
+ }
+
+ private SQLiteDatabase openDb() {
+ SQLiteDatabase newDb;
+ try {
+ newDb = dbHelper.getWritableDatabase();
+ newDb.disableWriteAheadLogging();
+ } catch (SQLException ex) {
+ Log.e(TAG, Log.getStackTraceString(ex));
+ newDb = dbHelper.getReadableDatabase();
+ }
+ return newDb;
+ }
+
+ public synchronized PodDBAdapter open() {
+ // do nothing
+ return this;
+ }
+
+ public synchronized void close() {
+ // do nothing
+ }
+
+ /**
+ * <p>Resets all database connections to ensure new database connections for
+ * the next test case. Call method only for unit tests.</p>
+ *
+ * <p>That's a workaround for a Robolectric issue in ShadowSQLiteConnection
+ * that leads to an error <tt>IllegalStateException: Illegal connection
+ * pointer</tt> if several threads try to use the same database connection.
+ * For more information see
+ * <a href="https://github.com/robolectric/robolectric/issues/1890">robolectric/robolectric#1890</a>.</p>
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ public static void tearDownTests() {
+ getInstance().dbHelper.close();
+ instance = null;
+ }
+
+ public static boolean deleteDatabase() {
+ PodDBAdapter adapter = getInstance();
+ adapter.open();
+ try {
+ for (String tableName : ALL_TABLES) {
+ adapter.db.delete(tableName, "1", null);
+ }
+ return true;
+ } finally {
+ adapter.close();
+ }
+ }
+
+ /**
+ * Inserts or updates a feed entry
+ *
+ * @return the id of the entry
+ */
+ private long setFeed(Feed feed) {
+ ContentValues values = new ContentValues();
+ values.put(KEY_TITLE, feed.getFeedTitle());
+ values.put(KEY_LINK, feed.getLink());
+ values.put(KEY_DESCRIPTION, feed.getDescription());
+ values.put(KEY_PAYMENT_LINK, FeedFunding.getPaymentLinksAsString(feed.getPaymentLinks()));
+ values.put(KEY_AUTHOR, feed.getAuthor());
+ values.put(KEY_LANGUAGE, feed.getLanguage());
+ values.put(KEY_IMAGE_URL, feed.getImageUrl());
+
+ values.put(KEY_FILE_URL, feed.getFile_url());
+ values.put(KEY_DOWNLOAD_URL, feed.getDownload_url());
+ values.put(KEY_DOWNLOADED, feed.isDownloaded());
+ values.put(KEY_LASTUPDATE, feed.getLastUpdate());
+ values.put(KEY_TYPE, feed.getType());
+ values.put(KEY_FEED_IDENTIFIER, feed.getFeedIdentifier());
+
+ values.put(KEY_IS_PAGED, feed.isPaged());
+ values.put(KEY_NEXT_PAGE_LINK, feed.getNextPageLink());
+ if (feed.getItemFilter() != null && feed.getItemFilter().getValues().length > 0) {
+ values.put(KEY_HIDE, TextUtils.join(",", feed.getItemFilter().getValues()));
+ } else {
+ values.put(KEY_HIDE, "");
+ }
+ values.put(KEY_SORT_ORDER, toCodeString(feed.getSortOrder()));
+ values.put(KEY_LAST_UPDATE_FAILED, feed.hasLastUpdateFailed());
+ if (feed.getId() == 0) {
+ // Create new entry
+ Log.d(this.toString(), "Inserting new Feed into db");
+ feed.setId(db.insert(TABLE_NAME_FEEDS, null, values));
+ } else {
+ Log.d(this.toString(), "Updating existing Feed in db");
+ db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?",
+ new String[]{String.valueOf(feed.getId())});
+ }
+ return feed.getId();
+ }
+
+ public void setFeedPreferences(FeedPreferences prefs) {
+ if (prefs.getFeedID() == 0) {
+ throw new IllegalArgumentException("Feed ID of preference must not be null");
+ }
+ ContentValues values = new ContentValues();
+ values.put(KEY_AUTO_DOWNLOAD_ENABLED, prefs.getAutoDownload());
+ values.put(KEY_KEEP_UPDATED, prefs.getKeepUpdated());
+ values.put(KEY_AUTO_DELETE_ACTION, prefs.getAutoDeleteAction().ordinal());
+ values.put(KEY_FEED_VOLUME_ADAPTION, prefs.getVolumeAdaptionSetting().toInteger());
+ values.put(KEY_USERNAME, prefs.getUsername());
+ values.put(KEY_PASSWORD, prefs.getPassword());
+ values.put(KEY_INCLUDE_FILTER, prefs.getFilter().getIncludeFilter());
+ values.put(KEY_EXCLUDE_FILTER, prefs.getFilter().getExcludeFilter());
+ values.put(KEY_MINIMAL_DURATION_FILTER, prefs.getFilter().getMinimalDurationFilter());
+ values.put(KEY_FEED_PLAYBACK_SPEED, prefs.getFeedPlaybackSpeed());
+ values.put(KEY_FEED_TAGS, prefs.getTagsAsString());
+ values.put(KEY_FEED_SKIP_INTRO, prefs.getFeedSkipIntro());
+ values.put(KEY_FEED_SKIP_ENDING, prefs.getFeedSkipEnding());
+ values.put(KEY_EPISODE_NOTIFICATION, prefs.getShowEpisodeNotification());
+ db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(prefs.getFeedID())});
+ }
+
+ public void setFeedItemFilter(long feedId, Set<String> filterValues) {
+ String valuesList = TextUtils.join(",", filterValues);
+ Log.d(TAG, String.format(Locale.US,
+ "setFeedItemFilter() called with: feedId = [%d], filterValues = [%s]", feedId, valuesList));
+ ContentValues values = new ContentValues();
+ values.put(KEY_HIDE, valuesList);
+ db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(feedId)});
+ }
+
+ public void setFeedItemSortOrder(long feedId, @Nullable SortOrder sortOrder) {
+ ContentValues values = new ContentValues();
+ values.put(KEY_SORT_ORDER, toCodeString(sortOrder));
+ db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(feedId)});
+ }
+
+ /**
+ * Inserts or updates a media entry
+ *
+ * @return the id of the entry
+ */
+ public long setMedia(FeedMedia media) {
+ ContentValues values = new ContentValues();
+ values.put(KEY_DURATION, media.getDuration());
+ values.put(KEY_POSITION, media.getPosition());
+ values.put(KEY_SIZE, media.getSize());
+ values.put(KEY_MIME_TYPE, media.getMime_type());
+ values.put(KEY_DOWNLOAD_URL, media.getDownload_url());
+ values.put(KEY_DOWNLOADED, media.isDownloaded());
+ values.put(KEY_FILE_URL, media.getFile_url());
+ values.put(KEY_HAS_EMBEDDED_PICTURE, media.hasEmbeddedPicture());
+ values.put(KEY_LAST_PLAYED_TIME, media.getLastPlayedTime());
+
+ if (media.getPlaybackCompletionDate() != null) {
+ values.put(KEY_PLAYBACK_COMPLETION_DATE, media.getPlaybackCompletionDate().getTime());
+ } else {
+ values.put(KEY_PLAYBACK_COMPLETION_DATE, 0);
+ }
+ if (media.getItem() != null) {
+ values.put(KEY_FEEDITEM, media.getItem().getId());
+ }
+ if (media.getId() == 0) {
+ media.setId(db.insert(TABLE_NAME_FEED_MEDIA, null, values));
+ } else {
+ db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?",
+ new String[]{String.valueOf(media.getId())});
+ }
+ return media.getId();
+ }
+
+ public void setFeedMediaPlaybackInformation(FeedMedia media) {
+ if (media.getId() != 0) {
+ ContentValues values = new ContentValues();
+ values.put(KEY_POSITION, media.getPosition());
+ values.put(KEY_DURATION, media.getDuration());
+ values.put(KEY_PLAYED_DURATION, media.getPlayedDuration());
+ values.put(KEY_LAST_PLAYED_TIME, media.getLastPlayedTime());
+ db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?",
+ new String[]{String.valueOf(media.getId())});
+ } else {
+ Log.e(TAG, "setFeedMediaPlaybackInformation: ID of media was 0");
+ }
+ }
+
+ public void setFeedMediaPlaybackCompletionDate(FeedMedia media) {
+ if (media.getId() != 0) {
+ ContentValues values = new ContentValues();
+ values.put(KEY_PLAYBACK_COMPLETION_DATE, media.getPlaybackCompletionDate().getTime());
+ values.put(KEY_PLAYED_DURATION, media.getPlayedDuration());
+ db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?",
+ new String[]{String.valueOf(media.getId())});
+ } else {
+ Log.e(TAG, "setFeedMediaPlaybackCompletionDate: ID of media was 0");
+ }
+ }
+
+ public void resetAllMediaPlayedDuration() {
+ try {
+ db.beginTransactionNonExclusive();
+ ContentValues values = new ContentValues();
+ values.put(KEY_PLAYED_DURATION, 0);
+ db.update(TABLE_NAME_FEED_MEDIA, values, null, new String[0]);
+ db.setTransactionSuccessful();
+ } catch (SQLException e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * Insert all FeedItems of a feed and the feed object itself in a single
+ * transaction
+ */
+ public void setCompleteFeed(Feed... feeds) {
+ try {
+ db.beginTransactionNonExclusive();
+ for (Feed feed : feeds) {
+ setFeed(feed);
+ if (feed.getItems() != null) {
+ for (FeedItem item : feed.getItems()) {
+ updateOrInsertFeedItem(item, false);
+ }
+ }
+ if (feed.getPreferences() != null) {
+ setFeedPreferences(feed.getPreferences());
+ }
+ }
+ db.setTransactionSuccessful();
+ } catch (SQLException e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * Updates the download URL of a Feed.
+ */
+ public void setFeedDownloadUrl(String original, String updated) {
+ ContentValues values = new ContentValues();
+ values.put(KEY_DOWNLOAD_URL, updated);
+ db.update(TABLE_NAME_FEEDS, values, KEY_DOWNLOAD_URL + "=?", new String[]{original});
+ }
+
+ public void storeFeedItemlist(List<FeedItem> items) {
+ try {
+ db.beginTransactionNonExclusive();
+ for (FeedItem item : items) {
+ updateOrInsertFeedItem(item, true);
+ }
+ db.setTransactionSuccessful();
+ } catch (SQLException e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ public long setSingleFeedItem(FeedItem item) {
+ long result = 0;
+ try {
+ db.beginTransactionNonExclusive();
+ result = updateOrInsertFeedItem(item, true);
+ db.setTransactionSuccessful();
+ } catch (SQLException e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ } finally {
+ db.endTransaction();
+ }
+ return result;
+ }
+
+ /**
+ * Inserts or updates a feeditem entry
+ *
+ * @param item The FeedItem
+ * @param saveFeed true if the Feed of the item should also be saved. This should be set to
+ * false if the method is executed on a list of FeedItems of the same Feed.
+ * @return the id of the entry
+ */
+ private long updateOrInsertFeedItem(FeedItem item, boolean saveFeed) {
+ if (item.getId() == 0 && item.getPubDate() == null) {
+ Log.e(TAG, "Newly saved item has no pubDate. Using current date as pubDate");
+ item.setPubDate(new Date());
+ }
+
+ ContentValues values = new ContentValues();
+ values.put(KEY_TITLE, item.getTitle());
+ values.put(KEY_LINK, item.getLink());
+ if (item.getDescription() != null) {
+ values.put(KEY_DESCRIPTION, item.getDescription());
+ }
+ values.put(KEY_PUBDATE, item.getPubDate().getTime());
+ values.put(KEY_PAYMENT_LINK, item.getPaymentLink());
+ if (saveFeed && item.getFeed() != null) {
+ setFeed(item.getFeed());
+ }
+ values.put(KEY_FEED, item.getFeed().getId());
+ if (item.isNew()) {
+ values.put(KEY_READ, FeedItem.NEW);
+ } else if (item.isPlayed()) {
+ values.put(KEY_READ, FeedItem.PLAYED);
+ } else {
+ values.put(KEY_READ, FeedItem.UNPLAYED);
+ }
+ values.put(KEY_HAS_CHAPTERS, item.getChapters() != null || item.hasChapters());
+ values.put(KEY_ITEM_IDENTIFIER, item.getItemIdentifier());
+ values.put(KEY_AUTO_DOWNLOAD_ATTEMPTS, item.getAutoDownloadAttemptsAndTime());
+ values.put(KEY_IMAGE_URL, item.getImageUrl());
+
+ if (item.getId() == 0) {
+ item.setId(db.insert(TABLE_NAME_FEED_ITEMS, null, values));
+ } else {
+ db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?",
+ new String[]{String.valueOf(item.getId())});
+ }
+ if (item.getMedia() != null) {
+ setMedia(item.getMedia());
+ }
+ if (item.getChapters() != null) {
+ setChapters(item);
+ }
+ return item.getId();
+ }
+
+ public void setFeedItemRead(int played, long itemId, long mediaId,
+ boolean resetMediaPosition) {
+ try {
+ db.beginTransactionNonExclusive();
+ ContentValues values = new ContentValues();
+
+ values.put(KEY_READ, played);
+ db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", new String[]{String.valueOf(itemId)});
+
+ if (resetMediaPosition) {
+ values.clear();
+ values.put(KEY_POSITION, 0);
+ db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?", new String[]{String.valueOf(mediaId)});
+ }
+
+ db.setTransactionSuccessful();
+ } catch (SQLException e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * Sets the 'read' attribute of the item.
+ *
+ * @param read must be one of FeedItem.PLAYED, FeedItem.NEW, FeedItem.UNPLAYED
+ * @param itemIds items to change the value of
+ */
+ public void setFeedItemRead(int read, long... itemIds) {
+ try {
+ db.beginTransactionNonExclusive();
+ ContentValues values = new ContentValues();
+ for (long id : itemIds) {
+ values.clear();
+ values.put(KEY_READ, read);
+ db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", new String[]{String.valueOf(id)});
+ }
+ db.setTransactionSuccessful();
+ } catch (SQLException e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ private void setChapters(FeedItem item) {
+ ContentValues values = new ContentValues();
+ for (Chapter chapter : item.getChapters()) {
+ values.put(KEY_TITLE, chapter.getTitle());
+ values.put(KEY_START, chapter.getStart());
+ values.put(KEY_FEEDITEM, item.getId());
+ values.put(KEY_LINK, chapter.getLink());
+ values.put(KEY_IMAGE_URL, chapter.getImageUrl());
+ if (chapter.getId() == 0) {
+ chapter.setId(db.insert(TABLE_NAME_SIMPLECHAPTERS, null, values));
+ } else {
+ db.update(TABLE_NAME_SIMPLECHAPTERS, values, KEY_ID + "=?",
+ new String[]{String.valueOf(chapter.getId())});
+ }
+ }
+ }
+
+ public void setFeedLastUpdateFailed(long feedId, boolean failed) {
+ final String sql = "UPDATE " + TABLE_NAME_FEEDS
+ + " SET " + KEY_LAST_UPDATE_FAILED + "=" + (failed ? "1" : "0")
+ + " WHERE " + KEY_ID + "=" + feedId;
+ db.execSQL(sql);
+ }
+
+ public void setFeedCustomTitle(long feedId, String customTitle) {
+ ContentValues values = new ContentValues();
+ values.put(KEY_CUSTOM_TITLE, customTitle);
+ db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(feedId)});
+ }
+
+ /**
+ * Inserts or updates a download status.
+ */
+ public long setDownloadStatus(DownloadStatus status) {
+ ContentValues values = new ContentValues();
+ values.put(KEY_FEEDFILE, status.getFeedfileId());
+ values.put(KEY_FEEDFILETYPE, status.getFeedfileType());
+ values.put(KEY_REASON, status.getReason().getCode());
+ values.put(KEY_SUCCESSFUL, status.isSuccessful());
+ values.put(KEY_COMPLETION_DATE, status.getCompletionDate().getTime());
+ values.put(KEY_REASON_DETAILED, status.getReasonDetailed());
+ values.put(KEY_DOWNLOADSTATUS_TITLE, status.getTitle());
+ if (status.getId() == 0) {
+ status.setId(db.insert(TABLE_NAME_DOWNLOAD_LOG, null, values));
+ } else {
+ db.update(TABLE_NAME_DOWNLOAD_LOG, values, KEY_ID + "=?",
+ new String[]{String.valueOf(status.getId())});
+ }
+ return status.getId();
+ }
+
+ public void setFavorites(List<FeedItem> favorites) {
+ ContentValues values = new ContentValues();
+ try {
+ db.beginTransactionNonExclusive();
+ db.delete(TABLE_NAME_FAVORITES, null, null);
+ for (int i = 0; i < favorites.size(); i++) {
+ FeedItem item = favorites.get(i);
+ values.put(KEY_ID, i);
+ values.put(KEY_FEEDITEM, item.getId());
+ values.put(KEY_FEED, item.getFeed().getId());
+ db.insertWithOnConflict(TABLE_NAME_FAVORITES, null, values, SQLiteDatabase.CONFLICT_REPLACE);
+ }
+ db.setTransactionSuccessful();
+ } catch (SQLException e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * Adds the item to favorites
+ */
+ public void addFavoriteItem(FeedItem item) {
+ // don't add an item that's already there...
+ if (isItemInFavorites(item)) {
+ Log.d(TAG, "item already in favorites");
+ return;
+ }
+ ContentValues values = new ContentValues();
+ values.put(KEY_FEEDITEM, item.getId());
+ values.put(KEY_FEED, item.getFeedId());
+ db.insert(TABLE_NAME_FAVORITES, null, values);
+ }
+
+ public void removeFavoriteItem(FeedItem item) {
+ String deleteClause = String.format("DELETE FROM %s WHERE %s=%s AND %s=%s",
+ TABLE_NAME_FAVORITES,
+ KEY_FEEDITEM, item.getId(),
+ KEY_FEED, item.getFeedId());
+ db.execSQL(deleteClause);
+ }
+
+ private boolean isItemInFavorites(FeedItem item) {
+ String query = String.format(Locale.US, "SELECT %s from %s WHERE %s=%d",
+ KEY_ID, TABLE_NAME_FAVORITES, KEY_FEEDITEM, item.getId());
+ Cursor c = db.rawQuery(query, null);
+ int count = c.getCount();
+ c.close();
+ return count > 0;
+ }
+
+ public void setQueue(List<FeedItem> queue) {
+ ContentValues values = new ContentValues();
+ try {
+ db.beginTransactionNonExclusive();
+ db.delete(TABLE_NAME_QUEUE, null, null);
+ for (int i = 0; i < queue.size(); i++) {
+ FeedItem item = queue.get(i);
+ values.put(KEY_ID, i);
+ values.put(KEY_FEEDITEM, item.getId());
+ values.put(KEY_FEED, item.getFeed().getId());
+ db.insertWithOnConflict(TABLE_NAME_QUEUE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
+ }
+ db.setTransactionSuccessful();
+ } catch (SQLException e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ public void clearQueue() {
+ db.delete(TABLE_NAME_QUEUE, null, null);
+ }
+
+ /**
+ * Remove the listed items and their FeedMedia entries.
+ */
+ public void removeFeedItems(@NonNull List<FeedItem> items) {
+ try {
+ StringBuilder mediaIds = new StringBuilder();
+ StringBuilder itemIds = new StringBuilder();
+ for (FeedItem item : items) {
+ if (item.getMedia() != null) {
+ if (mediaIds.length() != 0) {
+ mediaIds.append(",");
+ }
+ mediaIds.append(item.getMedia().getId());
+ }
+ if (itemIds.length() != 0) {
+ itemIds.append(",");
+ }
+ itemIds.append(item.getId());
+ }
+
+ db.beginTransactionNonExclusive();
+ db.delete(TABLE_NAME_SIMPLECHAPTERS, KEY_FEEDITEM + " IN (" + itemIds + ")", null);
+ db.delete(TABLE_NAME_DOWNLOAD_LOG, KEY_FEEDFILETYPE + "=" + FeedMedia.FEEDFILETYPE_FEEDMEDIA
+ + " AND " + KEY_FEEDFILE + " IN (" + mediaIds + ")", null);
+ db.delete(TABLE_NAME_FEED_MEDIA, KEY_ID + " IN (" + mediaIds + ")", null);
+ db.delete(TABLE_NAME_FEED_ITEMS, KEY_ID + " IN (" + itemIds + ")", null);
+ db.setTransactionSuccessful();
+ } catch (SQLException e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * Remove a feed with all its FeedItems and Media entries.
+ */
+ public void removeFeed(Feed feed) {
+ try {
+ db.beginTransactionNonExclusive();
+ if (feed.getItems() != null) {
+ removeFeedItems(feed.getItems());
+ }
+ // delete download log entries for feed
+ db.delete(TABLE_NAME_DOWNLOAD_LOG, KEY_FEEDFILE + "=? AND " + KEY_FEEDFILETYPE + "=?",
+ new String[]{String.valueOf(feed.getId()), String.valueOf(Feed.FEEDFILETYPE_FEED)});
+
+ db.delete(TABLE_NAME_FEEDS, KEY_ID + "=?",
+ new String[]{String.valueOf(feed.getId())});
+ db.setTransactionSuccessful();
+ } catch (SQLException e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ public void clearPlaybackHistory() {
+ ContentValues values = new ContentValues();
+ values.put(KEY_PLAYBACK_COMPLETION_DATE, 0);
+ db.update(TABLE_NAME_FEED_MEDIA, values, null, null);
+ }
+
+ public void clearDownloadLog() {
+ db.delete(TABLE_NAME_DOWNLOAD_LOG, null, null);
+ }
+
+ /**
+ * Get all Feeds from the Feed Table.
+ *
+ * @return The cursor of the query
+ */
+ public final Cursor getAllFeedsCursor() {
+ return db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, null, null, null, null,
+ KEY_TITLE + " COLLATE NOCASE ASC");
+ }
+
+ public final Cursor getFeedCursorDownloadUrls() {
+ return db.query(TABLE_NAME_FEEDS, new String[]{KEY_ID, KEY_DOWNLOAD_URL}, null, null, null, null, null);
+ }
+
+ /**
+ * Returns a cursor with all FeedItems of a Feed. Uses FEEDITEM_SEL_FI_SMALL
+ *
+ * @param feed The feed you want to get the FeedItems from.
+ * @return The cursor of the query
+ */
+ public final Cursor getItemsOfFeedCursor(final Feed feed, FeedItemFilter filter) {
+ String filterQuery = FeedItemFilterQuery.generateFrom(filter);
+ String whereClauseAnd = "".equals(filterQuery) ? "" : " AND " + filterQuery;
+ final String query = SELECT_FEED_ITEMS_AND_MEDIA
+ + " WHERE " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + feed.getId()
+ + whereClauseAnd;
+ return db.rawQuery(query, null);
+ }
+
+ /**
+ * Return the description and content_encoded of item
+ */
+ public final Cursor getDescriptionOfItem(final FeedItem item) {
+ final String query = "SELECT " + KEY_DESCRIPTION
+ + " FROM " + TABLE_NAME_FEED_ITEMS
+ + " WHERE " + KEY_ID + "=" + item.getId();
+ return db.rawQuery(query, null);
+ }
+
+ public final Cursor getSimpleChaptersOfFeedItemCursor(final FeedItem item) {
+ return db.query(TABLE_NAME_SIMPLECHAPTERS, null, KEY_FEEDITEM
+ + "=?", new String[]{String.valueOf(item.getId())}, null,
+ null, null
+ );
+ }
+
+ public final Cursor getDownloadLog(final int feedFileType, final long feedFileId) {
+ final String query = "SELECT * FROM " + TABLE_NAME_DOWNLOAD_LOG +
+ " WHERE " + KEY_FEEDFILE + "=" + feedFileId + " AND " + KEY_FEEDFILETYPE + "=" + feedFileType
+ + " ORDER BY " + KEY_ID + " DESC";
+ return db.rawQuery(query, null);
+ }
+
+ public final Cursor getDownloadLogCursor(final int limit) {
+ return db.query(TABLE_NAME_DOWNLOAD_LOG, null, null, null, null,
+ null, KEY_COMPLETION_DATE + " DESC LIMIT " + limit);
+ }
+
+ /**
+ * Returns a cursor which contains all feed items in the queue. The returned
+ * cursor uses the FEEDITEM_SEL_FI_SMALL selection.
+ * cursor uses the FEEDITEM_SEL_FI_SMALL selection.
+ */
+ public final Cursor getQueueCursor() {
+ final String query = SELECT_FEED_ITEMS_AND_MEDIA
+ + " INNER JOIN " + TABLE_NAME_QUEUE
+ + " ON " + SELECT_KEY_ITEM_ID + " = " + TABLE_NAME_QUEUE + "." + KEY_FEEDITEM
+ + " ORDER BY " + TABLE_NAME_QUEUE + "." + KEY_ID;
+ return db.rawQuery(query, null);
+ }
+
+ public Cursor getQueueIDCursor() {
+ return db.query(TABLE_NAME_QUEUE, new String[]{KEY_FEEDITEM}, null, null, null, null, KEY_ID + " ASC", null);
+ }
+
+ public Cursor getNextInQueue(final FeedItem item) {
+ final String query = SELECT_FEED_ITEMS_AND_MEDIA
+ + "INNER JOIN " + TABLE_NAME_QUEUE
+ + " ON " + SELECT_KEY_ITEM_ID + " = " + TABLE_NAME_QUEUE + "." + KEY_FEEDITEM
+ + " WHERE Queue.ID > (SELECT Queue.ID FROM Queue WHERE Queue.FeedItem = "
+ + item.getId()
+ + ")"
+ + " ORDER BY Queue.ID"
+ + " LIMIT 1";
+ return db.rawQuery(query, null);
+ }
+
+ public final Cursor getFavoritesCursor(int offset, int limit) {
+ final String query = SELECT_FEED_ITEMS_AND_MEDIA
+ + " INNER JOIN " + TABLE_NAME_FAVORITES
+ + " ON " + SELECT_KEY_ITEM_ID + " = " + TABLE_NAME_FAVORITES + "." + KEY_FEEDITEM
+ + " ORDER BY " + TABLE_NAME_FEED_ITEMS + "." + KEY_PUBDATE + " DESC"
+ + " LIMIT " + offset + ", " + limit;
+ return db.rawQuery(query, null);
+ }
+
+ public void setFeedItems(int state) {
+ setFeedItems(Integer.MIN_VALUE, state, 0);
+ }
+
+ public void setFeedItems(int oldState, int newState) {
+ setFeedItems(oldState, newState, 0);
+ }
+
+ public void setFeedItems(int state, long feedId) {
+ setFeedItems(Integer.MIN_VALUE, state, feedId);
+ }
+
+ public void setFeedItems(int oldState, int newState, long feedId) {
+ String sql = "UPDATE " + TABLE_NAME_FEED_ITEMS + " SET " + KEY_READ + "=" + newState;
+ if (feedId > 0) {
+ sql += " WHERE " + KEY_FEED + "=" + feedId;
+ }
+ if (FeedItem.NEW <= oldState && oldState <= FeedItem.PLAYED) {
+ sql += feedId > 0 ? " AND " : " WHERE ";
+ sql += KEY_READ + "=" + oldState;
+ }
+ db.execSQL(sql);
+ }
+
+ /**
+ * Returns a cursor which contains all feed items that are considered new.
+ * Excludes those feeds that do not have 'Keep Updated' enabled.
+ * The returned cursor uses the FEEDITEM_SEL_FI_SMALL selection.
+ */
+ public final Cursor getNewItemsCursor(int offset, int limit) {
+ final String query = SELECT_FEED_ITEMS_AND_MEDIA
+ + " INNER JOIN " + TABLE_NAME_FEEDS
+ + " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + TABLE_NAME_FEEDS + "." + KEY_ID
+ + " WHERE " + TABLE_NAME_FEED_ITEMS + "." + KEY_READ + "=" + FeedItem.NEW
+ + " AND " + TABLE_NAME_FEEDS + "." + KEY_KEEP_UPDATED + " > 0"
+ + " ORDER BY " + TABLE_NAME_FEED_ITEMS + "." + KEY_PUBDATE + " DESC"
+ + " LIMIT " + offset + ", " + limit;
+ return db.rawQuery(query, null);
+ }
+
+ public final Cursor getRecentlyPublishedItemsCursor(int offset, int limit, FeedItemFilter filter) {
+ String filterQuery = FeedItemFilterQuery.generateFrom(filter);
+ String whereClause = "".equals(filterQuery) ? "" : " WHERE " + filterQuery;
+ final String query = SELECT_FEED_ITEMS_AND_MEDIA + whereClause
+ + " ORDER BY " + KEY_PUBDATE + " DESC LIMIT " + offset + ", " + limit;
+ return db.rawQuery(query, null);
+ }
+
+ public Cursor getDownloadedItemsCursor() {
+ final String query = SELECT_FEED_ITEMS_AND_MEDIA
+ + "WHERE " + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + " > 0";
+ return db.rawQuery(query, null);
+ }
+
+ public Cursor getPlayedItemsCursor() {
+ final String query = SELECT_FEED_ITEMS_AND_MEDIA
+ + "WHERE " + TABLE_NAME_FEED_ITEMS + "." + KEY_READ + "=" + FeedItem.PLAYED;
+ return db.rawQuery(query, null);
+ }
+
+ /**
+ * Returns a cursor which contains feed media objects with a playback
+ * completion date in ascending order.
+ *
+ * @param limit The maximum row count of the returned cursor. Must be an
+ * integer >= 0.
+ * @throws IllegalArgumentException if limit < 0
+ */
+ public final Cursor getCompletedMediaCursor(int limit) {
+ if (limit < 0) {
+ throw new IllegalArgumentException("Limit must be >= 0");
+ }
+
+ return db.query(TABLE_NAME_FEED_MEDIA, null,
+ KEY_PLAYBACK_COMPLETION_DATE + " > 0", null, null,
+ null, String.format(Locale.US, "%s DESC LIMIT %d", KEY_PLAYBACK_COMPLETION_DATE, limit));
+ }
+
+ public final Cursor getSingleFeedMediaCursor(long id) {
+ final String query = "SELECT " + KEYS_FEED_MEDIA + " FROM " + TABLE_NAME_FEED_MEDIA
+ + " WHERE " + KEY_ID + "=" + id;
+ return db.rawQuery(query, null);
+ }
+
+ public final Cursor getFeedCursor(final long id) {
+ return db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, KEY_ID + "=" + id, null,
+ null, null, null);
+ }
+
+ public final Cursor getFeedItemCursor(final String id) {
+ return getFeedItemCursor(new String[]{id});
+ }
+
+ public final Cursor getFeedItemCursor(final String[] ids) {
+ if (ids.length > IN_OPERATOR_MAXIMUM) {
+ throw new IllegalArgumentException("number of IDs must not be larger than " + IN_OPERATOR_MAXIMUM);
+ }
+ final String query = SELECT_FEED_ITEMS_AND_MEDIA
+ + " WHERE " + SELECT_KEY_ITEM_ID + " IN (" + TextUtils.join(",", ids) + ")";
+ return db.rawQuery(query, null);
+ }
+
+ public final Cursor getFeedItemCursor(final String guid, final String episodeUrl) {
+ String escapedEpisodeUrl = DatabaseUtils.sqlEscapeString(episodeUrl);
+ String whereClauseCondition = TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOAD_URL + "=" + escapedEpisodeUrl;
+
+ if (guid != null) {
+ String escapedGuid = DatabaseUtils.sqlEscapeString(guid);
+ whereClauseCondition = TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER + "=" + escapedGuid;
+ }
+
+ final String query = SELECT_FEED_ITEMS_AND_MEDIA
+ + " INNER JOIN " + TABLE_NAME_FEEDS
+ + " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + TABLE_NAME_FEEDS + "." + KEY_ID
+ + " WHERE " + whereClauseCondition;
+ return db.rawQuery(query, null);
+ }
+
+ public Cursor getImageAuthenticationCursor(final String imageUrl) {
+ String downloadUrl = DatabaseUtils.sqlEscapeString(imageUrl);
+ final String query = ""
+ + "SELECT " + KEY_USERNAME + "," + KEY_PASSWORD + " FROM " + TABLE_NAME_FEED_ITEMS
+ + " INNER JOIN " + TABLE_NAME_FEEDS
+ + " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + " = " + TABLE_NAME_FEEDS + "." + KEY_ID
+ + " WHERE " + TABLE_NAME_FEED_ITEMS + "." + KEY_IMAGE_URL + "=" + downloadUrl
+ + " UNION SELECT " + KEY_USERNAME + "," + KEY_PASSWORD + " FROM " + TABLE_NAME_FEEDS
+ + " WHERE " + TABLE_NAME_FEEDS + "." + KEY_IMAGE_URL + "=" + downloadUrl;
+ return db.rawQuery(query, null);
+ }
+
+ public final Cursor getMonthlyStatisticsCursor() {
+ final String query = "SELECT SUM(" + KEY_PLAYED_DURATION + ") AS total_duration"
+ + ", strftime('%m', datetime(" + KEY_LAST_PLAYED_TIME + "/1000, 'unixepoch')) AS month"
+ + ", strftime('%Y', datetime(" + KEY_LAST_PLAYED_TIME + "/1000, 'unixepoch')) AS year"
+ + " FROM " + TABLE_NAME_FEED_MEDIA
+ + " WHERE " + KEY_LAST_PLAYED_TIME + " > 0 AND " + KEY_PLAYED_DURATION + " > 0"
+ + " GROUP BY year, month"
+ + " ORDER BY year, month";
+ return db.rawQuery(query, null);
+ }
+
+ public int getQueueSize() {
+ final String query = String.format("SELECT COUNT(%s) FROM %s", KEY_ID, TABLE_NAME_QUEUE);
+ Cursor c = db.rawQuery(query, null);
+ int result = 0;
+ if (c.moveToFirst()) {
+ result = c.getInt(0);
+ }
+ c.close();
+ return result;
+ }
+
+ public final int getNumberOfNewItems() {
+ Object[] args = new String[]{
+ TABLE_NAME_FEED_ITEMS + "." + KEY_ID,
+ TABLE_NAME_FEED_ITEMS,
+ TABLE_NAME_FEEDS,
+ TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + TABLE_NAME_FEEDS + "." + KEY_ID,
+ TABLE_NAME_FEED_ITEMS + "." + KEY_READ + "=" + FeedItem.NEW
+ + " AND " + TABLE_NAME_FEEDS + "." + KEY_KEEP_UPDATED + " > 0"
+ };
+ final String query = String.format("SELECT COUNT(%s) FROM %s INNER JOIN %s ON %s WHERE %s", args);
+ Cursor c = db.rawQuery(query, null);
+ int result = 0;
+ if (c.moveToFirst()) {
+ result = c.getInt(0);
+ }
+ c.close();
+ return result;
+ }
+
+ public final LongIntMap getFeedCounters(FeedCounter setting, long... feedIds) {
+ String whereRead;
+ switch (setting) {
+ case SHOW_NEW_UNPLAYED_SUM:
+ whereRead = "(" + KEY_READ + "=" + FeedItem.NEW +
+ " OR " + KEY_READ + "=" + FeedItem.UNPLAYED + ")";
+ break;
+ case SHOW_NEW:
+ whereRead = KEY_READ + "=" + FeedItem.NEW;
+ break;
+ case SHOW_UNPLAYED:
+ whereRead = KEY_READ + "=" + FeedItem.UNPLAYED;
+ break;
+ case SHOW_DOWNLOADED:
+ whereRead = KEY_DOWNLOADED + "=1";
+ break;
+ case SHOW_NONE:
+ // deliberate fall-through
+ default: // NONE
+ return new LongIntMap(0);
+ }
+ return conditionalFeedCounterRead(whereRead, feedIds);
+ }
+
+ private LongIntMap conditionalFeedCounterRead(String whereRead, long... feedIds) {
+ String limitFeeds = "";
+ if (feedIds.length > 0) {
+ // work around TextUtils.join wanting only boxed items
+ // and StringUtils.join() causing NoSuchMethodErrors on MIUI
+ StringBuilder builder = new StringBuilder();
+ for (long id : feedIds) {
+ builder.append(id);
+ builder.append(',');
+ }
+ // there's an extra ',', get rid of it
+ builder.deleteCharAt(builder.length() - 1);
+ limitFeeds = KEY_FEED + " IN (" + builder.toString() + ") AND ";
+ }
+
+ final String query = "SELECT " + KEY_FEED + ", COUNT(" + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + ") AS count "
+ + " FROM " + TABLE_NAME_FEED_ITEMS
+ + " LEFT JOIN " + TABLE_NAME_FEED_MEDIA + " ON "
+ + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" + TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM
+ + " WHERE " + limitFeeds + " "
+ + whereRead + " GROUP BY " + KEY_FEED;
+
+ Cursor c = db.rawQuery(query, null);
+ LongIntMap result = new LongIntMap(c.getCount());
+ if (c.moveToFirst()) {
+ do {
+ long feedId = c.getLong(0);
+ int count = c.getInt(1);
+ result.put(feedId, count);
+ } while (c.moveToNext());
+ }
+ c.close();
+ return result;
+ }
+
+ public final LongIntMap getPlayedEpisodesCounters(long... feedIds) {
+ String whereRead = KEY_READ + "=" + FeedItem.PLAYED;
+ return conditionalFeedCounterRead(whereRead, feedIds);
+ }
+
+ public final Map<Long, Long> getMostRecentItemDates() {
+ final String query = "SELECT " + KEY_FEED + ","
+ + " MAX(" + TABLE_NAME_FEED_ITEMS + "." + KEY_PUBDATE + ") AS most_recent_pubdate"
+ + " FROM " + TABLE_NAME_FEED_ITEMS
+ + " GROUP BY " + KEY_FEED;
+
+ Cursor c = db.rawQuery(query, null);
+ Map<Long, Long> result = new HashMap<>();
+ if (c.moveToFirst()) {
+ do {
+ long feedId = c.getLong(0);
+ long date = c.getLong(1);
+ result.put(feedId, date);
+ } while (c.moveToNext());
+ }
+ c.close();
+ return result;
+ }
+
+ public final int getNumberOfDownloadedEpisodes() {
+ final String query = "SELECT COUNT(DISTINCT " + KEY_ID + ") AS count FROM " + TABLE_NAME_FEED_MEDIA +
+ " WHERE " + KEY_DOWNLOADED + " > 0";
+
+ Cursor c = db.rawQuery(query, null);
+ int result = 0;
+ if (c.moveToFirst()) {
+ result = c.getInt(0);
+ }
+ c.close();
+ return result;
+ }
+
+ /**
+ * Uses DatabaseUtils to escape a search query and removes ' at the
+ * beginning and the end of the string returned by the escape method.
+ */
+ private String[] prepareSearchQuery(String query) {
+ String[] queryWords = query.split("\\s+");
+ for (int i = 0; i < queryWords.length; ++i) {
+ StringBuilder builder = new StringBuilder();
+ DatabaseUtils.appendEscapedSQLString(builder, queryWords[i]);
+ builder.deleteCharAt(0);
+ builder.deleteCharAt(builder.length() - 1);
+ queryWords[i] = builder.toString();
+ }
+
+ return queryWords;
+ }
+
+ /**
+ * Searches for the given query in various values of all items or the items
+ * of a specified feed.
+ *
+ * @return A cursor with all search results in SEL_FI_EXTRA selection.
+ */
+ public Cursor searchItems(long feedID, String searchQuery) {
+ String[] queryWords = prepareSearchQuery(searchQuery);
+
+ String queryFeedId;
+ if (feedID != 0) {
+ // search items in specific feed
+ queryFeedId = KEY_FEED + " = " + feedID;
+ } else {
+ // search through all items
+ queryFeedId = "1 = 1";
+ }
+
+ String queryStart = SELECT_FEED_ITEMS_AND_MEDIA_WITH_DESCRIPTION
+ + " WHERE " + queryFeedId + " AND (";
+ StringBuilder sb = new StringBuilder(queryStart);
+
+ for (int i = 0; i < queryWords.length; i++) {
+ sb
+ .append("(")
+ .append(KEY_DESCRIPTION + " LIKE '%").append(queryWords[i])
+ .append("%' OR ")
+ .append(KEY_TITLE).append(" LIKE '%").append(queryWords[i])
+ .append("%') ");
+
+ if (i != queryWords.length - 1) {
+ sb.append("AND ");
+ }
+ }
+
+ sb.append(") ORDER BY " + KEY_PUBDATE + " DESC LIMIT 300");
+
+ return db.rawQuery(sb.toString(), null);
+ }
+
+ /**
+ * Searches for the given query in various values of all feeds.
+ *
+ * @return A cursor with all search results in SEL_FI_EXTRA selection.
+ */
+ public Cursor searchFeeds(String searchQuery) {
+ String[] queryWords = prepareSearchQuery(searchQuery);
+
+ String queryStart = "SELECT * FROM " + TABLE_NAME_FEEDS + " WHERE ";
+ StringBuilder sb = new StringBuilder(queryStart);
+
+ for (int i = 0; i < queryWords.length; i++) {
+ sb
+ .append("(")
+ .append(KEY_TITLE).append(" LIKE '%").append(queryWords[i])
+ .append("%' OR ")
+ .append(KEY_CUSTOM_TITLE).append(" LIKE '%").append(queryWords[i])
+ .append("%' OR ")
+ .append(KEY_AUTHOR).append(" LIKE '%").append(queryWords[i])
+ .append("%' OR ")
+ .append(KEY_DESCRIPTION).append(" LIKE '%").append(queryWords[i])
+ .append("%') ");
+
+ if (i != queryWords.length - 1) {
+ sb.append("AND ");
+ }
+ }
+
+ sb.append("ORDER BY " + KEY_TITLE + " ASC LIMIT 300");
+
+ return db.rawQuery(sb.toString(), null);
+ }
+
+ /**
+ * Insert raw data to the database.
+ * Call method only for unit tests.
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ public void insertTestData(@NonNull String table, @NonNull ContentValues values) {
+ db.insert(table, null, values);
+ }
+
+ /**
+ * Called when a database corruption happens.
+ */
+ public static class PodDbErrorHandler implements DatabaseErrorHandler {
+ @Override
+ public void onCorruption(SQLiteDatabase db) {
+ Log.e(TAG, "Database corrupted: " + db.getPath());
+
+ File dbPath = new File(db.getPath());
+ File backupFolder = PodDBAdapter.context.getExternalFilesDir(null);
+ File backupFile = new File(backupFolder, "CorruptedDatabaseBackup.db");
+ try {
+ FileUtils.copyFile(dbPath, backupFile);
+ Log.d(TAG, "Dumped database to " + backupFile.getPath());
+ } catch (IOException e) {
+ Log.d(TAG, Log.getStackTraceString(e));
+ }
+
+ new DefaultDatabaseErrorHandler().onCorruption(db); // This deletes the database
+ }
+ }
+
+ /**
+ * Helper class for opening the Antennapod database.
+ */
+ private static class PodDBHelper extends SQLiteOpenHelper {
+ /**
+ * Constructor.
+ *
+ * @param context Context to use
+ * @param name Name of the database
+ * @param factory to use for creating cursor objects
+ */
+ public PodDBHelper(final Context context, final String name, final CursorFactory factory) {
+ super(context, name, factory, VERSION, new PodDbErrorHandler());
+ }
+
+ @Override
+ public void onCreate(final SQLiteDatabase db) {
+ db.execSQL(CREATE_TABLE_FEEDS);
+ db.execSQL(CREATE_TABLE_FEED_ITEMS);
+ db.execSQL(CREATE_TABLE_FEED_MEDIA);
+ db.execSQL(CREATE_TABLE_DOWNLOAD_LOG);
+ db.execSQL(CREATE_TABLE_QUEUE);
+ db.execSQL(CREATE_TABLE_SIMPLECHAPTERS);
+ db.execSQL(CREATE_TABLE_FAVORITES);
+
+ db.execSQL(CREATE_INDEX_FEEDITEMS_FEED);
+ db.execSQL(CREATE_INDEX_FEEDITEMS_PUBDATE);
+ db.execSQL(CREATE_INDEX_FEEDITEMS_READ);
+ db.execSQL(CREATE_INDEX_FEEDMEDIA_FEEDITEM);
+ db.execSQL(CREATE_INDEX_QUEUE_FEEDITEM);
+ db.execSQL(CREATE_INDEX_SIMPLECHAPTERS_FEEDITEM);
+ }
+
+ @Override
+ public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
+ Log.w("DBAdapter", "Upgrading from version " + oldVersion + " to " + newVersion + ".");
+ DBUpgrader.upgrade(db, oldVersion, newVersion);
+ }
+ }
+}
diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/ChapterCursorMapper.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/ChapterCursorMapper.java
new file mode 100644
index 000000000..71e67812d
--- /dev/null
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/ChapterCursorMapper.java
@@ -0,0 +1,32 @@
+package de.danoeh.antennapod.storage.database.mapper;
+
+import android.database.Cursor;
+import androidx.annotation.NonNull;
+import de.danoeh.antennapod.model.feed.Chapter;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
+
+/**
+ * Converts a {@link Cursor} to a {@link Chapter} object.
+ */
+public abstract class ChapterCursorMapper {
+ /**
+ * Create a {@link Chapter} instance from a database row (cursor).
+ */
+ @NonNull
+ public static Chapter convert(@NonNull Cursor cursor) {
+ int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID);
+ int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_TITLE);
+ int indexStart = cursor.getColumnIndex(PodDBAdapter.KEY_START);
+ int indexLink = cursor.getColumnIndex(PodDBAdapter.KEY_LINK);
+ int indexImage = cursor.getColumnIndex(PodDBAdapter.KEY_IMAGE_URL);
+
+ long id = cursor.getLong(indexId);
+ String title = cursor.getString(indexTitle);
+ long start = cursor.getLong(indexStart);
+ String link = cursor.getString(indexLink);
+ String imageUrl = cursor.getString(indexImage);
+ Chapter chapter = new Chapter(start, title, link, imageUrl);
+ chapter.setId(id);
+ return chapter;
+ }
+}
diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/DownloadStatusCursorMapper.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/DownloadStatusCursorMapper.java
new file mode 100644
index 000000000..4a5a792af
--- /dev/null
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/DownloadStatusCursorMapper.java
@@ -0,0 +1,35 @@
+package de.danoeh.antennapod.storage.database.mapper;
+
+import android.database.Cursor;
+import androidx.annotation.NonNull;
+import de.danoeh.antennapod.model.download.DownloadStatus;
+import de.danoeh.antennapod.model.download.DownloadError;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
+
+import java.util.Date;
+
+/**
+ * Converts a {@link Cursor} to a {@link DownloadStatus} object.
+ */
+public abstract class DownloadStatusCursorMapper {
+ /**
+ * Create a {@link DownloadStatus} instance from a database row (cursor).
+ */
+ @NonNull
+ public static DownloadStatus convert(@NonNull Cursor cursor) {
+ int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID);
+ int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOADSTATUS_TITLE);
+ int indexFeedFile = cursor.getColumnIndex(PodDBAdapter.KEY_FEEDFILE);
+ int indexFileFileType = cursor.getColumnIndex(PodDBAdapter.KEY_FEEDFILETYPE);
+ int indexSuccessful = cursor.getColumnIndex(PodDBAdapter.KEY_SUCCESSFUL);
+ int indexReason = cursor.getColumnIndex(PodDBAdapter.KEY_REASON);
+ int indexCompletionDate = cursor.getColumnIndex(PodDBAdapter.KEY_COMPLETION_DATE);
+ int indexReasonDetailed = cursor.getColumnIndex(PodDBAdapter.KEY_REASON_DETAILED);
+
+ return new DownloadStatus(cursor.getLong(indexId), cursor.getString(indexTitle), cursor.getLong(indexFeedFile),
+ cursor.getInt(indexFileFileType), cursor.getInt(indexSuccessful) > 0, false, true,
+ DownloadError.fromCode(cursor.getInt(indexReason)),
+ new Date(cursor.getLong(indexCompletionDate)),
+ cursor.getString(indexReasonDetailed), false);
+ }
+}
diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedCursorMapper.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedCursorMapper.java
new file mode 100644
index 000000000..25df7313f
--- /dev/null
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedCursorMapper.java
@@ -0,0 +1,70 @@
+package de.danoeh.antennapod.storage.database.mapper;
+
+import android.database.Cursor;
+
+import androidx.annotation.NonNull;
+
+import de.danoeh.antennapod.model.feed.Feed;
+import de.danoeh.antennapod.model.feed.FeedPreferences;
+import de.danoeh.antennapod.model.feed.SortOrder;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
+
+/**
+ * Converts a {@link Cursor} to a {@link Feed} object.
+ */
+public abstract class FeedCursorMapper {
+
+ /**
+ * Create a {@link Feed} instance from a database row (cursor).
+ */
+ @NonNull
+ public static Feed convert(@NonNull Cursor cursor) {
+ int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID);
+ int indexLastUpdate = cursor.getColumnIndex(PodDBAdapter.KEY_LASTUPDATE);
+ int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_TITLE);
+ int indexCustomTitle = cursor.getColumnIndex(PodDBAdapter.KEY_CUSTOM_TITLE);
+ int indexLink = cursor.getColumnIndex(PodDBAdapter.KEY_LINK);
+ int indexDescription = cursor.getColumnIndex(PodDBAdapter.KEY_DESCRIPTION);
+ int indexPaymentLink = cursor.getColumnIndex(PodDBAdapter.KEY_PAYMENT_LINK);
+ int indexAuthor = cursor.getColumnIndex(PodDBAdapter.KEY_AUTHOR);
+ int indexLanguage = cursor.getColumnIndex(PodDBAdapter.KEY_LANGUAGE);
+ int indexType = cursor.getColumnIndex(PodDBAdapter.KEY_TYPE);
+ int indexFeedIdentifier = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_IDENTIFIER);
+ int indexFileUrl = cursor.getColumnIndex(PodDBAdapter.KEY_FILE_URL);
+ int indexDownloadUrl = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOAD_URL);
+ int indexDownloaded = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOADED);
+ int indexIsPaged = cursor.getColumnIndex(PodDBAdapter.KEY_IS_PAGED);
+ int indexNextPageLink = cursor.getColumnIndex(PodDBAdapter.KEY_NEXT_PAGE_LINK);
+ int indexHide = cursor.getColumnIndex(PodDBAdapter.KEY_HIDE);
+ int indexSortOrder = cursor.getColumnIndex(PodDBAdapter.KEY_SORT_ORDER);
+ int indexLastUpdateFailed = cursor.getColumnIndex(PodDBAdapter.KEY_LAST_UPDATE_FAILED);
+ int indexImageUrl = cursor.getColumnIndex(PodDBAdapter.KEY_IMAGE_URL);
+
+ Feed feed = new Feed(
+ cursor.getLong(indexId),
+ cursor.getString(indexLastUpdate),
+ cursor.getString(indexTitle),
+ cursor.getString(indexCustomTitle),
+ cursor.getString(indexLink),
+ cursor.getString(indexDescription),
+ cursor.getString(indexPaymentLink),
+ cursor.getString(indexAuthor),
+ cursor.getString(indexLanguage),
+ cursor.getString(indexType),
+ cursor.getString(indexFeedIdentifier),
+ cursor.getString(indexImageUrl),
+ cursor.getString(indexFileUrl),
+ cursor.getString(indexDownloadUrl),
+ cursor.getInt(indexDownloaded) > 0,
+ cursor.getInt(indexIsPaged) > 0,
+ cursor.getString(indexNextPageLink),
+ cursor.getString(indexHide),
+ SortOrder.fromCodeString(cursor.getString(indexSortOrder)),
+ cursor.getInt(indexLastUpdateFailed) > 0
+ );
+
+ FeedPreferences preferences = FeedPreferencesCursorMapper.convert(cursor);
+ feed.setPreferences(preferences);
+ return feed;
+ }
+}
diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedItemCursorMapper.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedItemCursorMapper.java
new file mode 100644
index 000000000..799ca5dde
--- /dev/null
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedItemCursorMapper.java
@@ -0,0 +1,46 @@
+package de.danoeh.antennapod.storage.database.mapper;
+
+import android.database.Cursor;
+import androidx.annotation.NonNull;
+import de.danoeh.antennapod.model.feed.FeedItem;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
+
+import java.util.Date;
+
+/**
+ * Converts a {@link Cursor} to a {@link FeedItem} object.
+ */
+public abstract class FeedItemCursorMapper {
+ /**
+ * Create a {@link FeedItem} instance from a database row (cursor).
+ */
+ @NonNull
+ public static FeedItem convert(@NonNull Cursor cursor) {
+ int indexId = cursor.getColumnIndexOrThrow(PodDBAdapter.SELECT_KEY_ITEM_ID);
+ int indexTitle = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_TITLE);
+ int indexLink = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_LINK);
+ int indexPubDate = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_PUBDATE);
+ int indexPaymentLink = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_PAYMENT_LINK);
+ int indexFeedId = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_FEED);
+ int indexHasChapters = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_HAS_CHAPTERS);
+ int indexRead = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_READ);
+ int indexItemIdentifier = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_ITEM_IDENTIFIER);
+ int indexAutoDownload = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_AUTO_DOWNLOAD_ATTEMPTS);
+ int indexImageUrl = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_IMAGE_URL);
+
+ long id = cursor.getInt(indexId);
+ String title = cursor.getString(indexTitle);
+ String link = cursor.getString(indexLink);
+ Date pubDate = new Date(cursor.getLong(indexPubDate));
+ String paymentLink = cursor.getString(indexPaymentLink);
+ long feedId = cursor.getLong(indexFeedId);
+ boolean hasChapters = cursor.getInt(indexHasChapters) > 0;
+ int state = cursor.getInt(indexRead);
+ String itemIdentifier = cursor.getString(indexItemIdentifier);
+ long autoDownload = cursor.getLong(indexAutoDownload);
+ String imageUrl = cursor.getString(indexImageUrl);
+
+ return new FeedItem(id, title, link, pubDate, paymentLink, feedId,
+ hasChapters, imageUrl, state, itemIdentifier, autoDownload);
+ }
+}
diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedItemFilterQuery.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedItemFilterQuery.java
new file mode 100644
index 000000000..1728a905f
--- /dev/null
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedItemFilterQuery.java
@@ -0,0 +1,76 @@
+package de.danoeh.antennapod.storage.database.mapper;
+
+import de.danoeh.antennapod.model.feed.FeedItemFilter;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class FeedItemFilterQuery {
+ private FeedItemFilterQuery() {
+ // Must not be instantiated
+ }
+
+ /**
+ * Express the filter using an SQL boolean statement that can be inserted into an SQL WHERE clause
+ * to yield output filtered according to the rules of this filter.
+ *
+ * @return An SQL boolean statement that matches the desired items,
+ * empty string if there is nothing to filter
+ */
+ public static String generateFrom(FeedItemFilter filter) {
+ // The keys used within this method, but explicitly combined with their table
+ String keyRead = PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_READ;
+ String keyPosition = PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + PodDBAdapter.KEY_POSITION;
+ String keyDownloaded = PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + PodDBAdapter.KEY_DOWNLOADED;
+ String keyMediaId = PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + PodDBAdapter.KEY_ID;
+ String keyItemId = PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_ID;
+ String keyFeedItem = PodDBAdapter.KEY_FEEDITEM;
+ String tableQueue = PodDBAdapter.TABLE_NAME_QUEUE;
+ String tableFavorites = PodDBAdapter.TABLE_NAME_FAVORITES;
+
+ List<String> statements = new ArrayList<>();
+ if (filter.showPlayed) {
+ statements.add(keyRead + " = 1 ");
+ } else if (filter.showUnplayed) {
+ statements.add(" NOT " + keyRead + " = 1 "); // Match "New" items (read = -1) as well
+ }
+ if (filter.showPaused) {
+ statements.add(" (" + keyPosition + " NOT NULL AND " + keyPosition + " > 0 " + ") ");
+ } else if (filter.showNotPaused) {
+ statements.add(" (" + keyPosition + " IS NULL OR " + keyPosition + " = 0 " + ") ");
+ }
+ if (filter.showQueued) {
+ statements.add(keyItemId + " IN (SELECT " + keyFeedItem + " FROM " + tableQueue + ") ");
+ } else if (filter.showNotQueued) {
+ statements.add(keyItemId + " NOT IN (SELECT " + keyFeedItem + " FROM " + tableQueue + ") ");
+ }
+ if (filter.showDownloaded) {
+ statements.add(keyDownloaded + " = 1 ");
+ } else if (filter.showNotDownloaded) {
+ statements.add(keyDownloaded + " = 0 ");
+ }
+ if (filter.showHasMedia) {
+ statements.add(keyMediaId + " NOT NULL ");
+ } else if (filter.showNoMedia) {
+ statements.add(keyMediaId + " IS NULL ");
+ }
+ if (filter.showIsFavorite) {
+ statements.add(keyItemId + " IN (SELECT " + keyFeedItem + " FROM " + tableFavorites + ") ");
+ } else if (filter.showNotFavorite) {
+ statements.add(keyItemId + " NOT IN (SELECT " + keyFeedItem + " FROM " + tableFavorites + ") ");
+ }
+
+ if (statements.isEmpty()) {
+ return "";
+ }
+
+ StringBuilder query = new StringBuilder(" (" + statements.get(0));
+ for (String r : statements.subList(1, statements.size())) {
+ query.append(" AND ");
+ query.append(r);
+ }
+ query.append(") ");
+ return query.toString();
+ }
+}
diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedMediaCursorMapper.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedMediaCursorMapper.java
new file mode 100644
index 000000000..f57e91b83
--- /dev/null
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedMediaCursorMapper.java
@@ -0,0 +1,67 @@
+package de.danoeh.antennapod.storage.database.mapper;
+
+import android.database.Cursor;
+import androidx.annotation.NonNull;
+import de.danoeh.antennapod.model.feed.FeedMedia;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
+
+import java.util.Date;
+
+/**
+ * Converts a {@link Cursor} to a {@link FeedMedia} object.
+ */
+public abstract class FeedMediaCursorMapper {
+ /**
+ * Create a {@link FeedMedia} instance from a database row (cursor).
+ */
+ @NonNull
+ public static FeedMedia convert(@NonNull Cursor cursor) {
+ int indexId = cursor.getColumnIndexOrThrow(PodDBAdapter.SELECT_KEY_MEDIA_ID);
+ int indexPlaybackCompletionDate = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_PLAYBACK_COMPLETION_DATE);
+ int indexDuration = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_DURATION);
+ int indexPosition = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_POSITION);
+ int indexSize = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_SIZE);
+ int indexMimeType = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_MIME_TYPE);
+ int indexFileUrl = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_FILE_URL);
+ int indexDownloadUrl = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_DOWNLOAD_URL);
+ int indexDownloaded = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_DOWNLOADED);
+ int indexPlayedDuration = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_PLAYED_DURATION);
+ int indexLastPlayedTime = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_LAST_PLAYED_TIME);
+
+ long mediaId = cursor.getLong(indexId);
+ Date playbackCompletionDate = null;
+ long playbackCompletionTime = cursor.getLong(indexPlaybackCompletionDate);
+ if (playbackCompletionTime > 0) {
+ playbackCompletionDate = new Date(playbackCompletionTime);
+ }
+
+ Boolean hasEmbeddedPicture;
+ switch (cursor.getInt(cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_HAS_EMBEDDED_PICTURE))) {
+ case 1:
+ hasEmbeddedPicture = Boolean.TRUE;
+ break;
+ case 0:
+ hasEmbeddedPicture = Boolean.FALSE;
+ break;
+ default:
+ hasEmbeddedPicture = null;
+ break;
+ }
+
+ return new FeedMedia(
+ mediaId,
+ null,
+ cursor.getInt(indexDuration),
+ cursor.getInt(indexPosition),
+ cursor.getLong(indexSize),
+ cursor.getString(indexMimeType),
+ cursor.getString(indexFileUrl),
+ cursor.getString(indexDownloadUrl),
+ cursor.getInt(indexDownloaded) > 0,
+ playbackCompletionDate,
+ cursor.getInt(indexPlayedDuration),
+ hasEmbeddedPicture,
+ cursor.getLong(indexLastPlayedTime)
+ );
+ }
+}
diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedPreferencesCursorMapper.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedPreferencesCursorMapper.java
new file mode 100644
index 000000000..9fc70a2d7
--- /dev/null
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedPreferencesCursorMapper.java
@@ -0,0 +1,74 @@
+package de.danoeh.antennapod.storage.database.mapper;
+
+import android.database.Cursor;
+import android.text.TextUtils;
+import androidx.annotation.NonNull;
+import de.danoeh.antennapod.model.feed.FeedFilter;
+import de.danoeh.antennapod.model.feed.FeedPreferences;
+import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
+
+import java.util.Arrays;
+import java.util.HashSet;
+
+/**
+ * Converts a {@link Cursor} to a {@link FeedPreferences} object.
+ */
+public abstract class FeedPreferencesCursorMapper {
+ /**
+ * Create a {@link FeedPreferences} instance from a database row (cursor).
+ */
+ @NonNull
+ public static FeedPreferences convert(@NonNull Cursor cursor) {
+ int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID);
+ int indexAutoDownload = cursor.getColumnIndex(PodDBAdapter.KEY_AUTO_DOWNLOAD_ENABLED);
+ int indexAutoRefresh = cursor.getColumnIndex(PodDBAdapter.KEY_KEEP_UPDATED);
+ int indexAutoDeleteAction = cursor.getColumnIndex(PodDBAdapter.KEY_AUTO_DELETE_ACTION);
+ int indexVolumeAdaption = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_VOLUME_ADAPTION);
+ int indexUsername = cursor.getColumnIndex(PodDBAdapter.KEY_USERNAME);
+ int indexPassword = cursor.getColumnIndex(PodDBAdapter.KEY_PASSWORD);
+ int indexIncludeFilter = cursor.getColumnIndex(PodDBAdapter.KEY_INCLUDE_FILTER);
+ int indexExcludeFilter = cursor.getColumnIndex(PodDBAdapter.KEY_EXCLUDE_FILTER);
+ int indexMinimalDurationFilter = cursor.getColumnIndex(PodDBAdapter.KEY_MINIMAL_DURATION_FILTER);
+ int indexFeedPlaybackSpeed = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_PLAYBACK_SPEED);
+ int indexAutoSkipIntro = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_SKIP_INTRO);
+ int indexAutoSkipEnding = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_SKIP_ENDING);
+ int indexEpisodeNotification = cursor.getColumnIndex(PodDBAdapter.KEY_EPISODE_NOTIFICATION);
+ int indexTags = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_TAGS);
+
+ long feedId = cursor.getLong(indexId);
+ boolean autoDownload = cursor.getInt(indexAutoDownload) > 0;
+ boolean autoRefresh = cursor.getInt(indexAutoRefresh) > 0;
+ int autoDeleteActionIndex = cursor.getInt(indexAutoDeleteAction);
+ FeedPreferences.AutoDeleteAction autoDeleteAction =
+ FeedPreferences.AutoDeleteAction.values()[autoDeleteActionIndex];
+ int volumeAdaptionValue = cursor.getInt(indexVolumeAdaption);
+ VolumeAdaptionSetting volumeAdaptionSetting = VolumeAdaptionSetting.fromInteger(volumeAdaptionValue);
+ String username = cursor.getString(indexUsername);
+ String password = cursor.getString(indexPassword);
+ String includeFilter = cursor.getString(indexIncludeFilter);
+ String excludeFilter = cursor.getString(indexExcludeFilter);
+ int minimalDurationFilter = cursor.getInt(indexMinimalDurationFilter);
+ float feedPlaybackSpeed = cursor.getFloat(indexFeedPlaybackSpeed);
+ int feedAutoSkipIntro = cursor.getInt(indexAutoSkipIntro);
+ int feedAutoSkipEnding = cursor.getInt(indexAutoSkipEnding);
+ boolean showNotification = cursor.getInt(indexEpisodeNotification) > 0;
+ String tagsString = cursor.getString(indexTags);
+ if (TextUtils.isEmpty(tagsString)) {
+ tagsString = FeedPreferences.TAG_ROOT;
+ }
+ return new FeedPreferences(feedId,
+ autoDownload,
+ autoRefresh,
+ autoDeleteAction,
+ volumeAdaptionSetting,
+ username,
+ password,
+ new FeedFilter(includeFilter, excludeFilter, minimalDurationFilter),
+ feedPlaybackSpeed,
+ feedAutoSkipIntro,
+ feedAutoSkipEnding,
+ showNotification,
+ new HashSet<>(Arrays.asList(tagsString.split(FeedPreferences.TAG_SEPARATOR))));
+ }
+}