From ff3258c34a1797e75538b83baa1e867d0ea07ceb Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Sun, 27 Feb 2022 12:12:24 +0100 Subject: Move database to its own module --- storage/README.md | 3 + storage/database/README.md | 3 + storage/database/build.gradle | 17 + storage/database/src/main/AndroidManifest.xml | 1 + .../antennapod/storage/database/DBUpgrader.java | 331 +++++ .../antennapod/storage/database/LongIntMap.java | 263 ++++ .../antennapod/storage/database/PodDBAdapter.java | 1430 ++++++++++++++++++++ .../database/mapper/ChapterCursorMapper.java | 32 + .../mapper/DownloadStatusCursorMapper.java | 35 + .../storage/database/mapper/FeedCursorMapper.java | 70 + .../database/mapper/FeedItemCursorMapper.java | 46 + .../database/mapper/FeedItemFilterQuery.java | 76 ++ .../database/mapper/FeedMediaCursorMapper.java | 67 + .../mapper/FeedPreferencesCursorMapper.java | 74 + 14 files changed, 2448 insertions(+) create mode 100644 storage/README.md create mode 100644 storage/database/README.md create mode 100644 storage/database/build.gradle create mode 100644 storage/database/src/main/AndroidManifest.xml create mode 100644 storage/database/src/main/java/de/danoeh/antennapod/storage/database/DBUpgrader.java create mode 100644 storage/database/src/main/java/de/danoeh/antennapod/storage/database/LongIntMap.java create mode 100644 storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java create mode 100644 storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/ChapterCursorMapper.java create mode 100644 storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/DownloadStatusCursorMapper.java create mode 100644 storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedCursorMapper.java create mode 100644 storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedItemCursorMapper.java create mode 100644 storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedItemFilterQuery.java create mode 100644 storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedMediaCursorMapper.java create mode 100644 storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedPreferencesCursorMapper.java (limited to 'storage') 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 @@ + 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 0 + * 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 0...size()-1, returns + * the key from the indexth key-value mapping that this + * SparseLongArray stores. + * + *

The keys corresponding to indices in ascending order are guaranteed to + * be in ascending order, e.g., keyAt(0) will return the + * smallest key and keyAt(size()-1) will return the largest + * key.

+ */ + 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 0...size()-1, returns + * the value from the indexth key-value mapping that this + * SparseLongArray stores. + * + *

The values corresponding to indices in ascending order are guaranteed + * to be associated with keys in ascending order, e.g., + * valueAt(0) will return the value associated with the + * smallest key and valueAt(size()-1) will return the value + * associated with the largest key.

+ */ + 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 + } + + /** + *

Resets all database connections to ensure new database connections for + * the next test case. Call method only for unit tests.

+ * + *

That's a workaround for a Robolectric issue in ShadowSQLiteConnection + * that leads to an error IllegalStateException: Illegal connection + * pointer if several threads try to use the same database connection. + * For more information see + * robolectric/robolectric#1890.

+ */ + @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 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 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 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 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 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 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 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 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)))); + } +} -- cgit v1.2.3