diff --git a/app/schemas/org.schabi.newpipe.database.AppDatabase/10.json b/app/schemas/org.schabi.newpipe.database.AppDatabase/10.json deleted file mode 100644 index b6ee8079a..000000000 --- a/app/schemas/org.schabi.newpipe.database.AppDatabase/10.json +++ /dev/null @@ -1,856 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 10, - "identityHash": "92195bb0de0864bb1a0d7e4bbb16ec0f", - "entities": [ - { - "tableName": "subscriptions", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT, `notification_mode` INTEGER NOT NULL)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "serviceId", - "columnName": "service_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "url", - "columnName": "url", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "avatarUrl", - "columnName": "avatar_url", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "subscriberCount", - "columnName": "subscriber_count", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "notificationMode", - "columnName": "notification_mode", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "uid" - ] - }, - "indices": [ - { - "name": "index_subscriptions_service_id_url", - "unique": true, - "columnNames": [ - "service_id", - "url" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "search_history", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", - "fields": [ - { - "fieldPath": "creationDate", - "columnName": "creation_date", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "serviceId", - "columnName": "service_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "search", - "columnName": "search", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_search_history_search", - "unique": false, - "columnNames": [ - "search" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "streams", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `stream_type` TEXT NOT NULL, `duration` INTEGER NOT NULL, `uploader` TEXT NOT NULL, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "serviceId", - "columnName": "service_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "url", - "columnName": "url", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "streamType", - "columnName": "stream_type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "duration", - "columnName": "duration", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "uploader", - "columnName": "uploader", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "uploaderUrl", - "columnName": "uploader_url", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "thumbnailUrl", - "columnName": "thumbnail_url", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "viewCount", - "columnName": "view_count", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "textualUploadDate", - "columnName": "textual_upload_date", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "uploadDate", - "columnName": "upload_date", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "isUploadDateApproximation", - "columnName": "is_upload_date_approximation", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "uid" - ] - }, - "indices": [ - { - "name": "index_streams_service_id_url", - "unique": true, - "columnNames": [ - "service_id", - "url" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "stream_history", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "streamUid", - "columnName": "stream_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "accessDate", - "columnName": "access_date", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "repeatCount", - "columnName": "repeat_count", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "stream_id", - "access_date" - ] - }, - "indices": [ - { - "name": "index_stream_history_stream_id", - "unique": false, - "columnNames": [ - "stream_id" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)" - } - ], - "foreignKeys": [ - { - "table": "streams", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "stream_id" - ], - "referencedColumns": [ - "uid" - ] - } - ] - }, - { - "tableName": "stream_state", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "streamUid", - "columnName": "stream_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "progressMillis", - "columnName": "progress_time", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "stream_id" - ] - }, - "indices": [], - "foreignKeys": [ - { - "table": "streams", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "stream_id" - ], - "referencedColumns": [ - "uid" - ] - } - ] - }, - { - "tableName": "playlists", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, `thumbnail_stream_id` INTEGER NOT NULL, `display_index` INTEGER NOT NULL)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isThumbnailPermanent", - "columnName": "is_thumbnail_permanent", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "thumbnailStreamId", - "columnName": "thumbnail_stream_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "displayIndex", - "columnName": "display_index", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "uid" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "playlist_stream_join", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", - "fields": [ - { - "fieldPath": "playlistUid", - "columnName": "playlist_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "streamUid", - "columnName": "stream_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "index", - "columnName": "join_index", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "playlist_id", - "join_index" - ] - }, - "indices": [ - { - "name": "index_playlist_stream_join_playlist_id_join_index", - "unique": true, - "columnNames": [ - "playlist_id", - "join_index" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)" - }, - { - "name": "index_playlist_stream_join_stream_id", - "unique": false, - "columnNames": [ - "stream_id" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)" - } - ], - "foreignKeys": [ - { - "table": "playlists", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "playlist_id" - ], - "referencedColumns": [ - "uid" - ] - }, - { - "table": "streams", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "stream_id" - ], - "referencedColumns": [ - "uid" - ] - } - ] - }, - { - "tableName": "remote_playlists", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `display_index` INTEGER NOT NULL, `stream_count` INTEGER)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "serviceId", - "columnName": "service_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "url", - "columnName": "url", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "thumbnailUrl", - "columnName": "thumbnail_url", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "uploader", - "columnName": "uploader", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "displayIndex", - "columnName": "display_index", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "streamCount", - "columnName": "stream_count", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "uid" - ] - }, - "indices": [ - { - "name": "index_remote_playlists_service_id_url", - "unique": true, - "columnNames": [ - "service_id", - "url" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "feed", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `subscription_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", - "fields": [ - { - "fieldPath": "streamId", - "columnName": "stream_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "subscriptionId", - "columnName": "subscription_id", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "stream_id", - "subscription_id" - ] - }, - "indices": [ - { - "name": "index_feed_subscription_id", - "unique": false, - "columnNames": [ - "subscription_id" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)" - } - ], - "foreignKeys": [ - { - "table": "streams", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "stream_id" - ], - "referencedColumns": [ - "uid" - ] - }, - { - "table": "subscriptions", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "subscription_id" - ], - "referencedColumns": [ - "uid" - ] - } - ] - }, - { - "tableName": "feed_group", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "icon", - "columnName": "icon_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "sortOrder", - "columnName": "sort_order", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "uid" - ] - }, - "indices": [ - { - "name": "index_feed_group_sort_order", - "unique": false, - "columnNames": [ - "sort_order" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "feed_group_subscription_join", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`group_id`, `subscription_id`), FOREIGN KEY(`group_id`) REFERENCES `feed_group`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", - "fields": [ - { - "fieldPath": "feedGroupId", - "columnName": "group_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "subscriptionId", - "columnName": "subscription_id", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "group_id", - "subscription_id" - ] - }, - "indices": [ - { - "name": "index_feed_group_subscription_join_subscription_id", - "unique": false, - "columnNames": [ - "subscription_id" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)" - } - ], - "foreignKeys": [ - { - "table": "feed_group", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "group_id" - ], - "referencedColumns": [ - "uid" - ] - }, - { - "table": "subscriptions", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "subscription_id" - ], - "referencedColumns": [ - "uid" - ] - } - ] - }, - { - "tableName": "feed_last_updated", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subscription_id` INTEGER NOT NULL, `last_updated` INTEGER, PRIMARY KEY(`subscription_id`), FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", - "fields": [ - { - "fieldPath": "subscriptionId", - "columnName": "subscription_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "lastUpdated", - "columnName": "last_updated", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "subscription_id" - ] - }, - "indices": [], - "foreignKeys": [ - { - "table": "subscriptions", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "subscription_id" - ], - "referencedColumns": [ - "uid" - ] - } - ] - }, - { - "tableName": "downloaded_streams", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `stream_uid` INTEGER NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT NOT NULL, `file_uri` TEXT NOT NULL, `parent_uri` TEXT, `display_name` TEXT, `mime` TEXT, `size_bytes` INTEGER, `quality_label` TEXT, `duration_ms` INTEGER, `status` INTEGER NOT NULL, `added_at` INTEGER NOT NULL, `last_checked_at` INTEGER, `missing_since` INTEGER, FOREIGN KEY(`stream_uid`) REFERENCES `streams`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "streamUid", - "columnName": "stream_uid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "serviceId", - "columnName": "service_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "url", - "columnName": "url", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "fileUri", - "columnName": "file_uri", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "parentUri", - "columnName": "parent_uri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "displayName", - "columnName": "display_name", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "mime", - "columnName": "mime", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "sizeBytes", - "columnName": "size_bytes", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "qualityLabel", - "columnName": "quality_label", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "durationMs", - "columnName": "duration_ms", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "addedAt", - "columnName": "added_at", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "lastCheckedAt", - "columnName": "last_checked_at", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "missingSince", - "columnName": "missing_since", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_downloaded_streams_stream_uid", - "unique": true, - "columnNames": [ - "stream_uid" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_downloaded_streams_stream_uid` ON `${TABLE_NAME}` (`stream_uid`)" - } - ], - "foreignKeys": [ - { - "table": "streams", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "stream_uid" - ], - "referencedColumns": [ - "uid" - ] - } - ] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '92195bb0de0864bb1a0d7e4bbb16ec0f')" - ] - } -} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/App.kt b/app/src/main/java/org/schabi/newpipe/App.kt index 4a0664117..a34caa957 100644 --- a/app/src/main/java/org/schabi/newpipe/App.kt +++ b/app/src/main/java/org/schabi/newpipe/App.kt @@ -25,7 +25,6 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins import org.acra.ACRA.init import org.acra.ACRA.isACRASenderServiceProcess import org.acra.config.CoreConfigurationBuilder -import org.schabi.newpipe.download.DownloadMaintenance import org.schabi.newpipe.error.ReCaptchaActivity import org.schabi.newpipe.extractor.NewPipe import org.schabi.newpipe.extractor.downloader.Downloader @@ -121,8 +120,6 @@ open class App : configureRxJavaErrorHandler() YoutubeStreamExtractor.setPoTokenProvider(PoTokenProviderImpl) - - DownloadMaintenance.schedule(this) } override fun newImageLoader(context: Context): ImageLoader = diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java index ea1dbdf0c..21c5354f4 100644 --- a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java @@ -9,7 +9,6 @@ import static org.schabi.newpipe.database.Migrations.MIGRATION_5_6; import static org.schabi.newpipe.database.Migrations.MIGRATION_6_7; import static org.schabi.newpipe.database.Migrations.MIGRATION_7_8; import static org.schabi.newpipe.database.Migrations.MIGRATION_8_9; -import static org.schabi.newpipe.database.Migrations.MIGRATION_9_10; import android.content.Context; import android.database.Cursor; @@ -30,7 +29,7 @@ public final class NewPipeDatabase { return Room .databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME) .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, - MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9, MIGRATION_9_10) + MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9) .build(); } diff --git a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java index 8d5b951a1..04d93a238 100644 --- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java @@ -1,13 +1,11 @@ package org.schabi.newpipe.database; -import static org.schabi.newpipe.database.Migrations.DB_VER_10; +import static org.schabi.newpipe.database.Migrations.DB_VER_9; import androidx.room.Database; import androidx.room.RoomDatabase; import androidx.room.TypeConverters; -import org.schabi.newpipe.database.download.DownloadedStreamEntity; -import org.schabi.newpipe.database.download.DownloadedStreamsDao; import org.schabi.newpipe.database.feed.dao.FeedDAO; import org.schabi.newpipe.database.feed.dao.FeedGroupDAO; import org.schabi.newpipe.database.feed.model.FeedEntity; @@ -38,9 +36,9 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity; StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class, PlaylistEntity.class, PlaylistStreamEntity.class, PlaylistRemoteEntity.class, FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class, - FeedLastUpdatedEntity.class, DownloadedStreamEntity.class + FeedLastUpdatedEntity.class }, - version = DB_VER_10 + version = DB_VER_9 ) public abstract class AppDatabase extends RoomDatabase { public static final String DATABASE_NAME = "newpipe.db"; @@ -64,6 +62,4 @@ public abstract class AppDatabase extends RoomDatabase { public abstract FeedGroupDAO feedGroupDAO(); public abstract SubscriptionDAO subscriptionDAO(); - - public abstract DownloadedStreamsDao downloadedStreamsDao(); } diff --git a/app/src/main/java/org/schabi/newpipe/database/Converters.kt b/app/src/main/java/org/schabi/newpipe/database/Converters.kt index 95af4297b..ec097cc1b 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Converters.kt +++ b/app/src/main/java/org/schabi/newpipe/database/Converters.kt @@ -1,7 +1,6 @@ package org.schabi.newpipe.database import androidx.room.TypeConverter -import org.schabi.newpipe.database.download.DownloadedStreamStatus import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.local.subscription.FeedGroupIcon import java.time.Instant @@ -50,14 +49,4 @@ class Converters { fun feedGroupIconOf(id: Int): FeedGroupIcon { return FeedGroupIcon.entries.first { it.id == id } } - - @TypeConverter - fun downloadedStreamStatusOf(value: Int?): DownloadedStreamStatus? { - return value?.let { DownloadedStreamStatus.fromValue(it) } - } - - @TypeConverter - fun integerOf(status: DownloadedStreamStatus?): Int? { - return status?.value - } } diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.java b/app/src/main/java/org/schabi/newpipe/database/Migrations.java index a2f2171e7..c9f630869 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.java @@ -27,7 +27,6 @@ public final class Migrations { public static final int DB_VER_7 = 7; public static final int DB_VER_8 = 8; public static final int DB_VER_9 = 9; - public static final int DB_VER_10 = 10; private static final String TAG = Migrations.class.getName(); public static final boolean DEBUG = MainActivity.DEBUG; @@ -303,22 +302,6 @@ public final class Migrations { } }; - public static final Migration MIGRATION_9_10 = new Migration(DB_VER_9, DB_VER_10) { - @Override - public void migrate(@NonNull final SupportSQLiteDatabase database) { - database.execSQL("CREATE TABLE IF NOT EXISTS downloaded_streams " - + "(id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " - + "stream_uid INTEGER NOT NULL, service_id INTEGER NOT NULL, " - + "url TEXT NOT NULL, file_uri TEXT NOT NULL, parent_uri TEXT, " - + "display_name TEXT, mime TEXT, size_bytes INTEGER, quality_label TEXT, " - + "duration_ms INTEGER, status INTEGER NOT NULL, added_at INTEGER NOT NULL, " - + "last_checked_at INTEGER, missing_since INTEGER, FOREIGN KEY(stream_uid) " - + "REFERENCES streams(uid) ON UPDATE CASCADE ON DELETE CASCADE)"); - database.execSQL("CREATE UNIQUE INDEX index_downloaded_streams_stream_uid " - + "ON downloaded_streams (stream_uid)"); - } - }; - private Migrations() { } } diff --git a/app/src/main/java/org/schabi/newpipe/database/download/DownloadedStreamEntity.kt b/app/src/main/java/org/schabi/newpipe/database/download/DownloadedStreamEntity.kt deleted file mode 100644 index febc9f9d4..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/download/DownloadedStreamEntity.kt +++ /dev/null @@ -1,103 +0,0 @@ -package org.schabi.newpipe.database.download - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.Index -import androidx.room.PrimaryKey -import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_ADDED_AT -import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_DISPLAY_NAME -import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_DURATION_MS -import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_FILE_URI -import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_ID -import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_LAST_CHECKED_AT -import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_MIME -import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_MISSING_SINCE -import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_PARENT_URI -import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_QUALITY_LABEL -import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_SERVICE_ID -import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_SIZE_BYTES -import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_STATUS -import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_STREAM_UID -import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_URL -import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.TABLE_NAME -import org.schabi.newpipe.database.stream.model.StreamEntity - -@Entity( - tableName = TABLE_NAME, - indices = [Index(value = [COLUMN_STREAM_UID], unique = true)], - foreignKeys = [ - ForeignKey( - entity = StreamEntity::class, - parentColumns = [StreamEntity.STREAM_ID], - childColumns = [COLUMN_STREAM_UID], - onDelete = ForeignKey.CASCADE - ) - ] -) -data class DownloadedStreamEntity( - @PrimaryKey(autoGenerate = true) - @ColumnInfo(name = COLUMN_ID) - var id: Long = 0, - - @ColumnInfo(name = COLUMN_STREAM_UID) - var streamUid: Long, - - @ColumnInfo(name = COLUMN_SERVICE_ID) - var serviceId: Int, - - @ColumnInfo(name = COLUMN_URL) - var url: String, - - @ColumnInfo(name = COLUMN_FILE_URI) - var fileUri: String, - - @ColumnInfo(name = COLUMN_PARENT_URI) - var parentUri: String? = null, - - @ColumnInfo(name = COLUMN_DISPLAY_NAME) - var displayName: String? = null, - - @ColumnInfo(name = COLUMN_MIME) - var mime: String? = null, - - @ColumnInfo(name = COLUMN_SIZE_BYTES) - var sizeBytes: Long? = null, - - @ColumnInfo(name = COLUMN_QUALITY_LABEL) - var qualityLabel: String? = null, - - @ColumnInfo(name = COLUMN_DURATION_MS) - var durationMs: Long? = null, - - @ColumnInfo(name = COLUMN_STATUS) - var status: DownloadedStreamStatus, - - @ColumnInfo(name = COLUMN_ADDED_AT) - var addedAt: Long, - - @ColumnInfo(name = COLUMN_LAST_CHECKED_AT) - var lastCheckedAt: Long? = null, - - @ColumnInfo(name = COLUMN_MISSING_SINCE) - var missingSince: Long? = null -) { - companion object { - const val TABLE_NAME = "downloaded_streams" - const val COLUMN_ID = "id" - const val COLUMN_STREAM_UID = "stream_uid" - const val COLUMN_SERVICE_ID = "service_id" - const val COLUMN_URL = "url" - const val COLUMN_FILE_URI = "file_uri" - const val COLUMN_PARENT_URI = "parent_uri" - const val COLUMN_DISPLAY_NAME = "display_name" - const val COLUMN_MIME = "mime" - const val COLUMN_SIZE_BYTES = "size_bytes" - const val COLUMN_QUALITY_LABEL = "quality_label" - const val COLUMN_DURATION_MS = "duration_ms" - const val COLUMN_STATUS = "status" - const val COLUMN_ADDED_AT = "added_at" - const val COLUMN_LAST_CHECKED_AT = "last_checked_at" - const val COLUMN_MISSING_SINCE = "missing_since" - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/download/DownloadedStreamStatus.kt b/app/src/main/java/org/schabi/newpipe/database/download/DownloadedStreamStatus.kt deleted file mode 100644 index c890a657e..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/download/DownloadedStreamStatus.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.schabi.newpipe.database.download - -enum class DownloadedStreamStatus(val value: Int) { - IN_PROGRESS(0), - AVAILABLE(1), - MISSING(2), - UNLINKED(3); - - companion object { - fun fromValue(value: Int): DownloadedStreamStatus = entries.firstOrNull { - it.value == value - } ?: IN_PROGRESS - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/download/DownloadedStreamsDao.kt b/app/src/main/java/org/schabi/newpipe/database/download/DownloadedStreamsDao.kt deleted file mode 100644 index 5c6535e25..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/download/DownloadedStreamsDao.kt +++ /dev/null @@ -1,58 +0,0 @@ -package org.schabi.newpipe.database.download - -import androidx.room.Dao -import androidx.room.Delete -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import androidx.room.Transaction -import androidx.room.Update -import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.core.Maybe - -@Dao -interface DownloadedStreamsDao { - @Query("SELECT * FROM downloaded_streams WHERE stream_uid = :streamUid LIMIT 1") - fun observeByStreamUid(streamUid: Long): Flowable> - - @Query("SELECT * FROM downloaded_streams WHERE stream_uid = :streamUid LIMIT 1") - fun getByStreamUid(streamUid: Long): Maybe - - @Insert(onConflict = OnConflictStrategy.IGNORE) - fun insert(entity: DownloadedStreamEntity): Long - - @Update - fun update(entity: DownloadedStreamEntity): Int - - @Query("SELECT * FROM downloaded_streams WHERE stream_uid = :streamUid LIMIT 1") - fun findEntityByStreamUid(streamUid: Long): DownloadedStreamEntity? - - @Query("SELECT * FROM downloaded_streams WHERE id = :id LIMIT 1") - fun findEntityById(id: Long): DownloadedStreamEntity? - - @Transaction - fun insertOrUpdate(entity: DownloadedStreamEntity): Long { - val newId = insert(entity) - if (newId != -1L) { - entity.id = newId - return newId - } - update(entity) - return entity.id - } - - @Query("UPDATE downloaded_streams SET status = :status, last_checked_at = :lastCheckedAt, missing_since = :missingSince WHERE id = :id") - fun updateStatus(id: Long, status: DownloadedStreamStatus, lastCheckedAt: Long?, missingSince: Long?) - - @Query("UPDATE downloaded_streams SET file_uri = :fileUri WHERE id = :id") - fun updateFileUri(id: Long, fileUri: String) - - @Delete - fun delete(entity: DownloadedStreamEntity) - - @Query("DELETE FROM downloaded_streams WHERE stream_uid = :streamUid") - fun deleteByStreamUid(streamUid: Long): Int - - @Query("SELECT * FROM downloaded_streams WHERE status = :status ORDER BY last_checked_at ASC LIMIT :limit") - fun listByStatus(status: DownloadedStreamStatus, limit: Int): List -} diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java index d6c0c15b9..33702a6a3 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java @@ -81,14 +81,6 @@ public class DownloadActivity extends AppCompatActivity { return true; } - @Override - protected void onResume() { - super.onResume(); - new Thread(() -> - DownloadMaintenance.revalidateAvailable(DownloadActivity.this, 10) - ).start(); - } - @Override public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadAvailabilityChecker.kt b/app/src/main/java/org/schabi/newpipe/download/DownloadAvailabilityChecker.kt deleted file mode 100644 index ac39cf32b..000000000 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadAvailabilityChecker.kt +++ /dev/null @@ -1,33 +0,0 @@ -package org.schabi.newpipe.download - -import android.content.Context -import android.net.Uri -import android.util.Log -import org.schabi.newpipe.BuildConfig -import java.io.File - -object DownloadAvailabilityChecker { - private const val TAG = "DownloadAvailabilityChecker" - - fun isReadable(context: Context, uri: Uri): Boolean { - val scheme = uri.scheme - return when { - scheme.equals("file", ignoreCase = true) -> - File(uri.path ?: return false).canRead() - scheme.equals("content", ignoreCase = true) -> - probeContentUri(context, uri) - else -> probeContentUri(context, uri) - } - } - - private fun probeContentUri(context: Context, uri: Uri): Boolean { - return try { - context.contentResolver.openAssetFileDescriptor(uri, "r")?.use { true } ?: false - } catch (throwable: Throwable) { - if (BuildConfig.DEBUG) { - Log.w(TAG, "Failed to probe availability for $uri", throwable) - } - false - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index a43738e1f..98cfbb25c 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -79,9 +79,7 @@ import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.Optional; -import java.util.concurrent.TimeUnit; -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.disposables.CompositeDisposable; import us.shandian.giga.get.MissionRecoveryInfo; import us.shandian.giga.postprocessing.Postprocessing; @@ -1135,50 +1133,27 @@ public class DownloadDialog extends DialogFragment } final String qualityLabel = buildQualityLabel(selectedStream); - final MediaFormat selectedFormat = selectedStream.getFormat(); - final String resolvedMime = selectedFormat != null ? selectedFormat.getMimeType() - : storage.getType(); - final Long durationMs = currentInfo.getDuration() > 0 - ? TimeUnit.SECONDS.toMillis(currentInfo.getDuration()) : null; - final Long estimatedSize = nearLength > 0 ? nearLength : null; - final char missionKind = kind; - final int missionThreads = threads; - final String missionSourceUrl = currentInfo.getUrl(); - final String missionPsName = psName; - final String[] missionPsArgs = psArgs; - final long missionNearLength = nearLength; + DownloadManagerService.startMission( + context, + urls, + storage, + kind, + threads, + currentInfo.getUrl(), + psName, + psArgs, + nearLength, + new ArrayList<>(recoveryInfo), + -1L, + currentInfo.getServiceId(), + qualityLabel + ); - disposables.add(DownloadedStreamsRepository.INSTANCE - .upsertForEnqueued(requireContext(), currentInfo, storage, null, resolvedMime, - qualityLabel, durationMs, estimatedSize) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(association -> { - DownloadManagerService.startMission( - context, - urls, - storage, - missionKind, - missionThreads, - missionSourceUrl, - missionPsName, - missionPsArgs, - missionNearLength, - new ArrayList<>(recoveryInfo), - association.getStreamUid(), - association.getEntityId(), - currentInfo.getServiceId() - ); + Toast.makeText(context, getString(R.string.download_has_started), + Toast.LENGTH_SHORT).show(); - Toast.makeText(context, getString(R.string.download_has_started), - Toast.LENGTH_SHORT).show(); - - dismiss(); - }, - throwable -> ErrorUtil.createNotification(requireContext(), - new ErrorInfo(throwable, UserAction.DOWNLOAD_FAILED, - "Preparing download metadata", currentInfo)) - )); + dismiss(); } @Nullable diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadMaintenance.kt b/app/src/main/java/org/schabi/newpipe/download/DownloadMaintenance.kt deleted file mode 100644 index 44f0a96cf..000000000 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadMaintenance.kt +++ /dev/null @@ -1,45 +0,0 @@ -package org.schabi.newpipe.download - -import android.content.Context -import android.net.Uri -import androidx.work.ExistingPeriodicWorkPolicy -import androidx.work.PeriodicWorkRequestBuilder -import androidx.work.WorkManager -import org.schabi.newpipe.NewPipeDatabase -import org.schabi.newpipe.database.download.DownloadedStreamStatus -import java.util.concurrent.TimeUnit - -object DownloadMaintenance { - private const val WORK_NAME = "download_revalidation" - - @JvmStatic - fun revalidateAvailable(context: Context, limit: Int = 25) { - val dao = NewPipeDatabase.getInstance(context).downloadedStreamsDao() - val entries = dao.listByStatus(DownloadedStreamStatus.AVAILABLE, limit) - if (entries.isEmpty()) return - - val now = System.currentTimeMillis() - for (entry in entries) { - val uriString = entry.fileUri - if (uriString.isBlank()) { - dao.updateStatus(entry.id, DownloadedStreamStatus.MISSING, now, entry.missingSince ?: now) - continue - } - - val available = DownloadAvailabilityChecker.isReadable(context, Uri.parse(uriString)) - if (available) { - dao.updateStatus(entry.id, DownloadedStreamStatus.AVAILABLE, now, null) - } else { - dao.updateStatus(entry.id, DownloadedStreamStatus.MISSING, now, entry.missingSince ?: now) - } - } - } - - @JvmStatic - fun schedule(context: Context) { - val workRequest = PeriodicWorkRequestBuilder(1, TimeUnit.DAYS) - .build() - WorkManager.getInstance(context) - .enqueueUniquePeriodicWork(WORK_NAME, ExistingPeriodicWorkPolicy.KEEP, workRequest) - } -} diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadRevalidationWorker.kt b/app/src/main/java/org/schabi/newpipe/download/DownloadRevalidationWorker.kt deleted file mode 100644 index 5c80a28ac..000000000 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadRevalidationWorker.kt +++ /dev/null @@ -1,29 +0,0 @@ -package org.schabi.newpipe.download - -import android.content.Context -import android.util.Log -import androidx.work.Worker -import androidx.work.WorkerParameters -import org.schabi.newpipe.BuildConfig - -class DownloadRevalidationWorker( - appContext: Context, - workerParams: WorkerParameters, -) : Worker(appContext, workerParams) { - - override fun doWork(): Result { - return try { - DownloadMaintenance.revalidateAvailable(applicationContext) - Result.success() - } catch (throwable: Throwable) { - if (BuildConfig.DEBUG) { - Log.e(TAG, "Failed to revalidate downloads", throwable) - } - Result.retry() - } - } - - private companion object { - private const val TAG = "DownloadRevalidation" - } -} diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadStatusRepository.kt b/app/src/main/java/org/schabi/newpipe/download/DownloadStatusRepository.kt new file mode 100644 index 000000000..4002ef42c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadStatusRepository.kt @@ -0,0 +1,207 @@ +package org.schabi.newpipe.download + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.net.Uri +import android.os.Handler +import android.os.IBinder +import android.os.Message +import androidx.annotation.MainThread +import androidx.annotation.VisibleForTesting +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.suspendCancellableCoroutine +import us.shandian.giga.get.DownloadMission +import us.shandian.giga.get.FinishedMission +import us.shandian.giga.service.DownloadManager +import us.shandian.giga.service.DownloadManagerService +import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder +import us.shandian.giga.service.MissionState +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +sealed interface DownloadStatus { + data object None : DownloadStatus + data class InProgress(val running: Boolean) : DownloadStatus + data class Completed(val info: CompletedDownload) : DownloadStatus +} + +data class CompletedDownload( + val displayName: String?, + val qualityLabel: String?, + val mimeType: String?, + val fileUri: Uri?, + val parentUri: Uri?, + val fileAvailable: Boolean +) + +object DownloadStatusRepository { + + fun observe(context: Context, serviceId: Int, url: String): Flow = callbackFlow { + if (serviceId < 0 || url.isBlank()) { + trySend(DownloadStatus.None) + close() + return@callbackFlow + } + + val appContext = context.applicationContext + val intent = Intent(appContext, DownloadManagerService::class.java) + appContext.startService(intent) + var binder: DownloadManagerBinder? = null + var registeredCallback: Handler.Callback? = null + + val connection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + val downloadBinder = service as? DownloadManagerBinder + if (downloadBinder == null) { + trySend(DownloadStatus.None) + appContext.unbindService(this) + close() + return + } + binder = downloadBinder + trySend(downloadBinder.getDownloadStatus(serviceId, url, false).toDownloadStatus()) + + val callback = Handler.Callback { message: Message -> + val mission = message.obj + if (mission.matches(serviceId, url)) { + val snapshot = downloadBinder.getDownloadStatus(serviceId, url, false) + trySend(snapshot.toDownloadStatus()) + } + false + } + registeredCallback = callback + downloadBinder.addMissionEventListener(callback) + } + + override fun onServiceDisconnected(name: ComponentName?) { + registeredCallback?.let { callback -> binder?.removeMissionEventListener(callback) } + binder = null + trySend(DownloadStatus.None) + } + } + + val bound = appContext.bindService(intent, connection, Context.BIND_AUTO_CREATE) + if (!bound) { + trySend(DownloadStatus.None) + close() + return@callbackFlow + } + + awaitClose { + registeredCallback?.let { callback -> binder?.removeMissionEventListener(callback) } + runCatching { appContext.unbindService(connection) } + } + } + + suspend fun refresh(context: Context, serviceId: Int, url: String): DownloadStatus { + if (serviceId < 0 || url.isBlank()) return DownloadStatus.None + return withBinder(context) { binder -> + binder.getDownloadStatus(serviceId, url, true).toDownloadStatus() + } + } + + suspend fun deleteFile(context: Context, serviceId: Int, url: String): Boolean { + if (serviceId < 0 || url.isBlank()) return false + return withBinder(context) { binder -> + binder.deleteFinishedMission(serviceId, url, true) + } + } + + suspend fun removeLink(context: Context, serviceId: Int, url: String): Boolean { + if (serviceId < 0 || url.isBlank()) return false + return withBinder(context) { binder -> + binder.deleteFinishedMission(serviceId, url, false) + } + } + + private suspend fun withBinder(context: Context, block: (DownloadManagerBinder) -> T): T { + val appContext = context.applicationContext + val intent = Intent(appContext, DownloadManagerService::class.java) + appContext.startService(intent) + return suspendCancellableCoroutine { continuation -> + val connection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + val binder = service as? DownloadManagerBinder + if (binder == null) { + if (continuation.isActive) { + continuation.resumeWithException(IllegalStateException("Download service binder is null")) + } + appContext.unbindService(this) + return + } + try { + val result = block(binder) + if (continuation.isActive) { + continuation.resume(result) + } + } catch (throwable: Throwable) { + if (continuation.isActive) { + continuation.resumeWithException(throwable) + } + } finally { + appContext.unbindService(this) + } + } + + override fun onServiceDisconnected(name: ComponentName?) { + if (continuation.isActive) { + continuation.resumeWithException(IllegalStateException("Download service disconnected")) + } + } + } + + val bound = appContext.bindService(intent, connection, Context.BIND_AUTO_CREATE) + if (!bound) { + continuation.resumeWithException(IllegalStateException("Unable to bind download service")) + return@suspendCancellableCoroutine + } + + continuation.invokeOnCancellation { + runCatching { appContext.unbindService(connection) } + } + } + } + + private fun Any?.matches(serviceId: Int, url: String): Boolean { + return when (this) { + is DownloadMission -> this.serviceId == serviceId && url == this.source + is FinishedMission -> this.serviceId == serviceId && url == this.source + else -> false + } + } + + @VisibleForTesting + @MainThread + internal fun DownloadManager.DownloadStatusSnapshot?.toDownloadStatus(): DownloadStatus { + if (this == null || state == MissionState.None) { + return DownloadStatus.None + } + return when (state) { + MissionState.Pending, MissionState.PendingRunning -> + DownloadStatus.InProgress(state == MissionState.PendingRunning) + MissionState.Finished -> { + val mission = finishedMission + if (mission == null) { + DownloadStatus.None + } else { + val storage = mission.storage + val hasStorage = storage != null && !storage.isInvalid() + val info = CompletedDownload( + displayName = storage?.getName(), + qualityLabel = mission.qualityLabel, + mimeType = if (hasStorage) storage!!.getType() else null, + fileUri = if (hasStorage) storage!!.getUri() else null, + parentUri = if (hasStorage) storage!!.getParentUri() else null, + fileAvailable = fileExists && hasStorage + ) + DownloadStatus.Completed(info) + } + } + else -> DownloadStatus.None + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadedStreamsRepository.kt b/app/src/main/java/org/schabi/newpipe/download/DownloadedStreamsRepository.kt deleted file mode 100644 index 7d2354830..000000000 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadedStreamsRepository.kt +++ /dev/null @@ -1,238 +0,0 @@ -package org.schabi.newpipe.download - -import android.content.Context -import android.net.Uri -import io.reactivex.rxjava3.core.Completable -import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.core.Maybe -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers -import org.schabi.newpipe.NewPipeDatabase -import org.schabi.newpipe.database.AppDatabase -import org.schabi.newpipe.database.download.DownloadedStreamEntity -import org.schabi.newpipe.database.download.DownloadedStreamStatus -import org.schabi.newpipe.database.download.DownloadedStreamsDao -import org.schabi.newpipe.database.stream.model.StreamEntity -import org.schabi.newpipe.extractor.stream.StreamInfo -import org.schabi.newpipe.streams.io.StoredFileHelper - -object DownloadedStreamsRepository { - - data class DownloadAssociation( - val streamUid: Long, - val entityId: Long - ) - - private fun database(context: Context): AppDatabase { - return NewPipeDatabase.getInstance(context) - } - - private fun downloadedDao(context: Context): DownloadedStreamsDao { - return database(context).downloadedStreamsDao() - } - - fun observeByStreamUid(context: Context, streamUid: Long): Flowable> { - return downloadedDao(context) - .observeByStreamUid(streamUid) - .subscribeOn(Schedulers.io()) - } - - fun getByStreamUid(context: Context, streamUid: Long): Maybe { - return downloadedDao(context) - .getByStreamUid(streamUid) - .subscribeOn(Schedulers.io()) - } - - fun ensureStreamEntry(context: Context, info: StreamInfo): Single { - return Single.fromCallable { - database(context).streamDAO().upsert(StreamEntity(info)) - }.subscribeOn(Schedulers.io()) - } - - fun upsertForEnqueued( - context: Context, - info: StreamInfo, - storage: StoredFileHelper, - displayName: String?, - mime: String?, - qualityLabel: String?, - durationMs: Long?, - sizeBytes: Long? - ): Single { - return Single.fromCallable { - val db = database(context) - db.runInTransaction { - val streamDao = db.streamDAO() - val dao = db.downloadedStreamsDao() - val streamId = streamDao.upsert(StreamEntity(info)) - val now = System.currentTimeMillis() - val fileUri = storage.uriString() - val entity = dao.findEntityByStreamUid(streamId) - val resolvedDisplayName = displayName ?: storage.getName() - val resolvedMime = mime ?: storage.getType() - - if (entity == null) { - val newEntity = DownloadedStreamEntity( - streamUid = streamId, - serviceId = info.serviceId, - url = info.url, - fileUri = fileUri, - parentUri = storage.parentUriString(), - displayName = resolvedDisplayName, - mime = resolvedMime, - sizeBytes = sizeBytes, - qualityLabel = qualityLabel, - durationMs = durationMs, - status = DownloadedStreamStatus.IN_PROGRESS, - addedAt = now, - lastCheckedAt = null, - missingSince = null - ) - val insertedId = dao.insert(newEntity) - val resolvedId = if (insertedId == -1L) { - dao.findEntityByStreamUid(streamId)?.id - ?: throw IllegalStateException("Failed to resolve downloaded stream entry") - } else { - insertedId - } - newEntity.id = resolvedId - DownloadAssociation(streamId, resolvedId) - } else { - entity.serviceId = info.serviceId - entity.url = info.url - entity.fileUri = fileUri - val parentUri = storage.parentUriString() - if (parentUri != null) { - entity.parentUri = parentUri - } - entity.displayName = resolvedDisplayName - entity.mime = resolvedMime - entity.sizeBytes = sizeBytes - entity.qualityLabel = qualityLabel - entity.durationMs = durationMs - entity.status = DownloadedStreamStatus.IN_PROGRESS - entity.lastCheckedAt = null - entity.missingSince = null - if (entity.addedAt <= 0) { - entity.addedAt = now - } - dao.update(entity) - DownloadAssociation(streamId, entity.id) - } - } - }.subscribeOn(Schedulers.io()) - } - - fun markFinished( - context: Context, - association: DownloadAssociation, - serviceId: Int, - url: String, - storage: StoredFileHelper, - mime: String?, - qualityLabel: String?, - durationMs: Long?, - sizeBytes: Long? - ): Completable { - return Completable.fromAction { - val dao = downloadedDao(context) - val now = System.currentTimeMillis() - val entity = dao.findEntityById(association.entityId) - ?: dao.findEntityByStreamUid(association.streamUid) - ?: DownloadedStreamEntity( - streamUid = association.streamUid, - serviceId = serviceId, - url = url, - fileUri = storage.uriString(), - parentUri = storage.parentUriString(), - displayName = storage.getName(), - mime = mime ?: storage.getType(), - sizeBytes = sizeBytes, - qualityLabel = qualityLabel, - durationMs = durationMs, - status = DownloadedStreamStatus.IN_PROGRESS, - addedAt = now - ) - entity.serviceId = serviceId - entity.url = url - entity.fileUri = storage.uriString() - storage.parentUriString()?.let { entity.parentUri = it } - entity.displayName = storage.getName() - val resolvedMime = mime ?: storage.getType() ?: entity.mime - entity.mime = resolvedMime - entity.sizeBytes = sizeBytes ?: storage.safeLength() ?: entity.sizeBytes - if (qualityLabel != null) { - entity.qualityLabel = qualityLabel - } - if (durationMs != null) { - entity.durationMs = durationMs - } - entity.status = DownloadedStreamStatus.AVAILABLE - entity.lastCheckedAt = now - entity.missingSince = null - if (entity.addedAt <= 0) { - entity.addedAt = now - } - - if (entity.id == 0L) { - val newId = dao.insert(entity) - entity.id = newId - } else { - dao.update(entity) - } - }.subscribeOn(Schedulers.io()) - } - - fun updateStatus( - context: Context, - entityId: Long, - status: DownloadedStreamStatus, - lastCheckedAt: Long? = System.currentTimeMillis(), - missingSince: Long? = null - ): Completable { - return Completable.fromAction { - downloadedDao(context).updateStatus(entityId, status, lastCheckedAt, missingSince) - }.subscribeOn(Schedulers.io()) - } - - fun updateFileUri(context: Context, entityId: Long, uri: Uri): Completable { - return Completable.fromAction { - downloadedDao(context).updateFileUri(entityId, uri.toString()) - }.subscribeOn(Schedulers.io()) - } - - fun relink(context: Context, entity: DownloadedStreamEntity, uri: Uri): Completable { - return Single.fromCallable { - StoredFileHelper(context, uri, entity.mime ?: StoredFileHelper.DEFAULT_MIME) - }.flatMapCompletable { helper -> - val association = DownloadAssociation(entity.streamUid, entity.id) - markFinished( - context, - association, - entity.serviceId, - entity.url, - helper, - helper.type, - entity.qualityLabel, - entity.durationMs, - helper.safeLength() - ) - }.subscribeOn(Schedulers.io()) - } - - fun deleteByStreamUid(context: Context, streamUid: Long): Completable { - return Completable.fromAction { - downloadedDao(context).deleteByStreamUid(streamUid) - }.subscribeOn(Schedulers.io()) - } - - private fun StoredFileHelper.uriString(): String = getUri().toString() - - private fun StoredFileHelper.safeLength(): Long? { - return runCatching { length() }.getOrNull() - } - - private fun StoredFileHelper.parentUriString(): String? { - return runCatching { getParentUri() }.getOrNull()?.toString() - } -} diff --git a/app/src/main/java/org/schabi/newpipe/download/ui/DownloadStatusUi.kt b/app/src/main/java/org/schabi/newpipe/download/ui/DownloadStatusUi.kt new file mode 100644 index 000000000..ef36b7d09 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/download/ui/DownloadStatusUi.kt @@ -0,0 +1,126 @@ +package org.schabi.newpipe.download.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.AssistChip +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.schabi.newpipe.R +import org.schabi.newpipe.download.CompletedDownload +import org.schabi.newpipe.fragments.detail.DownloadChipState +import org.schabi.newpipe.fragments.detail.DownloadUiState + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DownloadStatusHost( + state: DownloadUiState, + onChipClick: () -> Unit, + onDismissSheet: () -> Unit, + onOpenFile: (CompletedDownload) -> Unit, + onDeleteFile: (CompletedDownload) -> Unit, + onRemoveLink: (CompletedDownload) -> Unit, + onShowInFolder: (CompletedDownload) -> Unit +) { + val chipState = state.chipState + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + if (state.isSheetVisible && chipState is DownloadChipState.Downloaded) { + ModalBottomSheet( + onDismissRequest = onDismissSheet, + sheetState = sheetState + ) { + DownloadSheetContent( + info = chipState.info, + onOpenFile = { onOpenFile(chipState.info) }, + onDeleteFile = { onDeleteFile(chipState.info) }, + onRemoveLink = { onRemoveLink(chipState.info) }, + onShowInFolder = { onShowInFolder(chipState.info) } + ) + } + } + + when (chipState) { + DownloadChipState.Hidden -> Unit + DownloadChipState.Downloading -> AssistChip( + onClick = onChipClick, + label = { Text(text = stringResource(id = R.string.download_status_downloading)) } + ) + is DownloadChipState.Downloaded -> { + val label = chipState.info.qualityLabel + val text = if (!label.isNullOrBlank()) { + stringResource(R.string.download_status_downloaded, label) + } else { + stringResource(R.string.download_status_downloaded_simple) + } + AssistChip( + onClick = onChipClick, + label = { Text(text = text) } + ) + } + } +} + +@Composable +private fun DownloadSheetContent( + info: CompletedDownload, + onOpenFile: () -> Unit, + onDeleteFile: () -> Unit, + onRemoveLink: () -> Unit, + onShowInFolder: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 16.dp) + ) { + val title = info.displayName ?: stringResource(id = R.string.download) + Text(text = title, style = MaterialTheme.typography.titleLarge) + + val subtitleParts = buildList { + info.qualityLabel?.takeIf { it.isNotBlank() }?.let { add(it) } + if (!info.fileAvailable) { + add(stringResource(id = R.string.download_status_missing)) + } + } + if (subtitleParts.isNotEmpty()) { + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = subtitleParts.joinToString(" • "), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + Spacer(modifier = Modifier.height(12.dp)) + + val showFileActions = info.fileAvailable && info.fileUri != null + if (showFileActions) { + TextButton(onClick = onOpenFile) { + Text(text = stringResource(id = R.string.download_action_open)) + } + TextButton(onClick = onShowInFolder, enabled = info.parentUri != null) { + Text(text = stringResource(id = R.string.download_action_show_in_folder)) + } + TextButton(onClick = onDeleteFile) { + Text(text = stringResource(id = R.string.download_action_delete)) + } + } + + TextButton(onClick = onRemoveLink) { + Text(text = stringResource(id = R.string.download_action_remove_link), color = MaterialTheme.colorScheme.error) + } + + Spacer(modifier = Modifier.height(8.dp)) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.kt b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.kt index 73412eba3..88382bc62 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.kt @@ -4,7 +4,6 @@ import android.animation.ValueAnimator import android.annotation.SuppressLint import android.app.Activity import android.content.BroadcastReceiver -import android.content.ContentResolver import android.content.Context import android.content.Intent import android.content.IntentFilter @@ -13,7 +12,6 @@ import android.content.pm.ActivityInfo import android.database.ContentObserver import android.graphics.Color import android.graphics.Rect -import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Handler @@ -23,7 +21,6 @@ import android.provider.Settings import android.util.DisplayMetrics import android.util.Log import android.util.TypedValue -import android.view.ContextThemeWrapper import android.view.LayoutInflater import android.view.MotionEvent import android.view.View @@ -35,14 +32,14 @@ import android.view.WindowManager import android.view.animation.DecelerateInterpolator import android.widget.FrameLayout import android.widget.RelativeLayout -import android.widget.TextView import android.widget.Toast -import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.AttrRes import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.widget.Toolbar +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.ContextCompat import androidx.core.content.edit @@ -50,7 +47,9 @@ import androidx.core.net.toUri import androidx.core.os.postDelayed import androidx.core.view.isGone import androidx.core.view.isVisible -import androidx.documentfile.provider.DocumentFile +import androidx.fragment.app.viewModels +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import coil3.util.CoilUtils import com.evernote.android.state.State @@ -59,22 +58,18 @@ import com.google.android.exoplayer2.PlaybackParameters import com.google.android.material.appbar.AppBarLayout import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback -import com.google.android.material.bottomsheet.BottomSheetDialog import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.schedulers.Schedulers +import kotlinx.coroutines.launch import org.schabi.newpipe.App import org.schabi.newpipe.R -import org.schabi.newpipe.database.download.DownloadedStreamEntity -import org.schabi.newpipe.database.download.DownloadedStreamStatus import org.schabi.newpipe.database.stream.model.StreamEntity -import org.schabi.newpipe.databinding.DownloadStatusSheetBinding import org.schabi.newpipe.databinding.FragmentVideoDetailBinding -import org.schabi.newpipe.download.DownloadActivity -import org.schabi.newpipe.download.DownloadAvailabilityChecker +import org.schabi.newpipe.download.CompletedDownload import org.schabi.newpipe.download.DownloadDialog -import org.schabi.newpipe.download.DownloadedStreamsRepository +import org.schabi.newpipe.download.ui.DownloadStatusHost import org.schabi.newpipe.error.ErrorInfo import org.schabi.newpipe.error.ErrorUtil.Companion.showSnackbar import org.schabi.newpipe.error.ErrorUtil.Companion.showUiErrorSnackbar @@ -113,6 +108,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueue import org.schabi.newpipe.player.playqueue.SinglePlayQueue import org.schabi.newpipe.player.ui.MainPlayerUi import org.schabi.newpipe.player.ui.VideoPlayerUi +import org.schabi.newpipe.ui.theme.AppTheme import org.schabi.newpipe.util.DependentPreferenceHelper import org.schabi.newpipe.util.DeviceUtils import org.schabi.newpipe.util.ExtractorHelper @@ -129,7 +125,6 @@ import org.schabi.newpipe.util.ThemeHelper import org.schabi.newpipe.util.external_communication.KoreUtils import org.schabi.newpipe.util.external_communication.ShareUtils import org.schabi.newpipe.util.image.CoilHelper -import java.io.File import java.util.LinkedList import java.util.concurrent.TimeUnit import kotlin.math.abs @@ -158,6 +153,7 @@ class VideoDetailFragment : // can't make this lateinit because it needs to be set to null when the view is destroyed private var nullableBinding: FragmentVideoDetailBinding? = null private val binding: FragmentVideoDetailBinding get() = nullableBinding!! + private val downloadStatusViewModel: VideoDownloadStatusViewModel by viewModels() private lateinit var pageAdapter: TabAdapter private var settingsContentObserver: ContentObserver? = null @@ -196,17 +192,6 @@ class VideoDetailFragment : private var currentWorker: Disposable? = null private val disposables = CompositeDisposable() private var positionSubscriber: Disposable? = null - private var downloadStatusDisposable: Disposable? = null - private var currentStreamUid: Long? = null - private var currentDownloadedStream: DownloadedStreamEntity? = null - private var pendingRelinkEntity: DownloadedStreamEntity? = null - - private val relinkLauncher = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> - if (uri != null && pendingRelinkEntity != null) { - handleRelinkResult(pendingRelinkEntity!!, uri) - } - pendingRelinkEntity = null - } /*////////////////////////////////////////////////////////////////////////// // Service management @@ -295,6 +280,26 @@ class VideoDetailFragment : ): View { val newBinding = FragmentVideoDetailBinding.inflate(inflater, container, false) nullableBinding = newBinding + newBinding.detailDownloadStatusCompose?.apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + AppTheme { + val uiState = downloadStatusViewModel.uiState.collectAsStateWithLifecycle().value + val composeContext = LocalContext.current + DownloadStatusHost( + state = uiState, + onChipClick = { + downloadStatusViewModel.onChipClicked(composeContext.applicationContext) + }, + onDismissSheet = { downloadStatusViewModel.dismissSheet() }, + onOpenFile = { info -> openDownloaded(info) }, + onDeleteFile = { info -> deleteDownloadedFile(info) }, + onRemoveLink = { info -> removeDownloadLink(info) }, + onShowInFolder = { info -> showDownloadedInFolder(info) } + ) + } + } + } return newBinding.getRoot() } @@ -374,13 +379,6 @@ class VideoDetailFragment : override fun onDestroyView() { super.onDestroyView() - downloadStatusDisposable?.let { - disposables.remove(it) - it.dispose() - } - downloadStatusDisposable = null - currentDownloadedStream = null - currentStreamUid = null nullableBinding = null } @@ -1399,8 +1397,8 @@ class VideoDetailFragment : currentInfo = info setInitialData(info.serviceId, info.originalUrl, info.name, playQueue) - updateDownloadChip(null) - observeDownloadStatus(info) + downloadStatusViewModel.dismissSheet() + downloadStatusViewModel.setStream(requireContext().applicationContext, info.serviceId, info.url) updateTabs(info) @@ -1580,165 +1578,15 @@ class VideoDetailFragment : } } - private fun observeDownloadStatus(info: StreamInfo) { - val context = context ?: return - downloadStatusDisposable?.let { - disposables.remove(it) - it.dispose() - } - - val disposable = DownloadedStreamsRepository.ensureStreamEntry(context, info) - .flatMapPublisher { streamUid: Long -> - currentStreamUid = streamUid - DownloadedStreamsRepository.observeByStreamUid(context, streamUid) - } - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { entities: List -> - val entity = entities.firstOrNull() - updateDownloadChip(entity) - }, - { throwable -> - if (DEBUG) { - Log.e(TAG, "Failed to observe download state", throwable) - } - updateDownloadChip(null) - } - ) - - downloadStatusDisposable = disposable - disposables.add(disposable) - } - - private fun updateDownloadChip(entity: DownloadedStreamEntity?) { - if (nullableBinding == null) return - - currentDownloadedStream = entity - val chip = binding.detailDownloadStatusChip ?: return - - if (entity == null || entity.status == DownloadedStreamStatus.UNLINKED) { - chip.isGone = true - chip.setOnClickListener(null) + private fun openDownloaded(info: CompletedDownload) { + val uri = info.fileUri + if (uri == null) { + Toast.makeText(requireContext(), R.string.download_open_failed, Toast.LENGTH_SHORT).show() return } - chip.isVisible = true - when (entity.status) { - DownloadedStreamStatus.IN_PROGRESS -> { - chip.text = getString(R.string.download_status_downloading) - chip.setOnClickListener { openDownloadsActivity() } - } - DownloadedStreamStatus.AVAILABLE, - DownloadedStreamStatus.MISSING -> { - chip.text = buildDownloadedLabel(entity) - chip.setOnClickListener { showDownloadOptions(entity) } - } - DownloadedStreamStatus.UNLINKED -> { - chip.isGone = true - chip.setOnClickListener(null) - } - } - } - - private fun buildDownloadedLabel(entity: DownloadedStreamEntity): String { - val quality = entity.qualityLabel?.takeIf { it.isNotBlank() } - return if (quality != null) { - getString(R.string.download_status_downloaded, quality) - } else { - getString(R.string.download_status_downloaded_simple) - } - } - - private fun showDownloadOptions(entity: DownloadedStreamEntity) { - val baseContext = requireContext() - val dialogTheme = ThemeHelper.getDialogTheme(baseContext) - val themedContext = ContextThemeWrapper(baseContext, dialogTheme) - val sheetBinding = DownloadStatusSheetBinding.inflate(LayoutInflater.from(themedContext)) - val dialog = BottomSheetDialog(themedContext) - dialog.setContentView(sheetBinding.root) - - val primaryTextColor = ThemeHelper.resolveColorFromAttr(themedContext, android.R.attr.textColorPrimary) - val secondaryTextColor = ThemeHelper.resolveColorFromAttr(themedContext, android.R.attr.textColorSecondary) - val backgroundDrawable = ThemeHelper.resolveDrawable(themedContext, android.R.attr.windowBackground) - val rippleDrawable = ThemeHelper.resolveDrawable(themedContext, R.attr.selector) - val accentColor = ThemeHelper.resolveColorFromAttr(themedContext, androidx.appcompat.R.attr.colorAccent) - - sheetBinding.root.background = backgroundDrawable - sheetBinding.downloadStatusTitle.setTextColor(primaryTextColor) - sheetBinding.downloadStatusSubtitle.setTextColor(secondaryTextColor) - - fun styleAction(textView: TextView) { - textView.setTextColor(primaryTextColor) - textView.background = rippleDrawable - } - - styleAction(sheetBinding.downloadStatusOpen) - styleAction(sheetBinding.downloadStatusDelete) - styleAction(sheetBinding.downloadStatusShowInFolder) - sheetBinding.downloadStatusRemoveLink.apply { - setTextColor(accentColor) - background = rippleDrawable - } - - val fileAvailable = entity.fileUri.takeUnless { it.isBlank() } - ?.let { DownloadAvailabilityChecker.isReadable(baseContext, Uri.parse(it)) } - ?: false - - val title = entity.displayName?.takeIf { it.isNotBlank() } - ?: currentInfo?.name - ?: getString(R.string.download) - sheetBinding.downloadStatusTitle.text = title - - val subtitleParts = mutableListOf() - entity.qualityLabel?.takeIf { it.isNotBlank() }?.let(subtitleParts::add) - if (!fileAvailable) { - subtitleParts.add(getString(R.string.download_status_missing)) - } - - if (subtitleParts.isEmpty()) { - sheetBinding.downloadStatusSubtitle.isGone = true - } else { - sheetBinding.downloadStatusSubtitle.isVisible = true - sheetBinding.downloadStatusSubtitle.text = subtitleParts.joinToString(" • ") - } - - sheetBinding.downloadStatusOpen.text = getString(R.string.download_action_open) - sheetBinding.downloadStatusDelete.text = getString(R.string.download_action_delete) - sheetBinding.downloadStatusShowInFolder.text = getString(R.string.download_action_show_in_folder) - sheetBinding.downloadStatusRemoveLink.text = getString(R.string.download_action_remove_link) - - sheetBinding.downloadStatusOpen.isVisible = fileAvailable - sheetBinding.downloadStatusDelete.isVisible = fileAvailable - sheetBinding.downloadStatusShowInFolder.isVisible = fileAvailable && !entity.parentUri.isNullOrBlank() - sheetBinding.downloadStatusRemoveLink.isVisible = true - - sheetBinding.downloadStatusOpen.setOnClickListener { - dialog.dismiss() - openDownloaded(entity) - } - - sheetBinding.downloadStatusDelete.setOnClickListener { - dialog.dismiss() - deleteDownloadedFile(entity) - } - - sheetBinding.downloadStatusShowInFolder.setOnClickListener { - dialog.dismiss() - showInFolder(entity) - } - - sheetBinding.downloadStatusRemoveLink.setOnClickListener { - dialog.dismiss() - removeDownloadAssociation(entity) - } - - dialog.show() - } - - private fun openDownloaded(entity: DownloadedStreamEntity) { - val uri = entity.fileUri.takeUnless { it.isBlank() }?.let(Uri::parse) ?: return val intent = Intent(Intent.ACTION_VIEW).apply { - setDataAndType(uri, entity.mime ?: "*/*") + setDataAndType(uri, info.mimeType ?: "*/*") addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } @@ -1749,98 +1597,61 @@ class VideoDetailFragment : } } - private fun showInFolder(entity: DownloadedStreamEntity) { - val parent = entity.parentUri?.takeIf { it.isNotBlank() }?.let(Uri::parse) + private fun showDownloadedInFolder(info: CompletedDownload) { + val parent = info.parentUri if (parent == null) { Toast.makeText(requireContext(), R.string.download_folder_open_failed, Toast.LENGTH_SHORT).show() return } - val intent = Intent(Intent.ACTION_VIEW).apply { + val context = requireContext() + val viewIntent = Intent(Intent.ACTION_VIEW).apply { setDataAndType(parent, DocumentsContract.Document.MIME_TYPE_DIR) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } - runCatching { startActivity(intent) } + runCatching { startActivity(viewIntent) } .onFailure { val treeIntent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply { putExtra(DocumentsContract.EXTRA_INITIAL_URI, parent) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } - runCatching { startActivity(treeIntent) } .onFailure { throwable -> if (DEBUG) Log.e(TAG, "Failed to open folder", throwable) - Toast.makeText(requireContext(), R.string.download_folder_open_failed, Toast.LENGTH_SHORT).show() + Toast.makeText(context, R.string.download_folder_open_failed, Toast.LENGTH_SHORT).show() } } } - private fun removeDownloadAssociation(entity: DownloadedStreamEntity) { - val context = requireContext() - disposables.add( - DownloadedStreamsRepository.deleteByStreamUid(context, entity.streamUid) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { Toast.makeText(context, R.string.download_link_removed, Toast.LENGTH_SHORT).show() }, - { throwable -> - if (DEBUG) Log.e(TAG, "Failed to remove download link", throwable) - showUiErrorSnackbar(this, "Removing download link", throwable) - } - ) - ) - } - - private fun deleteDownloadedFile(entity: DownloadedStreamEntity) { - val context = requireContext() - val uriString = entity.fileUri.takeUnless { it.isBlank() } - if (uriString.isNullOrBlank()) { - Toast.makeText(context, R.string.download_delete_failed, Toast.LENGTH_SHORT).show() + private fun deleteDownloadedFile(info: CompletedDownload) { + if (!info.fileAvailable) { + Toast.makeText(requireContext(), R.string.download_delete_failed, Toast.LENGTH_SHORT).show() return } - - val uri = Uri.parse(uriString) - val deleted = when (uri.scheme?.lowercase()) { - ContentResolver.SCHEME_CONTENT -> DocumentFile.fromSingleUri(context, uri)?.delete() ?: false - ContentResolver.SCHEME_FILE -> uri.path?.let { File(it).delete() } ?: false - else -> runCatching { context.contentResolver.delete(uri, null, null) > 0 }.getOrDefault(false) + val appContext = requireContext().applicationContext + viewLifecycleOwner.lifecycleScope.launch { + val success = downloadStatusViewModel.deleteFile(appContext) + val message = if (success) { + R.string.download_deleted + } else { + R.string.download_delete_failed + } + Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() } - - if (!deleted) { - Toast.makeText(context, R.string.download_delete_failed, Toast.LENGTH_SHORT).show() - return - } - - removeDownloadAssociation(entity) - Toast.makeText(context, R.string.download_deleted, Toast.LENGTH_SHORT).show() } - private fun handleRelinkResult(entity: DownloadedStreamEntity, uri: Uri) { - val context = requireContext() - runCatching { - context.contentResolver.takePersistableUriPermission( - uri, - Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION - ) + private fun removeDownloadLink(@Suppress("UNUSED_PARAMETER") info: CompletedDownload) { + val appContext = requireContext().applicationContext + viewLifecycleOwner.lifecycleScope.launch { + val success = downloadStatusViewModel.removeLink(appContext) + val message = if (success) { + R.string.download_link_removed + } else { + R.string.download_delete_failed + } + Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() } - - disposables.add( - DownloadedStreamsRepository.relink(context, entity, uri) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { Toast.makeText(context, R.string.download_relinked, Toast.LENGTH_SHORT).show() }, - { throwable -> - if (DEBUG) Log.e(TAG, "Failed to relink download", throwable) - Toast.makeText(context, R.string.download_relink_failed, Toast.LENGTH_SHORT).show() - } - ) - ) - } - - private fun openDownloadsActivity() { - val context = requireContext() - val intent = Intent(context, DownloadActivity::class.java) - runCatching { startActivity(intent) } } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDownloadStatusViewModel.kt b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDownloadStatusViewModel.kt new file mode 100644 index 000000000..940cd774f --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDownloadStatusViewModel.kt @@ -0,0 +1,119 @@ +package org.schabi.newpipe.fragments.detail + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.schabi.newpipe.download.CompletedDownload +import org.schabi.newpipe.download.DownloadStatus +import org.schabi.newpipe.download.DownloadStatusRepository + +class VideoDownloadStatusViewModel : ViewModel() { + + private val _uiState = MutableStateFlow(DownloadUiState()) + val uiState: StateFlow = _uiState + + private var observeJob: Job? = null + private var currentServiceId: Int = -1 + private var currentUrl: String? = null + + fun setStream(context: Context, serviceId: Int, url: String?) { + val normalizedUrl = url ?: "" + if (serviceId < 0 || normalizedUrl.isBlank()) { + observeJob?.cancel() + observeJob = null + currentServiceId = -1 + currentUrl = null + _uiState.value = DownloadUiState() + return + } + + if (currentServiceId == serviceId && currentUrl == normalizedUrl) { + return + } + + currentServiceId = serviceId + currentUrl = normalizedUrl + + val appContext = context.applicationContext + + observeJob?.cancel() + observeJob = viewModelScope.launch { + DownloadStatusRepository.observe(appContext, serviceId, normalizedUrl).collectLatest { status -> + _uiState.update { it.copy(chipState = status.toChipState()) } + } + } + } + + fun onChipClicked(context: Context) { + val url = currentUrl ?: return + val serviceId = currentServiceId + viewModelScope.launch { + val result = runCatching { + DownloadStatusRepository.refresh(context.applicationContext, serviceId, url) + } + result.getOrNull()?.let { status -> + _uiState.update { + val chipState = status.toChipState() + it.copy( + chipState = chipState, + isSheetVisible = chipState is DownloadChipState.Downloaded + ) + } + } + if (result.isFailure) { + _uiState.update { it.copy(isSheetVisible = false) } + } + } + } + + fun dismissSheet() { + _uiState.update { it.copy(isSheetVisible = false) } + } + + suspend fun deleteFile(context: Context): Boolean { + val url = currentUrl ?: return false + val serviceId = currentServiceId + val success = runCatching { + DownloadStatusRepository.deleteFile(context.applicationContext, serviceId, url) + }.getOrDefault(false) + if (success) { + _uiState.update { it.copy(isSheetVisible = false) } + } + return success + } + + suspend fun removeLink(context: Context): Boolean { + val url = currentUrl ?: return false + val serviceId = currentServiceId + val success = runCatching { + DownloadStatusRepository.removeLink(context.applicationContext, serviceId, url) + }.getOrDefault(false) + if (success) { + _uiState.update { it.copy(isSheetVisible = false) } + } + return success + } + + private fun DownloadStatus.toChipState(): DownloadChipState = when (this) { + DownloadStatus.None -> DownloadChipState.Hidden + is DownloadStatus.InProgress -> DownloadChipState.Downloading + is DownloadStatus.Completed -> DownloadChipState.Downloaded(info) + } +} + +data class DownloadUiState( + val chipState: DownloadChipState = DownloadChipState.Hidden, + val isSheetVisible: Boolean = false +) + +sealed interface DownloadChipState { + data object Hidden : DownloadChipState + data object Downloading : DownloadChipState + data class Downloaded(val info: CompletedDownload) : DownloadChipState +} diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMission.java b/app/src/main/java/us/shandian/giga/get/DownloadMission.java index f0c9374fe..726a55102 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -135,8 +135,8 @@ public class DownloadMission extends Mission { public MissionRecoveryInfo[] recoveryInfo; public long streamUid = -1; - public long downloadedEntityId = -1; public int serviceId = -1; + public String qualityLabel = null; private transient int finishCount; public transient volatile boolean running; diff --git a/app/src/main/java/us/shandian/giga/get/FinishedMission.java b/app/src/main/java/us/shandian/giga/get/FinishedMission.java index 29f3c6296..94bf508a6 100644 --- a/app/src/main/java/us/shandian/giga/get/FinishedMission.java +++ b/app/src/main/java/us/shandian/giga/get/FinishedMission.java @@ -4,6 +4,10 @@ import androidx.annotation.NonNull; public class FinishedMission extends Mission { + public int serviceId = -1; + public long streamUid = -1; + public String qualityLabel = null; + public FinishedMission() { } @@ -13,6 +17,9 @@ public class FinishedMission extends Mission { timestamp = mission.timestamp; kind = mission.kind; storage = mission.storage; + serviceId = mission.serviceId; + streamUid = mission.streamUid; + qualityLabel = mission.qualityLabel; } } diff --git a/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java b/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java index 704385212..c535d687d 100644 --- a/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java +++ b/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java @@ -27,7 +27,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper { // TODO: use NewPipeSQLiteHelper ('s constants) when playlist branch is merged (?) private static final String DATABASE_NAME = "downloads.db"; - private static final int DATABASE_VERSION = 4; + private static final int DATABASE_VERSION = 5; /** * The table name of download missions (old) @@ -56,6 +56,12 @@ public class FinishedMissionStore extends SQLiteOpenHelper { private static final String KEY_PATH = "path"; + private static final String KEY_SERVICE_ID = "service_id"; + + private static final String KEY_STREAM_UID = "stream_uid"; + + private static final String KEY_QUALITY_LABEL = "quality_label"; + /** * The statement to create the table */ @@ -66,6 +72,9 @@ public class FinishedMissionStore extends SQLiteOpenHelper { KEY_DONE + " INTEGER NOT NULL, " + KEY_TIMESTAMP + " INTEGER NOT NULL, " + KEY_KIND + " TEXT NOT NULL, " + + KEY_SERVICE_ID + " INTEGER NOT NULL DEFAULT -1, " + + KEY_STREAM_UID + " INTEGER NOT NULL DEFAULT -1, " + + KEY_QUALITY_LABEL + " TEXT, " + " UNIQUE(" + KEY_TIMESTAMP + ", " + KEY_PATH + "));"; @@ -121,6 +130,17 @@ public class FinishedMissionStore extends SQLiteOpenHelper { cursor.close(); db.execSQL("DROP TABLE " + MISSIONS_TABLE_NAME_v2); + oldVersion++; + } + + if (oldVersion == 4) { + db.execSQL("ALTER TABLE " + FINISHED_TABLE_NAME + " ADD COLUMN " + + KEY_SERVICE_ID + " INTEGER NOT NULL DEFAULT -1"); + db.execSQL("ALTER TABLE " + FINISHED_TABLE_NAME + " ADD COLUMN " + + KEY_STREAM_UID + " INTEGER NOT NULL DEFAULT -1"); + db.execSQL("ALTER TABLE " + FINISHED_TABLE_NAME + " ADD COLUMN " + + KEY_QUALITY_LABEL + " TEXT"); + oldVersion++; } } @@ -137,6 +157,17 @@ public class FinishedMissionStore extends SQLiteOpenHelper { values.put(KEY_DONE, downloadMission.length); values.put(KEY_TIMESTAMP, downloadMission.timestamp); values.put(KEY_KIND, String.valueOf(downloadMission.kind)); + if (downloadMission instanceof DownloadMission) { + DownloadMission dm = (DownloadMission) downloadMission; + values.put(KEY_SERVICE_ID, dm.serviceId); + values.put(KEY_STREAM_UID, dm.streamUid); + values.put(KEY_QUALITY_LABEL, dm.qualityLabel); + } else if (downloadMission instanceof FinishedMission) { + FinishedMission fm = (FinishedMission) downloadMission; + values.put(KEY_SERVICE_ID, fm.serviceId); + values.put(KEY_STREAM_UID, fm.streamUid); + values.put(KEY_QUALITY_LABEL, fm.qualityLabel); + } return values; } @@ -152,6 +183,9 @@ public class FinishedMissionStore extends SQLiteOpenHelper { mission.length = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE)); mission.timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP)); mission.kind = kind.charAt(0); + mission.serviceId = cursor.getInt(cursor.getColumnIndexOrThrow(KEY_SERVICE_ID)); + mission.streamUid = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_STREAM_UID)); + mission.qualityLabel = cursor.getString(cursor.getColumnIndexOrThrow(KEY_QUALITY_LABEL)); try { mission.storage = new StoredFileHelper(context,null, Uri.parse(path), ""); @@ -200,11 +234,10 @@ public class FinishedMissionStore extends SQLiteOpenHelper { database.delete(FINISHED_TABLE_NAME, KEY_TIMESTAMP + " = ?", new String[]{ts}); } else { database.delete(FINISHED_TABLE_NAME, KEY_TIMESTAMP + " = ? AND " + KEY_PATH + " = ?", new String[]{ - ts, mission.storage.getUri().toString() - }); + ts, mission.storage.getUri().toString()}); } } else { - throw new UnsupportedOperationException("DownloadMission"); + database.delete(FINISHED_TABLE_NAME, KEY_TIMESTAMP + " = ?", new String[]{ts}); } } @@ -217,11 +250,11 @@ public class FinishedMissionStore extends SQLiteOpenHelper { if (mission instanceof FinishedMission) { if (mission.storage.isInvalid()) { - rowsAffected = database.update(FINISHED_TABLE_NAME, values, KEY_TIMESTAMP + " = ?", new String[]{ts}); + rowsAffected = database.update(FINISHED_TABLE_NAME, values, KEY_TIMESTAMP + " = ?", + new String[]{ts}); } else { - rowsAffected = database.update(FINISHED_TABLE_NAME, values, KEY_PATH + " = ?", new String[]{ - mission.storage.getUri().toString() - }); + rowsAffected = database.update(FINISHED_TABLE_NAME, values, KEY_PATH + " = ?", + new String[]{mission.storage.getUri().toString()}); } } else { throw new UnsupportedOperationException("DownloadMission"); diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManager.java b/app/src/main/java/us/shandian/giga/service/DownloadManager.java index d02f77bc1..9ba021c12 100644 --- a/app/src/main/java/us/shandian/giga/service/DownloadManager.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManager.java @@ -14,6 +14,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Objects; import us.shandian.giga.get.DownloadMission; import us.shandian.giga.get.FinishedMission; @@ -333,6 +334,16 @@ public class DownloadManager { return null; } + @Nullable + private DownloadMission getPendingMission(int serviceId, String url) { + for (DownloadMission mission : mMissionsPending) { + if (mission.serviceId == serviceId && Objects.equals(mission.source, url)) { + return mission; + } + } + return null; + } + /** * Get the index into {@link #mMissionsFinished} of a finished mission by its path, return * {@code -1} if there is no such mission. This function also checks if the matched mission's @@ -342,6 +353,26 @@ public class DownloadManager { * @param storage where the file would be stored * @return the mission index or -1 if no such mission exists */ + @Nullable + private FinishedMission getFinishedMission(int serviceId, String url) { + for (FinishedMission mission : mMissionsFinished) { + if (mission.serviceId == serviceId && Objects.equals(mission.source, url)) { + return mission; + } + } + return null; + } + + private boolean isFileAvailable(@NonNull FinishedMission mission) { + if (mission.storage == null || mission.storage.isInvalid()) { + return false; + } + if (!mission.storage.existsAsFile()) { + return false; + } + return mission.storage.length() > 0; + } + private int getFinishedMissionIndex(StoredFileHelper storage) { for (int i = 0; i < mMissionsFinished.size(); i++) { if (mMissionsFinished.get(i).storage.equals(storage)) { @@ -427,6 +458,50 @@ public class DownloadManager { } } + public static final class DownloadStatusSnapshot { + public final MissionState state; + public final DownloadMission pendingMission; + public final FinishedMission finishedMission; + public final boolean fileExists; + + DownloadStatusSnapshot(MissionState state, DownloadMission pendingMission, + FinishedMission finishedMission, boolean fileExists) { + this.state = state; + this.pendingMission = pendingMission; + this.finishedMission = finishedMission; + this.fileExists = fileExists; + } + } + + DownloadStatusSnapshot getDownloadStatus(int serviceId, String url, boolean revalidateFile) { + synchronized (this) { + DownloadMission pending = getPendingMission(serviceId, url); + if (pending != null) { + MissionState state = pending.running + ? MissionState.PendingRunning + : MissionState.Pending; + return new DownloadStatusSnapshot(state, pending, null, true); + } + + FinishedMission finished = getFinishedMission(serviceId, url); + if (finished != null) { + boolean available = !revalidateFile || isFileAvailable(finished); + return new DownloadStatusSnapshot(MissionState.Finished, null, finished, available); + } + } + + return new DownloadStatusSnapshot(MissionState.None, null, null, false); + } + + boolean deleteFinishedMission(int serviceId, String url, boolean deleteFile) { + FinishedMission mission = getFinishedMission(serviceId, url); + if (mission == null) { + return false; + } + deleteMission(mission, deleteFile); + return true; + } + /** * runs one or multiple missions in from queue if possible * diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java index d36e9c796..53726ab54 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -39,8 +39,6 @@ import androidx.core.content.IntentCompat; import androidx.preference.PreferenceManager; import org.schabi.newpipe.R; -import org.schabi.newpipe.download.DownloadedStreamsRepository; -import org.schabi.newpipe.download.DownloadedStreamsRepository.DownloadAssociation; import org.schabi.newpipe.download.DownloadActivity; import org.schabi.newpipe.player.helper.LockManager; import org.schabi.newpipe.streams.io.StoredDirectoryHelper; @@ -58,8 +56,6 @@ import us.shandian.giga.get.MissionRecoveryInfo; import us.shandian.giga.postprocessing.Postprocessing; import us.shandian.giga.service.DownloadManager.NetworkState; -import io.reactivex.rxjava3.disposables.CompositeDisposable; - public class DownloadManagerService extends Service { private static final String TAG = "DownloadManagerService"; @@ -85,8 +81,8 @@ public class DownloadManagerService extends Service { private static final String EXTRA_STORAGE_TAG = "DownloadManagerService.extra.storageTag"; private static final String EXTRA_RECOVERY_INFO = "DownloadManagerService.extra.recoveryInfo"; private static final String EXTRA_STREAM_UID = "DownloadManagerService.extra.streamUid"; - private static final String EXTRA_DOWNLOADED_ID = "DownloadManagerService.extra.downloadedId"; private static final String EXTRA_SERVICE_ID = "DownloadManagerService.extra.serviceId"; + private static final String EXTRA_QUALITY_LABEL = "DownloadManagerService.extra.qualityLabel"; private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished"; private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished"; @@ -125,8 +121,6 @@ public class DownloadManagerService extends Service { private PendingIntent mOpenDownloadList; - private final CompositeDisposable disposables = new CompositeDisposable(); - /** * notify media scanner on downloaded media file ... * @@ -253,7 +247,6 @@ public class DownloadManagerService extends Service { if (icLauncher != null) icLauncher.recycle(); mHandler = null; - disposables.clear(); mManager.pauseAllMissions(true); } @@ -269,18 +262,6 @@ public class DownloadManagerService extends Service { switch (msg.what) { case MESSAGE_FINISHED: - if (mission.streamUid >= 0) { - DownloadAssociation association = - new DownloadAssociation(mission.streamUid, mission.downloadedEntityId); - disposables.add(DownloadedStreamsRepository.INSTANCE - .markFinished(this, association, mission.serviceId, mission.source, - mission.storage, null, null, null, null) - .subscribe( - () -> { }, - throwable -> Log.e(TAG, - "Failed to update downloaded stream entry", throwable) - )); - } notifyMediaScanner(mission.storage.getUri()); notifyFinishedDownload(mission.storage.getName()); mManager.setFinished(mission); @@ -384,7 +365,7 @@ public class DownloadManagerService extends Service { char kind, int threads, String source, String psName, String[] psArgs, long nearLength, ArrayList recoveryInfo, - long streamUid, long downloadedEntityId, int serviceId) { + long streamUid, int serviceId, String qualityLabel) { final Intent intent = new Intent(context, DownloadManagerService.class) .setAction(Intent.ACTION_RUN) .putExtra(EXTRA_URLS, urls) @@ -399,8 +380,8 @@ public class DownloadManagerService extends Service { .putExtra(EXTRA_PATH, storage.getUri()) .putExtra(EXTRA_STORAGE_TAG, storage.getTag()) .putExtra(EXTRA_STREAM_UID, streamUid) - .putExtra(EXTRA_DOWNLOADED_ID, downloadedEntityId) - .putExtra(EXTRA_SERVICE_ID, serviceId); + .putExtra(EXTRA_SERVICE_ID, serviceId) + .putExtra(EXTRA_QUALITY_LABEL, qualityLabel); context.startService(intent); } @@ -417,8 +398,8 @@ public class DownloadManagerService extends Service { long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0); String tag = intent.getStringExtra(EXTRA_STORAGE_TAG); long streamUid = intent.getLongExtra(EXTRA_STREAM_UID, -1L); - long downloadedEntityId = intent.getLongExtra(EXTRA_DOWNLOADED_ID, -1L); int serviceId = intent.getIntExtra(EXTRA_SERVICE_ID, -1); + String qualityLabel = intent.getStringExtra(EXTRA_QUALITY_LABEL); final var recovery = IntentCompat.getParcelableArrayListExtra(intent, EXTRA_RECOVERY_INFO, MissionRecoveryInfo.class); Objects.requireNonNull(recovery); @@ -442,8 +423,8 @@ public class DownloadManagerService extends Service { mission.nearLength = nearLength; mission.recoveryInfo = recovery.toArray(new MissionRecoveryInfo[0]); mission.streamUid = streamUid; - mission.downloadedEntityId = downloadedEntityId; mission.serviceId = serviceId; + mission.qualityLabel = qualityLabel; if (ps != null) ps.setTemporalDir(DownloadManager.pickAvailableTemporalDir(this)); @@ -615,6 +596,15 @@ public class DownloadManagerService extends Service { mDownloadNotificationEnable = enable; } + public DownloadManager.DownloadStatusSnapshot getDownloadStatus(int serviceId, String source, + boolean revalidateFile) { + return mManager.getDownloadStatus(serviceId, source, revalidateFile); + } + + public boolean deleteFinishedMission(int serviceId, String source, boolean deleteFile) { + return mManager.deleteFinishedMission(serviceId, source, deleteFile); + } + } } diff --git a/app/src/main/res/layout/download_status_sheet.xml b/app/src/main/res/layout/download_status_sheet.xml deleted file mode 100644 index 07eccf030..000000000 --- a/app/src/main/res/layout/download_status_sheet.xml +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_video_detail.xml b/app/src/main/res/layout/fragment_video_detail.xml index 71a9937bd..24e167a5e 100644 --- a/app/src/main/res/layout/fragment_video_detail.xml +++ b/app/src/main/res/layout/fragment_video_detail.xml @@ -551,27 +551,21 @@ - + android:visibility="visible" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d50f03310..e8f1440a6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,10 +25,8 @@ Delete file Remove link Download link removed - Download relinked Unable to open downloaded file Unable to open folder - Unable to relink file Unable to delete downloaded file Deleted downloaded file Search diff --git a/app/src/main/res/values/styles_download.xml b/app/src/main/res/values/styles_download.xml deleted file mode 100644 index 82a01a2e9..000000000 --- a/app/src/main/res/values/styles_download.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - -