mirror of
https://github.com/TeamNewPipe/NewPipe.git
synced 2025-10-03 09:49:21 +02:00
Refactor download status persistence and UI
Restore finshed mission conditoina
This commit is contained in:
parent
b5cb367edb
commit
13b10b6e52
28 changed files with 682 additions and 1852 deletions
|
@ -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')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -25,7 +25,6 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||||
import org.acra.ACRA.init
|
import org.acra.ACRA.init
|
||||||
import org.acra.ACRA.isACRASenderServiceProcess
|
import org.acra.ACRA.isACRASenderServiceProcess
|
||||||
import org.acra.config.CoreConfigurationBuilder
|
import org.acra.config.CoreConfigurationBuilder
|
||||||
import org.schabi.newpipe.download.DownloadMaintenance
|
|
||||||
import org.schabi.newpipe.error.ReCaptchaActivity
|
import org.schabi.newpipe.error.ReCaptchaActivity
|
||||||
import org.schabi.newpipe.extractor.NewPipe
|
import org.schabi.newpipe.extractor.NewPipe
|
||||||
import org.schabi.newpipe.extractor.downloader.Downloader
|
import org.schabi.newpipe.extractor.downloader.Downloader
|
||||||
|
@ -121,8 +120,6 @@ open class App :
|
||||||
configureRxJavaErrorHandler()
|
configureRxJavaErrorHandler()
|
||||||
|
|
||||||
YoutubeStreamExtractor.setPoTokenProvider(PoTokenProviderImpl)
|
YoutubeStreamExtractor.setPoTokenProvider(PoTokenProviderImpl)
|
||||||
|
|
||||||
DownloadMaintenance.schedule(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun newImageLoader(context: Context): ImageLoader =
|
override fun newImageLoader(context: Context): ImageLoader =
|
||||||
|
|
|
@ -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_6_7;
|
||||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_7_8;
|
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_8_9;
|
||||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_9_10;
|
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
@ -30,7 +29,7 @@ public final class NewPipeDatabase {
|
||||||
return Room
|
return Room
|
||||||
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
|
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
|
||||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5,
|
.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();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
package org.schabi.newpipe.database;
|
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.Database;
|
||||||
import androidx.room.RoomDatabase;
|
import androidx.room.RoomDatabase;
|
||||||
import androidx.room.TypeConverters;
|
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.FeedDAO;
|
||||||
import org.schabi.newpipe.database.feed.dao.FeedGroupDAO;
|
import org.schabi.newpipe.database.feed.dao.FeedGroupDAO;
|
||||||
import org.schabi.newpipe.database.feed.model.FeedEntity;
|
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,
|
StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class,
|
||||||
PlaylistEntity.class, PlaylistStreamEntity.class, PlaylistRemoteEntity.class,
|
PlaylistEntity.class, PlaylistStreamEntity.class, PlaylistRemoteEntity.class,
|
||||||
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.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 abstract class AppDatabase extends RoomDatabase {
|
||||||
public static final String DATABASE_NAME = "newpipe.db";
|
public static final String DATABASE_NAME = "newpipe.db";
|
||||||
|
@ -64,6 +62,4 @@ public abstract class AppDatabase extends RoomDatabase {
|
||||||
public abstract FeedGroupDAO feedGroupDAO();
|
public abstract FeedGroupDAO feedGroupDAO();
|
||||||
|
|
||||||
public abstract SubscriptionDAO subscriptionDAO();
|
public abstract SubscriptionDAO subscriptionDAO();
|
||||||
|
|
||||||
public abstract DownloadedStreamsDao downloadedStreamsDao();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package org.schabi.newpipe.database
|
package org.schabi.newpipe.database
|
||||||
|
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import org.schabi.newpipe.database.download.DownloadedStreamStatus
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType
|
import org.schabi.newpipe.extractor.stream.StreamType
|
||||||
import org.schabi.newpipe.local.subscription.FeedGroupIcon
|
import org.schabi.newpipe.local.subscription.FeedGroupIcon
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
@ -50,14 +49,4 @@ class Converters {
|
||||||
fun feedGroupIconOf(id: Int): FeedGroupIcon {
|
fun feedGroupIconOf(id: Int): FeedGroupIcon {
|
||||||
return FeedGroupIcon.entries.first { it.id == id }
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,6 @@ public final class Migrations {
|
||||||
public static final int DB_VER_7 = 7;
|
public static final int DB_VER_7 = 7;
|
||||||
public static final int DB_VER_8 = 8;
|
public static final int DB_VER_8 = 8;
|
||||||
public static final int DB_VER_9 = 9;
|
public static final int DB_VER_9 = 9;
|
||||||
public static final int DB_VER_10 = 10;
|
|
||||||
|
|
||||||
private static final String TAG = Migrations.class.getName();
|
private static final String TAG = Migrations.class.getName();
|
||||||
public static final boolean DEBUG = MainActivity.DEBUG;
|
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() {
|
private Migrations() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<List<DownloadedStreamEntity>>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM downloaded_streams WHERE stream_uid = :streamUid LIMIT 1")
|
|
||||||
fun getByStreamUid(streamUid: Long): Maybe<DownloadedStreamEntity>
|
|
||||||
|
|
||||||
@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<DownloadedStreamEntity>
|
|
||||||
}
|
|
|
@ -81,14 +81,6 @@ public class DownloadActivity extends AppCompatActivity {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
new Thread(() ->
|
|
||||||
DownloadMaintenance.revalidateAvailable(DownloadActivity.this, 10)
|
|
||||||
).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -79,9 +79,7 @@ import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import us.shandian.giga.get.MissionRecoveryInfo;
|
import us.shandian.giga.get.MissionRecoveryInfo;
|
||||||
import us.shandian.giga.postprocessing.Postprocessing;
|
import us.shandian.giga.postprocessing.Postprocessing;
|
||||||
|
@ -1135,50 +1133,27 @@ public class DownloadDialog extends DialogFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
final String qualityLabel = buildQualityLabel(selectedStream);
|
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;
|
DownloadManagerService.startMission(
|
||||||
final int missionThreads = threads;
|
context,
|
||||||
final String missionSourceUrl = currentInfo.getUrl();
|
urls,
|
||||||
final String missionPsName = psName;
|
storage,
|
||||||
final String[] missionPsArgs = psArgs;
|
kind,
|
||||||
final long missionNearLength = nearLength;
|
threads,
|
||||||
|
currentInfo.getUrl(),
|
||||||
|
psName,
|
||||||
|
psArgs,
|
||||||
|
nearLength,
|
||||||
|
new ArrayList<>(recoveryInfo),
|
||||||
|
-1L,
|
||||||
|
currentInfo.getServiceId(),
|
||||||
|
qualityLabel
|
||||||
|
);
|
||||||
|
|
||||||
disposables.add(DownloadedStreamsRepository.INSTANCE
|
Toast.makeText(context, getString(R.string.download_has_started),
|
||||||
.upsertForEnqueued(requireContext(), currentInfo, storage, null, resolvedMime,
|
Toast.LENGTH_SHORT).show();
|
||||||
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),
|
dismiss();
|
||||||
Toast.LENGTH_SHORT).show();
|
|
||||||
|
|
||||||
dismiss();
|
|
||||||
},
|
|
||||||
throwable -> ErrorUtil.createNotification(requireContext(),
|
|
||||||
new ErrorInfo(throwable, UserAction.DOWNLOAD_FAILED,
|
|
||||||
"Preparing download metadata", currentInfo))
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
|
|
@ -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<DownloadRevalidationWorker>(1, TimeUnit.DAYS)
|
|
||||||
.build()
|
|
||||||
WorkManager.getInstance(context)
|
|
||||||
.enqueueUniquePeriodicWork(WORK_NAME, ExistingPeriodicWorkPolicy.KEEP, workRequest)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<DownloadStatus> = 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 <T> 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<List<DownloadedStreamEntity>> {
|
|
||||||
return downloadedDao(context)
|
|
||||||
.observeByStreamUid(streamUid)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getByStreamUid(context: Context, streamUid: Long): Maybe<DownloadedStreamEntity> {
|
|
||||||
return downloadedDao(context)
|
|
||||||
.getByStreamUid(streamUid)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ensureStreamEntry(context: Context, info: StreamInfo): Single<Long> {
|
|
||||||
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<DownloadAssociation> {
|
|
||||||
return Single.fromCallable {
|
|
||||||
val db = database(context)
|
|
||||||
db.runInTransaction<DownloadAssociation> {
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,6 @@ import android.animation.ValueAnimator
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.ContentResolver
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
|
@ -13,7 +12,6 @@ import android.content.pm.ActivityInfo
|
||||||
import android.database.ContentObserver
|
import android.database.ContentObserver
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
|
@ -23,7 +21,6 @@ import android.provider.Settings
|
||||||
import android.util.DisplayMetrics
|
import android.util.DisplayMetrics
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.ContextThemeWrapper
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -35,14 +32,14 @@ import android.view.WindowManager
|
||||||
import android.view.animation.DecelerateInterpolator
|
import android.view.animation.DecelerateInterpolator
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import android.widget.TextView
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.annotation.AttrRes
|
import androidx.annotation.AttrRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
|
@ -50,7 +47,9 @@ import androidx.core.net.toUri
|
||||||
import androidx.core.os.postDelayed
|
import androidx.core.os.postDelayed
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
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 androidx.preference.PreferenceManager
|
||||||
import coil3.util.CoilUtils
|
import coil3.util.CoilUtils
|
||||||
import com.evernote.android.state.State
|
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.appbar.AppBarLayout
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
|
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.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
import io.reactivex.rxjava3.disposables.Disposable
|
import io.reactivex.rxjava3.disposables.Disposable
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.schabi.newpipe.App
|
import org.schabi.newpipe.App
|
||||||
import org.schabi.newpipe.R
|
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.database.stream.model.StreamEntity
|
||||||
import org.schabi.newpipe.databinding.DownloadStatusSheetBinding
|
|
||||||
import org.schabi.newpipe.databinding.FragmentVideoDetailBinding
|
import org.schabi.newpipe.databinding.FragmentVideoDetailBinding
|
||||||
import org.schabi.newpipe.download.DownloadActivity
|
import org.schabi.newpipe.download.CompletedDownload
|
||||||
import org.schabi.newpipe.download.DownloadAvailabilityChecker
|
|
||||||
import org.schabi.newpipe.download.DownloadDialog
|
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.ErrorInfo
|
||||||
import org.schabi.newpipe.error.ErrorUtil.Companion.showSnackbar
|
import org.schabi.newpipe.error.ErrorUtil.Companion.showSnackbar
|
||||||
import org.schabi.newpipe.error.ErrorUtil.Companion.showUiErrorSnackbar
|
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.playqueue.SinglePlayQueue
|
||||||
import org.schabi.newpipe.player.ui.MainPlayerUi
|
import org.schabi.newpipe.player.ui.MainPlayerUi
|
||||||
import org.schabi.newpipe.player.ui.VideoPlayerUi
|
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.DependentPreferenceHelper
|
||||||
import org.schabi.newpipe.util.DeviceUtils
|
import org.schabi.newpipe.util.DeviceUtils
|
||||||
import org.schabi.newpipe.util.ExtractorHelper
|
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.KoreUtils
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||||
import org.schabi.newpipe.util.image.CoilHelper
|
import org.schabi.newpipe.util.image.CoilHelper
|
||||||
import java.io.File
|
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.math.abs
|
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
|
// can't make this lateinit because it needs to be set to null when the view is destroyed
|
||||||
private var nullableBinding: FragmentVideoDetailBinding? = null
|
private var nullableBinding: FragmentVideoDetailBinding? = null
|
||||||
private val binding: FragmentVideoDetailBinding get() = nullableBinding!!
|
private val binding: FragmentVideoDetailBinding get() = nullableBinding!!
|
||||||
|
private val downloadStatusViewModel: VideoDownloadStatusViewModel by viewModels()
|
||||||
private lateinit var pageAdapter: TabAdapter
|
private lateinit var pageAdapter: TabAdapter
|
||||||
private var settingsContentObserver: ContentObserver? = null
|
private var settingsContentObserver: ContentObserver? = null
|
||||||
|
|
||||||
|
@ -196,17 +192,6 @@ class VideoDetailFragment :
|
||||||
private var currentWorker: Disposable? = null
|
private var currentWorker: Disposable? = null
|
||||||
private val disposables = CompositeDisposable()
|
private val disposables = CompositeDisposable()
|
||||||
private var positionSubscriber: Disposable? = null
|
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
|
// Service management
|
||||||
|
@ -295,6 +280,26 @@ class VideoDetailFragment :
|
||||||
): View {
|
): View {
|
||||||
val newBinding = FragmentVideoDetailBinding.inflate(inflater, container, false)
|
val newBinding = FragmentVideoDetailBinding.inflate(inflater, container, false)
|
||||||
nullableBinding = newBinding
|
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()
|
return newBinding.getRoot()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -374,13 +379,6 @@ class VideoDetailFragment :
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
downloadStatusDisposable?.let {
|
|
||||||
disposables.remove(it)
|
|
||||||
it.dispose()
|
|
||||||
}
|
|
||||||
downloadStatusDisposable = null
|
|
||||||
currentDownloadedStream = null
|
|
||||||
currentStreamUid = null
|
|
||||||
nullableBinding = null
|
nullableBinding = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1399,8 +1397,8 @@ class VideoDetailFragment :
|
||||||
currentInfo = info
|
currentInfo = info
|
||||||
setInitialData(info.serviceId, info.originalUrl, info.name, playQueue)
|
setInitialData(info.serviceId, info.originalUrl, info.name, playQueue)
|
||||||
|
|
||||||
updateDownloadChip(null)
|
downloadStatusViewModel.dismissSheet()
|
||||||
observeDownloadStatus(info)
|
downloadStatusViewModel.setStream(requireContext().applicationContext, info.serviceId, info.url)
|
||||||
|
|
||||||
updateTabs(info)
|
updateTabs(info)
|
||||||
|
|
||||||
|
@ -1580,165 +1578,15 @@ class VideoDetailFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeDownloadStatus(info: StreamInfo) {
|
private fun openDownloaded(info: CompletedDownload) {
|
||||||
val context = context ?: return
|
val uri = info.fileUri
|
||||||
downloadStatusDisposable?.let {
|
if (uri == null) {
|
||||||
disposables.remove(it)
|
Toast.makeText(requireContext(), R.string.download_open_failed, Toast.LENGTH_SHORT).show()
|
||||||
it.dispose()
|
|
||||||
}
|
|
||||||
|
|
||||||
val disposable = DownloadedStreamsRepository.ensureStreamEntry(context, info)
|
|
||||||
.flatMapPublisher { streamUid: Long ->
|
|
||||||
currentStreamUid = streamUid
|
|
||||||
DownloadedStreamsRepository.observeByStreamUid(context, streamUid)
|
|
||||||
}
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(
|
|
||||||
{ entities: List<DownloadedStreamEntity> ->
|
|
||||||
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)
|
|
||||||
return
|
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<String>()
|
|
||||||
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 {
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
setDataAndType(uri, entity.mime ?: "*/*")
|
setDataAndType(uri, info.mimeType ?: "*/*")
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1749,98 +1597,61 @@ class VideoDetailFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showInFolder(entity: DownloadedStreamEntity) {
|
private fun showDownloadedInFolder(info: CompletedDownload) {
|
||||||
val parent = entity.parentUri?.takeIf { it.isNotBlank() }?.let(Uri::parse)
|
val parent = info.parentUri
|
||||||
if (parent == null) {
|
if (parent == null) {
|
||||||
Toast.makeText(requireContext(), R.string.download_folder_open_failed, Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), R.string.download_folder_open_failed, Toast.LENGTH_SHORT).show()
|
||||||
return
|
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)
|
setDataAndType(parent, DocumentsContract.Document.MIME_TYPE_DIR)
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
}
|
}
|
||||||
|
|
||||||
runCatching { startActivity(intent) }
|
runCatching { startActivity(viewIntent) }
|
||||||
.onFailure {
|
.onFailure {
|
||||||
val treeIntent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
val treeIntent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
||||||
putExtra(DocumentsContract.EXTRA_INITIAL_URI, parent)
|
putExtra(DocumentsContract.EXTRA_INITIAL_URI, parent)
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
}
|
}
|
||||||
|
|
||||||
runCatching { startActivity(treeIntent) }
|
runCatching { startActivity(treeIntent) }
|
||||||
.onFailure { throwable ->
|
.onFailure { throwable ->
|
||||||
if (DEBUG) Log.e(TAG, "Failed to open folder", 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) {
|
private fun deleteDownloadedFile(info: CompletedDownload) {
|
||||||
val context = requireContext()
|
if (!info.fileAvailable) {
|
||||||
disposables.add(
|
Toast.makeText(requireContext(), R.string.download_delete_failed, Toast.LENGTH_SHORT).show()
|
||||||
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()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
val appContext = requireContext().applicationContext
|
||||||
val uri = Uri.parse(uriString)
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
val deleted = when (uri.scheme?.lowercase()) {
|
val success = downloadStatusViewModel.deleteFile(appContext)
|
||||||
ContentResolver.SCHEME_CONTENT -> DocumentFile.fromSingleUri(context, uri)?.delete() ?: false
|
val message = if (success) {
|
||||||
ContentResolver.SCHEME_FILE -> uri.path?.let { File(it).delete() } ?: false
|
R.string.download_deleted
|
||||||
else -> runCatching { context.contentResolver.delete(uri, null, null) > 0 }.getOrDefault(false)
|
} 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) {
|
private fun removeDownloadLink(@Suppress("UNUSED_PARAMETER") info: CompletedDownload) {
|
||||||
val context = requireContext()
|
val appContext = requireContext().applicationContext
|
||||||
runCatching {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
context.contentResolver.takePersistableUriPermission(
|
val success = downloadStatusViewModel.removeLink(appContext)
|
||||||
uri,
|
val message = if (success) {
|
||||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
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) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -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<DownloadUiState> = _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
|
||||||
|
}
|
|
@ -135,8 +135,8 @@ public class DownloadMission extends Mission {
|
||||||
public MissionRecoveryInfo[] recoveryInfo;
|
public MissionRecoveryInfo[] recoveryInfo;
|
||||||
|
|
||||||
public long streamUid = -1;
|
public long streamUid = -1;
|
||||||
public long downloadedEntityId = -1;
|
|
||||||
public int serviceId = -1;
|
public int serviceId = -1;
|
||||||
|
public String qualityLabel = null;
|
||||||
|
|
||||||
private transient int finishCount;
|
private transient int finishCount;
|
||||||
public transient volatile boolean running;
|
public transient volatile boolean running;
|
||||||
|
|
|
@ -4,6 +4,10 @@ import androidx.annotation.NonNull;
|
||||||
|
|
||||||
public class FinishedMission extends Mission {
|
public class FinishedMission extends Mission {
|
||||||
|
|
||||||
|
public int serviceId = -1;
|
||||||
|
public long streamUid = -1;
|
||||||
|
public String qualityLabel = null;
|
||||||
|
|
||||||
public FinishedMission() {
|
public FinishedMission() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,6 +17,9 @@ public class FinishedMission extends Mission {
|
||||||
timestamp = mission.timestamp;
|
timestamp = mission.timestamp;
|
||||||
kind = mission.kind;
|
kind = mission.kind;
|
||||||
storage = mission.storage;
|
storage = mission.storage;
|
||||||
|
serviceId = mission.serviceId;
|
||||||
|
streamUid = mission.streamUid;
|
||||||
|
qualityLabel = mission.qualityLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
|
||||||
// TODO: use NewPipeSQLiteHelper ('s constants) when playlist branch is merged (?)
|
// TODO: use NewPipeSQLiteHelper ('s constants) when playlist branch is merged (?)
|
||||||
private static final String DATABASE_NAME = "downloads.db";
|
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)
|
* 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_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
|
* The statement to create the table
|
||||||
*/
|
*/
|
||||||
|
@ -66,6 +72,9 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
|
||||||
KEY_DONE + " INTEGER NOT NULL, " +
|
KEY_DONE + " INTEGER NOT NULL, " +
|
||||||
KEY_TIMESTAMP + " INTEGER NOT NULL, " +
|
KEY_TIMESTAMP + " INTEGER NOT NULL, " +
|
||||||
KEY_KIND + " TEXT 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 + "));";
|
" UNIQUE(" + KEY_TIMESTAMP + ", " + KEY_PATH + "));";
|
||||||
|
|
||||||
|
|
||||||
|
@ -121,6 +130,17 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
|
||||||
|
|
||||||
cursor.close();
|
cursor.close();
|
||||||
db.execSQL("DROP TABLE " + MISSIONS_TABLE_NAME_v2);
|
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_DONE, downloadMission.length);
|
||||||
values.put(KEY_TIMESTAMP, downloadMission.timestamp);
|
values.put(KEY_TIMESTAMP, downloadMission.timestamp);
|
||||||
values.put(KEY_KIND, String.valueOf(downloadMission.kind));
|
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;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,6 +183,9 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
|
||||||
mission.length = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE));
|
mission.length = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE));
|
||||||
mission.timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP));
|
mission.timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP));
|
||||||
mission.kind = kind.charAt(0);
|
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 {
|
try {
|
||||||
mission.storage = new StoredFileHelper(context,null, Uri.parse(path), "");
|
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});
|
database.delete(FINISHED_TABLE_NAME, KEY_TIMESTAMP + " = ?", new String[]{ts});
|
||||||
} else {
|
} else {
|
||||||
database.delete(FINISHED_TABLE_NAME, KEY_TIMESTAMP + " = ? AND " + KEY_PATH + " = ?", new String[]{
|
database.delete(FINISHED_TABLE_NAME, KEY_TIMESTAMP + " = ? AND " + KEY_PATH + " = ?", new String[]{
|
||||||
ts, mission.storage.getUri().toString()
|
ts, mission.storage.getUri().toString()});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else {
|
} 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 instanceof FinishedMission) {
|
||||||
if (mission.storage.isInvalid()) {
|
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 {
|
} else {
|
||||||
rowsAffected = database.update(FINISHED_TABLE_NAME, values, KEY_PATH + " = ?", new String[]{
|
rowsAffected = database.update(FINISHED_TABLE_NAME, values, KEY_PATH + " = ?",
|
||||||
mission.storage.getUri().toString()
|
new String[]{mission.storage.getUri().toString()});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new UnsupportedOperationException("DownloadMission");
|
throw new UnsupportedOperationException("DownloadMission");
|
||||||
|
|
|
@ -14,6 +14,7 @@ import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import us.shandian.giga.get.DownloadMission;
|
import us.shandian.giga.get.DownloadMission;
|
||||||
import us.shandian.giga.get.FinishedMission;
|
import us.shandian.giga.get.FinishedMission;
|
||||||
|
@ -333,6 +334,16 @@ public class DownloadManager {
|
||||||
return null;
|
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
|
* 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
|
* {@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
|
* @param storage where the file would be stored
|
||||||
* @return the mission index or -1 if no such mission exists
|
* @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) {
|
private int getFinishedMissionIndex(StoredFileHelper storage) {
|
||||||
for (int i = 0; i < mMissionsFinished.size(); i++) {
|
for (int i = 0; i < mMissionsFinished.size(); i++) {
|
||||||
if (mMissionsFinished.get(i).storage.equals(storage)) {
|
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
|
* runs one or multiple missions in from queue if possible
|
||||||
*
|
*
|
||||||
|
|
|
@ -39,8 +39,6 @@ import androidx.core.content.IntentCompat;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
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.download.DownloadActivity;
|
||||||
import org.schabi.newpipe.player.helper.LockManager;
|
import org.schabi.newpipe.player.helper.LockManager;
|
||||||
import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
|
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.postprocessing.Postprocessing;
|
||||||
import us.shandian.giga.service.DownloadManager.NetworkState;
|
import us.shandian.giga.service.DownloadManager.NetworkState;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
|
||||||
|
|
||||||
public class DownloadManagerService extends Service {
|
public class DownloadManagerService extends Service {
|
||||||
|
|
||||||
private static final String TAG = "DownloadManagerService";
|
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_STORAGE_TAG = "DownloadManagerService.extra.storageTag";
|
||||||
private static final String EXTRA_RECOVERY_INFO = "DownloadManagerService.extra.recoveryInfo";
|
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_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_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_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished";
|
||||||
private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_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 PendingIntent mOpenDownloadList;
|
||||||
|
|
||||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* notify media scanner on downloaded media file ...
|
* notify media scanner on downloaded media file ...
|
||||||
*
|
*
|
||||||
|
@ -253,7 +247,6 @@ public class DownloadManagerService extends Service {
|
||||||
if (icLauncher != null) icLauncher.recycle();
|
if (icLauncher != null) icLauncher.recycle();
|
||||||
|
|
||||||
mHandler = null;
|
mHandler = null;
|
||||||
disposables.clear();
|
|
||||||
mManager.pauseAllMissions(true);
|
mManager.pauseAllMissions(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,18 +262,6 @@ public class DownloadManagerService extends Service {
|
||||||
|
|
||||||
switch (msg.what) {
|
switch (msg.what) {
|
||||||
case MESSAGE_FINISHED:
|
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());
|
notifyMediaScanner(mission.storage.getUri());
|
||||||
notifyFinishedDownload(mission.storage.getName());
|
notifyFinishedDownload(mission.storage.getName());
|
||||||
mManager.setFinished(mission);
|
mManager.setFinished(mission);
|
||||||
|
@ -384,7 +365,7 @@ public class DownloadManagerService extends Service {
|
||||||
char kind, int threads, String source, String psName,
|
char kind, int threads, String source, String psName,
|
||||||
String[] psArgs, long nearLength,
|
String[] psArgs, long nearLength,
|
||||||
ArrayList<MissionRecoveryInfo> recoveryInfo,
|
ArrayList<MissionRecoveryInfo> recoveryInfo,
|
||||||
long streamUid, long downloadedEntityId, int serviceId) {
|
long streamUid, int serviceId, String qualityLabel) {
|
||||||
final Intent intent = new Intent(context, DownloadManagerService.class)
|
final Intent intent = new Intent(context, DownloadManagerService.class)
|
||||||
.setAction(Intent.ACTION_RUN)
|
.setAction(Intent.ACTION_RUN)
|
||||||
.putExtra(EXTRA_URLS, urls)
|
.putExtra(EXTRA_URLS, urls)
|
||||||
|
@ -399,8 +380,8 @@ public class DownloadManagerService extends Service {
|
||||||
.putExtra(EXTRA_PATH, storage.getUri())
|
.putExtra(EXTRA_PATH, storage.getUri())
|
||||||
.putExtra(EXTRA_STORAGE_TAG, storage.getTag())
|
.putExtra(EXTRA_STORAGE_TAG, storage.getTag())
|
||||||
.putExtra(EXTRA_STREAM_UID, streamUid)
|
.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);
|
context.startService(intent);
|
||||||
}
|
}
|
||||||
|
@ -417,8 +398,8 @@ public class DownloadManagerService extends Service {
|
||||||
long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0);
|
long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0);
|
||||||
String tag = intent.getStringExtra(EXTRA_STORAGE_TAG);
|
String tag = intent.getStringExtra(EXTRA_STORAGE_TAG);
|
||||||
long streamUid = intent.getLongExtra(EXTRA_STREAM_UID, -1L);
|
long streamUid = intent.getLongExtra(EXTRA_STREAM_UID, -1L);
|
||||||
long downloadedEntityId = intent.getLongExtra(EXTRA_DOWNLOADED_ID, -1L);
|
|
||||||
int serviceId = intent.getIntExtra(EXTRA_SERVICE_ID, -1);
|
int serviceId = intent.getIntExtra(EXTRA_SERVICE_ID, -1);
|
||||||
|
String qualityLabel = intent.getStringExtra(EXTRA_QUALITY_LABEL);
|
||||||
final var recovery = IntentCompat.getParcelableArrayListExtra(intent, EXTRA_RECOVERY_INFO,
|
final var recovery = IntentCompat.getParcelableArrayListExtra(intent, EXTRA_RECOVERY_INFO,
|
||||||
MissionRecoveryInfo.class);
|
MissionRecoveryInfo.class);
|
||||||
Objects.requireNonNull(recovery);
|
Objects.requireNonNull(recovery);
|
||||||
|
@ -442,8 +423,8 @@ public class DownloadManagerService extends Service {
|
||||||
mission.nearLength = nearLength;
|
mission.nearLength = nearLength;
|
||||||
mission.recoveryInfo = recovery.toArray(new MissionRecoveryInfo[0]);
|
mission.recoveryInfo = recovery.toArray(new MissionRecoveryInfo[0]);
|
||||||
mission.streamUid = streamUid;
|
mission.streamUid = streamUid;
|
||||||
mission.downloadedEntityId = downloadedEntityId;
|
|
||||||
mission.serviceId = serviceId;
|
mission.serviceId = serviceId;
|
||||||
|
mission.qualityLabel = qualityLabel;
|
||||||
|
|
||||||
if (ps != null)
|
if (ps != null)
|
||||||
ps.setTemporalDir(DownloadManager.pickAvailableTemporalDir(this));
|
ps.setTemporalDir(DownloadManager.pickAvailableTemporalDir(this));
|
||||||
|
@ -615,6 +596,15 @@ public class DownloadManagerService extends Service {
|
||||||
mDownloadNotificationEnable = enable;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,76 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:background="?attr/contrast_background_color"
|
|
||||||
android:paddingStart="24dp"
|
|
||||||
android:paddingTop="16dp"
|
|
||||||
android:paddingEnd="24dp"
|
|
||||||
android:paddingBottom="12dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/download_status_title"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Headline"
|
|
||||||
android:textColor="?android:textColorPrimary"
|
|
||||||
tools:text="Sample Video" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/download_status_subtitle"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingTop="4dp"
|
|
||||||
android:paddingBottom="12dp"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
|
|
||||||
android:textColor="?android:textColorSecondary"
|
|
||||||
tools:text="1080p • 12 Apr 2024" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="1dp"
|
|
||||||
android:background="?attr/separator_color" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/download_status_open"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="?attr/selector"
|
|
||||||
android:paddingVertical="16dp"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
|
||||||
android:textColor="?android:textColorPrimary"
|
|
||||||
tools:text="Open downloaded" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/download_status_delete"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="?attr/selector"
|
|
||||||
android:paddingVertical="16dp"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
|
||||||
android:textColor="?android:textColorPrimary"
|
|
||||||
tools:text="Delete file" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/download_status_show_in_folder"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="?attr/selector"
|
|
||||||
android:paddingVertical="16dp"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
|
||||||
android:textColor="?android:textColorPrimary"
|
|
||||||
tools:text="Show in folder" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/download_status_remove_link"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="?attr/selector"
|
|
||||||
android:paddingVertical="16dp"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
|
||||||
android:textColor="?attr/colorAccent"
|
|
||||||
tools:text="Remove link" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
|
@ -551,27 +551,21 @@
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<com.google.android.material.chip.Chip
|
<androidx.compose.ui.platform.ComposeView
|
||||||
android:id="@+id/detail_download_status_chip"
|
android:id="@+id/detail_download_status_compose"
|
||||||
style="@style/Widget.MaterialComponents.Chip.Action"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_below="@id/detail_control_panel"
|
android:layout_below="@id/detail_control_panel"
|
||||||
android:layout_marginStart="12dp"
|
android:layout_marginStart="12dp"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:layout_marginEnd="12dp"
|
android:layout_marginEnd="12dp"
|
||||||
android:theme="@style/Theme.MaterialComponents.DayNight.Bridge"
|
android:visibility="visible" />
|
||||||
android:visibility="gone"
|
|
||||||
app:chipIconVisible="false"
|
|
||||||
app:chipStrokeWidth="0dp"
|
|
||||||
tools:text="Downloaded • 1080p"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:id="@+id/detail_meta_info_separator"
|
android:id="@+id/detail_meta_info_separator"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="1px"
|
android:layout_height="1px"
|
||||||
android:layout_below="@id/detail_download_status_chip"
|
android:layout_below="@id/detail_download_status_compose"
|
||||||
android:layout_marginLeft="8dp"
|
android:layout_marginLeft="8dp"
|
||||||
android:layout_marginRight="8dp"
|
android:layout_marginRight="8dp"
|
||||||
android:background="?attr/separator_color" />
|
android:background="?attr/separator_color" />
|
||||||
|
|
|
@ -25,10 +25,8 @@
|
||||||
<string name="download_action_delete">Delete file</string>
|
<string name="download_action_delete">Delete file</string>
|
||||||
<string name="download_action_remove_link">Remove link</string>
|
<string name="download_action_remove_link">Remove link</string>
|
||||||
<string name="download_link_removed">Download link removed</string>
|
<string name="download_link_removed">Download link removed</string>
|
||||||
<string name="download_relinked">Download relinked</string>
|
|
||||||
<string name="download_open_failed">Unable to open downloaded file</string>
|
<string name="download_open_failed">Unable to open downloaded file</string>
|
||||||
<string name="download_folder_open_failed">Unable to open folder</string>
|
<string name="download_folder_open_failed">Unable to open folder</string>
|
||||||
<string name="download_relink_failed">Unable to relink file</string>
|
|
||||||
<string name="download_delete_failed">Unable to delete downloaded file</string>
|
<string name="download_delete_failed">Unable to delete downloaded file</string>
|
||||||
<string name="download_deleted">Deleted downloaded file</string>
|
<string name="download_deleted">Deleted downloaded file</string>
|
||||||
<string name="search">Search</string>
|
<string name="search">Search</string>
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<style name="Theme.MaterialComponents.DayNight.BottomSheet" parent="Theme.MaterialComponents.DayNight">
|
|
||||||
<item name="textAppearanceBody1">@style/TextAppearance.MaterialComponents.Body1</item>
|
|
||||||
<item name="textAppearanceBody2">@style/TextAppearance.MaterialComponents.Body2</item>
|
|
||||||
<item name="textAppearanceHeadline6">@style/TextAppearance.MaterialComponents.Headline6</item>
|
|
||||||
<item name="textAppearanceSubtitle1">@style/TextAppearance.MaterialComponents.Body2</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
Loading…
Add table
Add a link
Reference in a new issue